-
Notifications
You must be signed in to change notification settings - Fork 10
/
day09.rs
157 lines (132 loc) · 5.26 KB
/
day09.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! # Disk Fragmenter
//!
//! ## Part One
//!
//! Computes the checksum by simultaneously scanning forward for free blocks and
//! backwards for files. No memory is allocated which makes it very fast.
//!
//! ## Part Two
//!
//! We build 10 [min heaps](https://en.wikipedia.org/wiki/Heap_(data_structure)) in an array to
//! store the free space offsets. The index of the array implicitly stores the size of the
//! free block. The heaps are implemented as a simple reversed `vec`. Usually items are added
//! directly to the top of the heap, so this is faster than a real heap.
//!
//! When moving a file to a free block, the corresponding heap is popped and then any leftover
//! space is pushed back to the heap at a smaller index. The heap at index zero is not used
//! but makes the indexing easier.
/// [Triangular numbers](https://en.wikipedia.org/wiki/Triangular_number) offset by two.
/// Files can be a max size of 9 so we only need the first 10 values, including zero to make
/// indexing easier.
const TRIANGLE: [usize; 10] = [0, 0, 1, 3, 6, 10, 15, 21, 28, 36];
/// Remove any trailing newlines and convert to `usize`.
pub fn parse(input: &str) -> Vec<usize> {
input.trim().bytes().map(|b| (b - b'0') as usize).collect()
}
/// Block by block checksum comparison that doesn't allocate any memory.
pub fn part1(disk: &[usize]) -> usize {
// Start at the first free block and the last file.
let mut left = 0;
let mut right = disk.len() - 2 + disk.len() % 2;
let mut needed = disk[right];
let mut block = 0;
let mut checksum = 0;
while left < right {
// When moving to the next free block, add the checksum for the file we're skipping over.
(checksum, block) = update(checksum, block, left, disk[left]);
let mut available = disk[left + 1];
left += 2;
while available > 0 {
if needed == 0 {
if left == right {
break;
}
right -= 2;
needed = disk[right];
}
// Take as much space as possible from the current free block range.
let size = needed.min(available);
(checksum, block) = update(checksum, block, right, size);
available -= size;
needed -= size;
}
}
// Account for any remaining file blocks left over.
(checksum, _) = update(checksum, block, right, needed);
checksum
}
#[allow(clippy::needless_range_loop)]
pub fn part2(disk: &[usize]) -> usize {
let mut block = 0;
let mut checksum = 0;
let mut free: Vec<_> = (0..10).map(|_| Vec::with_capacity(1_100)).collect();
// Build a min-heap (leftmost free block first) where the size of each block is
// implicit in the index of the array.
for (index, &size) in disk.iter().enumerate() {
if index % 2 == 1 && size > 0 {
free[size].push(block);
}
block += size;
}
// Add sentinel value and reverse vecs so that smallest blocks are last.
for i in 0..10 {
free[i].push(block);
free[i].reverse();
}
for (index, &size) in disk.iter().enumerate().rev() {
block -= size;
// Count any previous free blocks to decrement block offset correctly.
if index % 2 == 1 {
continue;
}
// Find the leftmost free block that can fit the file (if any).
let mut next_block = block;
let mut next_index = usize::MAX;
for i in size..free.len() {
let top = free[i].len() - 1;
let first = free[i][top];
if first < next_block {
next_block = first;
next_index = i;
}
}
// We can make smaller free block from bigger blocks but not the other way around.
// As an optimization if all blocks of the biggest size are after our position then
// we can ignore them.
if !free.is_empty() {
let biggest = free.len() - 1;
let top = free[biggest].len() - 1;
if free[biggest][top] > block {
free.pop();
}
}
// Update the checksum with the file's location (possibly unchanged).
let id = index / 2;
let extra = next_block * size + TRIANGLE[size];
checksum += id * extra;
// If we used a free block, remove then add back any leftover space.
if next_index != usize::MAX {
free[next_index].pop();
// Insert the new smaller block into the correct location.
// Most frequently this is directly at the end of the vector so even though this
// is technically `O(n)`, in practice it's faster than a real heap.
let to = next_index - size;
if to > 0 {
let mut i = free[to].len();
let value = next_block + size;
while free[to][i - 1] < value {
i -= 1;
}
free[to].insert(i, value);
}
}
}
checksum
}
/// Convenience function to update checksum based on file location and size.
#[inline]
fn update(checksum: usize, block: usize, index: usize, size: usize) -> (usize, usize) {
let id = index / 2;
let extra = block * size + TRIANGLE[size];
(checksum + id * extra, block + size)
}