Skip to content

Commit fa3cb11

Browse files
committed
Year 2024 Day 9
1 parent d886476 commit fa3cb11

File tree

8 files changed

+164
-0
lines changed

8 files changed

+164
-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/util/heap.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,9 @@ impl<K: Ord, V> MinHeap<K, V> {
5959
pub fn pop(&mut self) -> Option<(K, V)> {
6060
self.heap.pop().map(|w| (w.key, w.value))
6161
}
62+
63+
#[inline]
64+
pub fn peek(&self) -> Option<(&K, &V)> {
65+
self.heap.peek().map(|w| (&w.key, &w.value))
66+
}
6267
}

src/year2024/day09.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 free = 0;
33+
let mut file = disk.len() + disk.len() % 2;
34+
35+
let mut available = 0;
36+
let mut needed = 0;
37+
38+
let mut block = 0;
39+
let mut checksum = 0;
40+
41+
while free < file {
42+
// Take as much space as possible from the current free block range.
43+
let size = needed.min(available);
44+
(checksum, block) = update(checksum, block, file, size);
45+
available -= size;
46+
needed -= size;
47+
48+
// One or both of "available" and "free" could be zero.
49+
if needed == 0 {
50+
file -= 2;
51+
needed = disk[file];
52+
}
53+
54+
// When moving to the next free block, add the checksum for the file we're skipping over.
55+
if available == 0 {
56+
let size = disk[free];
57+
(checksum, block) = update(checksum, block, free, size);
58+
available = disk[free + 1];
59+
free += 2;
60+
}
61+
}
62+
63+
// Account for any remaining file blocks left over.
64+
(checksum, _) = update(checksum, block, file, needed);
65+
checksum
66+
}
67+
68+
pub fn part2(disk: &[usize]) -> usize {
69+
let mut block = 0;
70+
let mut checksum = 0;
71+
let mut free: Vec<_> = (0..10).map(|_| MinHeap::with_capacity(1_000)).collect();
72+
73+
// Build a min-heap (leftmost free block first) where the size of each block is
74+
// implicit in the index of the array.
75+
for (index, &size) in disk.iter().enumerate() {
76+
if index % 2 == 1 && size > 0 {
77+
free[size].push(block, ());
78+
}
79+
80+
block += size;
81+
}
82+
83+
for (index, &size) in disk.iter().enumerate().rev() {
84+
block -= size;
85+
86+
// Count any previous free blocks to decrement block offset correctly.
87+
if index % 2 == 1 {
88+
continue;
89+
}
90+
91+
// Find the leftmost free block that can fit the file (if any).
92+
let mut next_block = block;
93+
let mut next_index = usize::MAX;
94+
95+
for (i, heap) in free.iter().enumerate().skip(size) {
96+
if let Some((&first, ())) = heap.peek() {
97+
if first < next_block {
98+
next_block = first;
99+
next_index = i;
100+
}
101+
}
102+
}
103+
104+
// We can make smaller free block from bigger blocks but not the other way around.
105+
// As an optimization if all blocks of the biggest size are after our position then
106+
// we can ignore them.
107+
if !free.is_empty() {
108+
let last = free.len() - 1;
109+
if let Some((&first, ())) = free[last].peek() {
110+
if first > block {
111+
free.pop();
112+
}
113+
}
114+
}
115+
116+
// Update the checksum with the file's location (possibly unchanged).
117+
let id = index / 2;
118+
let extra = next_block * size + EXTRA[size];
119+
checksum += id * extra;
120+
121+
// If we used a free block, remove then add back any leftover space.
122+
if next_index != usize::MAX {
123+
free[next_index].pop();
124+
if size < next_index {
125+
free[next_index - size].push(next_block + size, ());
126+
}
127+
}
128+
}
129+
130+
checksum
131+
}
132+
133+
/// Convenience function to update checksum based on file location and size.
134+
#[inline]
135+
fn update(checksum: usize, block: usize, index: usize, size: usize) -> (usize, usize) {
136+
let id = index / 2;
137+
let extra = block * size + EXTRA[size];
138+
(checksum + id * extra, block + size)
139+
}

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)