Skip to content

Commit b3d91b2

Browse files
committed
Faster implementation using reversed vec as a heap
1 parent cf7a4d0 commit b3d91b2

File tree

2 files changed

+40
-21
lines changed

2 files changed

+40
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
8080
| 6 | [Guard Gallivant](https://adventofcode.com/2024/day/6) | [Source](src/year2024/day06.rs) | 386 |
8181
| 7 | [Bridge Repair](https://adventofcode.com/2024/day/7) | [Source](src/year2024/day07.rs) | 136 |
8282
| 8 | [Resonant Collinearity](https://adventofcode.com/2024/day/8) | [Source](src/year2024/day08.rs) | 8 |
83-
| 9 | [Disk Fragmenter](https://adventofcode.com/2024/day/9) | [Source](src/year2024/day09.rs) | 163 |
83+
| 9 | [Disk Fragmenter](https://adventofcode.com/2024/day/9) | [Source](src/year2024/day09.rs) | 106 |
8484
| 10 | [Hoof It](https://adventofcode.com/2024/day/10) | [Source](src/year2024/day10.rs) | 38 |
8585

8686
## 2023

src/year2024/day09.rs

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@
99
//!
1010
//! We build 10 [min heaps](https://en.wikipedia.org/wiki/Heap_(data_structure)) in an array to
1111
//! store the free space offsets. The index of the array implicitly stores the size of the
12-
//! free block.
12+
//! free block. The heaps are implemented as a simple reversed `vec`. Usually items are added
13+
//! directly to the top of the heap, so this is faster than a real heap.
1314
//!
1415
//! When moving a file to a free block, the corresponding heap is popped and then any leftover
1516
//! space is pushed back to the heap at a smaller index. The heap at index zero is not used
1617
//! but makes the indexing easier.
17-
use crate::util::heap::*;
1818
1919
/// [Triangular numbers](https://en.wikipedia.org/wiki/Triangular_number) offset by two.
2020
/// Files can be a max size of 9 so we only need the first 10 values, including zero to make
2121
/// indexing easier.
22-
const EXTRA: [usize; 10] = [0, 0, 1, 3, 6, 10, 15, 21, 28, 36];
22+
const TRIANGLE: [usize; 10] = [0, 0, 1, 3, 6, 10, 15, 21, 28, 36];
2323

2424
/// Remove any trailing newlines and convert to `usize`.
2525
pub fn parse(input: &str) -> Vec<usize> {
@@ -63,21 +63,28 @@ pub fn part1(disk: &[usize]) -> usize {
6363
checksum
6464
}
6565

66+
#[allow(clippy::needless_range_loop)]
6667
pub fn part2(disk: &[usize]) -> usize {
6768
let mut block = 0;
6869
let mut checksum = 0;
69-
let mut free: Vec<_> = (0..10).map(|_| MinHeap::with_capacity(1_000)).collect();
70+
let mut free: Vec<_> = (0..10).map(|_| Vec::with_capacity(1_100)).collect();
7071

7172
// Build a min-heap (leftmost free block first) where the size of each block is
7273
// implicit in the index of the array.
7374
for (index, &size) in disk.iter().enumerate() {
7475
if index % 2 == 1 && size > 0 {
75-
free[size].push(block, ());
76+
free[size].push(block);
7677
}
7778

7879
block += size;
7980
}
8081

82+
// Add sentinel value and reverse vecs so that smallest blocks are last.
83+
for i in 0..10 {
84+
free[i].push(block);
85+
free[i].reverse();
86+
}
87+
8188
for (index, &size) in disk.iter().enumerate().rev() {
8289
block -= size;
8390

@@ -90,38 +97,50 @@ pub fn part2(disk: &[usize]) -> usize {
9097
let mut next_block = block;
9198
let mut next_index = usize::MAX;
9299

93-
#[allow(clippy::needless_range_loop)]
94100
for i in size..free.len() {
95-
if let Some((&first, ())) = free[i].peek() {
96-
if first < next_block {
97-
next_block = first;
98-
next_index = i;
99-
}
101+
let top = free[i].len() - 1;
102+
let first = free[i][top];
103+
104+
if first < next_block {
105+
next_block = first;
106+
next_index = i;
100107
}
101108
}
102109

103110
// We can make smaller free block from bigger blocks but not the other way around.
104111
// As an optimization if all blocks of the biggest size are after our position then
105112
// we can ignore them.
106113
if !free.is_empty() {
107-
let last = free.len() - 1;
108-
if let Some((&first, ())) = free[last].peek() {
109-
if first > block {
110-
free.pop();
111-
}
114+
let biggest = free.len() - 1;
115+
let top = free[biggest].len() - 1;
116+
117+
if free[biggest][top] > block {
118+
free.pop();
112119
}
113120
}
114121

115122
// Update the checksum with the file's location (possibly unchanged).
116123
let id = index / 2;
117-
let extra = next_block * size + EXTRA[size];
124+
let extra = next_block * size + TRIANGLE[size];
118125
checksum += id * extra;
119126

120127
// If we used a free block, remove then add back any leftover space.
121128
if next_index != usize::MAX {
122129
free[next_index].pop();
123-
if size < next_index {
124-
free[next_index - size].push(next_block + size, ());
130+
131+
// Insert the new smaller block into the correct location.
132+
// Most frequently this is directly at the end of the vector so even though this
133+
// is technically `O(n)`, in practice it's faster than a real heap.
134+
let to = next_index - size;
135+
if to > 0 {
136+
let mut i = free[to].len();
137+
let value = next_block + size;
138+
139+
while free[to][i - 1] < value {
140+
i -= 1;
141+
}
142+
143+
free[to].insert(i, value);
125144
}
126145
}
127146
}
@@ -133,6 +152,6 @@ pub fn part2(disk: &[usize]) -> usize {
133152
#[inline]
134153
fn update(checksum: usize, block: usize, index: usize, size: usize) -> (usize, usize) {
135154
let id = index / 2;
136-
let extra = block * size + EXTRA[size];
155+
let extra = block * size + TRIANGLE[size];
137156
(checksum + id * extra, block + size)
138157
}

0 commit comments

Comments
 (0)