Skip to content

Commit e5bfc16

Browse files
committed
Year 2024 Day 9
1 parent a81d351 commit e5bfc16

File tree

7 files changed

+158
-0
lines changed

7 files changed

+158
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +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 |
8384

8485
## 2023
8586

benches/benchmark.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,5 @@ mod year2024 {
300300
benchmark!(year2024, day06);
301301
benchmark!(year2024, day07);
302302
benchmark!(year2024, day08);
303+
benchmark!(year2024, day09);
303304
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,5 @@ pub mod year2024 {
299299
pub mod day06;
300300
pub mod day07;
301301
pub mod day08;
302+
pub mod day09;
302303
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,5 +369,6 @@ fn year2024() -> Vec<Solution> {
369369
solution!(year2024, day06),
370370
solution!(year2024, day07),
371371
solution!(year2024, day08),
372+
solution!(year2024, day09),
372373
]
373374
}

src/year2024/day09.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! # Disk Fragmenter
2+
//!
3+
//! ## Part One
4+
//!
5+
//! Computes the checksum by simultaneously scanning forward for free blocks and
6+
//! backwards for files. No memory is allocated which makes it very fast.
7+
//!
8+
//! ## Part Two
9+
//!
10+
//! We build 10 [min heaps](https://en.wikipedia.org/wiki/Heap_(data_structure)) in an array to
11+
//! store the free space offsets. The index of the array implicitly stores the size of the
12+
//! free block.
13+
//!
14+
//! When moving a file to a free block, the corresponding heap is popped and then any leftover
15+
//! space is pushed back to the heap at a smaller index. The heap at index zero is not used
16+
//! but makes the indexing easier.
17+
use crate::util::heap::*;
18+
19+
/// [Triangular numbers](https://en.wikipedia.org/wiki/Triangular_number) offset by two.
20+
/// Files can be a max size of 9 so we only need the first 10 values, including zero to make
21+
/// indexing easier.
22+
const EXTRA: [usize; 10] = [0, 0, 1, 3, 6, 10, 15, 21, 28, 36];
23+
24+
/// Remove any trailing newlines and convert to `usize`.
25+
pub fn parse(input: &str) -> Vec<usize> {
26+
input.trim().bytes().map(|b| (b - b'0') as usize).collect()
27+
}
28+
29+
/// Block by block checksum comparison that doesn't allocate any memory.
30+
pub fn part1(disk: &[usize]) -> usize {
31+
// Start at the first free block and the last file.
32+
let mut left = 0;
33+
let mut right = disk.len() - 2 + disk.len() % 2;
34+
let mut needed = disk[right];
35+
let mut block = 0;
36+
let mut checksum = 0;
37+
38+
while left < right {
39+
// When moving to the next free block, add the checksum for the file we're skipping over.
40+
(checksum, block) = update(checksum, block, left, disk[left]);
41+
let mut available = disk[left + 1];
42+
left += 2;
43+
44+
while available > 0 {
45+
if needed == 0 {
46+
if left == right {
47+
break;
48+
}
49+
right -= 2;
50+
needed = disk[right];
51+
}
52+
53+
// Take as much space as possible from the current free block range.
54+
let size = needed.min(available);
55+
(checksum, block) = update(checksum, block, right, size);
56+
available -= size;
57+
needed -= size;
58+
}
59+
}
60+
61+
// Account for any remaining file blocks left over.
62+
(checksum, _) = update(checksum, block, right, needed);
63+
checksum
64+
}
65+
66+
pub fn part2(disk: &[usize]) -> usize {
67+
let mut block = 0;
68+
let mut checksum = 0;
69+
let mut free: Vec<_> = (0..10).map(|_| MinHeap::with_capacity(1_000)).collect();
70+
71+
// Build a min-heap (leftmost free block first) where the size of each block is
72+
// implicit in the index of the array.
73+
for (index, &size) in disk.iter().enumerate() {
74+
if index % 2 == 1 && size > 0 {
75+
free[size].push(block, ());
76+
}
77+
78+
block += size;
79+
}
80+
81+
for (index, &size) in disk.iter().enumerate().rev() {
82+
block -= size;
83+
84+
// Count any previous free blocks to decrement block offset correctly.
85+
if index % 2 == 1 {
86+
continue;
87+
}
88+
89+
// Find the leftmost free block that can fit the file (if any).
90+
let mut next_block = block;
91+
let mut next_index = usize::MAX;
92+
93+
#[allow(clippy::needless_range_loop)]
94+
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+
}
100+
}
101+
}
102+
103+
// We can make smaller free block from bigger blocks but not the other way around.
104+
// As an optimization if all blocks of the biggest size are after our position then
105+
// we can ignore them.
106+
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+
}
112+
}
113+
}
114+
115+
// Update the checksum with the file's location (possibly unchanged).
116+
let id = index / 2;
117+
let extra = next_block * size + EXTRA[size];
118+
checksum += id * extra;
119+
120+
// If we used a free block, remove then add back any leftover space.
121+
if next_index != usize::MAX {
122+
free[next_index].pop();
123+
if size < next_index {
124+
free[next_index - size].push(next_block + size, ());
125+
}
126+
}
127+
}
128+
129+
checksum
130+
}
131+
132+
/// Convenience function to update checksum based on file location and size.
133+
#[inline]
134+
fn update(checksum: usize, block: usize, index: usize, size: usize) -> (usize, usize) {
135+
let id = index / 2;
136+
let extra = block * size + EXTRA[size];
137+
(checksum + id * extra, block + size)
138+
}

tests/test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,5 @@ mod year2024 {
289289
mod day06_test;
290290
mod day07_test;
291291
mod day08_test;
292+
mod day09_test;
292293
}

tests/year2024/day09_test.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use aoc::year2024::day09::*;
2+
3+
const EXAMPLE: &str = "2333133121414131402";
4+
5+
#[test]
6+
fn part1_test() {
7+
let input = parse(EXAMPLE);
8+
assert_eq!(part1(&input), 1928);
9+
}
10+
11+
#[test]
12+
fn part2_test() {
13+
let input = parse(EXAMPLE);
14+
assert_eq!(part2(&input), 2858);
15+
}

0 commit comments

Comments
 (0)