year2024/
day09.rs

1use std::collections::VecDeque;
2use utils::prelude::*;
3
4/// Implementing disk defragmentation.
5#[derive(Clone, Debug)]
6pub struct Day09<'a> {
7    input: &'a str,
8    total_length: u32,
9}
10
11impl<'a> Day09<'a> {
12    pub fn new(input: &'a str, _: InputType) -> Result<Self, InputError> {
13        let mut pos = 0;
14        for b in input.bytes() {
15            if b.is_ascii_digit() {
16                pos += (b - b'0') as u32;
17            } else {
18                return Err(InputError::new(input, b as char, "expected digit"));
19            }
20        }
21        Ok(Self {
22            input,
23            total_length: pos,
24        })
25    }
26
27    #[must_use]
28    pub fn part1(&self) -> u64 {
29        let mut free_iterator = self.free_space_iter();
30        let mut file_iterator = self.rev_file_iter();
31
32        let mut checksum = 0;
33        let (mut free_pos, mut free_len) = (0, 0);
34        let (mut file_pos, mut file_len, mut file_id) = (0, 0, 0);
35        loop {
36            if free_len == 0 {
37                let Some(next) = free_iterator.next() else {
38                    break;
39                };
40                (free_pos, free_len) = next;
41            }
42            if file_len == 0 {
43                let Some(next) = file_iterator.next() else {
44                    break;
45                };
46                (file_pos, file_len, file_id) = next;
47            }
48
49            if free_pos > file_pos {
50                break;
51            }
52
53            let len = free_len.min(file_len);
54            checksum += Self::file_checksum(free_pos, len, file_id);
55            free_pos += len;
56            free_len -= len;
57            file_len -= len;
58        }
59
60        // Handle the remaining file_len that wasn't moved into a free space, if any
61        if file_len > 0 {
62            checksum += Self::file_checksum(file_pos, file_len, file_id);
63        }
64
65        // Handle remaining complete files, if any
66        for (file_pos, file_len, file_id) in file_iterator {
67            checksum += Self::file_checksum(file_pos, file_len, file_id);
68        }
69
70        checksum
71    }
72
73    #[must_use]
74    pub fn part2(&self) -> u64 {
75        // Each VecDeque stores a lazily populated sorted list of free space positions that are i+1
76        // long. Elements are always popped from the start of the deque, and newly parsed free
77        // spaces are always pushed to the end of the deque.
78        //
79        // When a file is moved and partially consumes a free space, the remainder has to be
80        // inserted into the correct index to maintain the sort order, which may be in the middle of
81        // the deque. However, this happens relatively infrequently in the input (<400 times),
82        // and in my testing using a deque is faster than using a BTreeSet or BinaryHeap.
83        let mut free_positions: [_; 9] =
84            std::array::from_fn(|_| VecDeque::with_capacity(self.input.len() / 10));
85        for (pos, len) in self.free_space_iter() {
86            free_positions[len as usize - 1].push_back(pos);
87        }
88
89        let mut checksum = 0;
90        let mut file_iterator = self.rev_file_iter();
91        for (file_pos, file_len, file_id) in &mut file_iterator {
92            let (mut next_pos, mut free_len) = (file_pos, 0);
93
94            // Check for free spaces at least as long as the file before the file's position
95            for len in file_len..=9 {
96                if let Some(&pos) = free_positions[len as usize - 1].front() {
97                    if pos < next_pos {
98                        (next_pos, free_len) = (pos, len);
99                    }
100                }
101            }
102
103            checksum += Self::file_checksum(next_pos, file_len, file_id);
104
105            if next_pos < file_pos {
106                // Remove now used free space, and add remaining space if any
107                free_positions[free_len as usize - 1].pop_front();
108                if free_len > file_len {
109                    let (pos, len) = (next_pos + file_len, free_len - file_len);
110                    match free_positions[len as usize - 1].binary_search(&pos) {
111                        Ok(_) => unreachable!(),
112                        Err(i) => free_positions[len as usize - 1].insert(i, pos),
113                    }
114                }
115            } else if file_len == 1 {
116                // If there is no space prior to the current file to move a 1-length file, then
117                // either there is no free space remaining or it is all after the remaining files
118                break;
119            }
120        }
121
122        // Handle remaining files, if any
123        for (file_pos, file_len, file_id) in file_iterator {
124            checksum += Self::file_checksum(file_pos, file_len, file_id);
125        }
126
127        checksum
128    }
129
130    fn free_space_iter(&self) -> impl Iterator<Item = (u32, u32)> + use<'_> {
131        struct FreeSpaceIterator<'a> {
132            input: &'a [u8],
133            pos: u32,
134        }
135
136        impl Iterator for FreeSpaceIterator<'_> {
137            type Item = (u32, u32);
138
139            #[inline]
140            fn next(&mut self) -> Option<Self::Item> {
141                while self.input.len() >= 2 {
142                    let file_len = (self.input[0] - b'0') as u32;
143                    let free_len = (self.input[1] - b'0') as u32;
144                    let free_pos = self.pos + file_len;
145
146                    self.input = &self.input[2..];
147                    self.pos += file_len + free_len;
148
149                    if free_len > 0 {
150                        return Some((free_pos, free_len));
151                    }
152                }
153                None
154            }
155        }
156
157        FreeSpaceIterator {
158            input: self.input.as_bytes(),
159            pos: 0,
160        }
161    }
162
163    fn rev_file_iter(&self) -> impl Iterator<Item = (u32, u32, u32)> + use<'_> {
164        struct ReverseFileIterator<'a> {
165            input: &'a [u8],
166            pos: u32,
167            id: u32,
168        }
169
170        impl Iterator for ReverseFileIterator<'_> {
171            type Item = (u32, u32, u32);
172
173            #[inline]
174            fn next(&mut self) -> Option<Self::Item> {
175                while !self.input.is_empty() {
176                    if self.input.len() % 2 == 0 {
177                        self.pos -= (self.input[self.input.len() - 1] - b'0') as u32;
178                        self.input = &self.input[..self.input.len() - 1];
179                    }
180
181                    let file_len = (self.input[self.input.len() - 1] - b'0') as u32;
182                    self.pos -= file_len;
183                    self.id -= 1;
184                    self.input = &self.input[..self.input.len() - 1];
185
186                    if file_len > 0 {
187                        return Some((self.pos, file_len, self.id));
188                    }
189                }
190                None
191            }
192        }
193
194        ReverseFileIterator {
195            input: self.input.as_bytes(),
196            pos: self.total_length,
197            id: self.input.len().div_ceil(2) as u32,
198        }
199    }
200
201    #[inline]
202    fn file_checksum(pos: u32, len: u32, id: u32) -> u64 {
203        let pos_sum = len * (2 * pos + len - 1) / 2;
204        pos_sum as u64 * id as u64
205    }
206}
207
208examples!(Day09<'_> -> (u64, u64) [
209    {input: "2333133121414131402", part1: 1928, part2: 2858},
210]);