aoc/year2016/
day14.rs

1//! # One-Time Pad
2//!
3//! Brute force slog through all possible keys, parallelized as much as possible. An optimization
4//! for part two is a quick method to convert `u32` to 8 ASCII digits.
5use crate::util::md5::*;
6use crate::util::thread::*;
7use std::collections::{BTreeMap, BTreeSet};
8use std::sync::Mutex;
9
10/// Atomics can be safely shared between threads.
11struct Shared<'a> {
12    input: &'a str,
13    part_two: bool,
14    iter: AtomicIter,
15    mutex: Mutex<Exclusive>,
16}
17
18/// Regular data structures need to be protected by a mutex.
19struct Exclusive {
20    threes: BTreeMap<i32, u32>,
21    fives: BTreeMap<i32, u32>,
22    found: BTreeSet<i32>,
23}
24
25pub fn parse(input: &str) -> &str {
26    input.trim()
27}
28
29/// Hash each key once.
30pub fn part1(input: &str) -> i32 {
31    generate_pad(input, false)
32}
33
34/// Hash each key an additional 2016 times.
35pub fn part2(input: &str) -> i32 {
36    generate_pad(input, true)
37}
38
39/// Find the first 64 keys that satisfy the rules.
40fn generate_pad(input: &str, part_two: bool) -> i32 {
41    let step = if cfg!(feature = "simd") { 32 } else { 1 };
42
43    let exclusive =
44        Exclusive { threes: BTreeMap::new(), fives: BTreeMap::new(), found: BTreeSet::new() };
45    let shared =
46        Shared { input, part_two, iter: AtomicIter::new(0, step), mutex: Mutex::new(exclusive) };
47
48    // Use as many cores as possible to parallelize the search.
49    #[cfg(not(feature = "simd"))]
50    spawn(|| scalar::worker(&shared));
51    #[cfg(feature = "simd")]
52    spawn(|| simd::worker(&shared));
53
54    let exclusive = shared.mutex.into_inner().unwrap();
55    *exclusive.found.iter().nth(63).unwrap()
56}
57
58/// Write the salt and integer index as ASCII characters.
59fn format_string(prefix: &str, n: i32) -> ([u8; 64], usize) {
60    let string = format!("{prefix}{n}");
61    let size = string.len();
62
63    let mut buffer = [0; 64];
64    buffer[0..size].copy_from_slice(string.as_bytes());
65
66    (buffer, size)
67}
68
69/// Quickly convert a `u32` to an array of 8 ASCII values.
70#[inline]
71fn to_ascii(n: u32) -> [u8; 8] {
72    // Spread each nibble into its own byte, for example `1234abcd` becomes `010203040a0b0c0d`.
73    let mut n = n as u64;
74    n = ((n << 16) & 0x0000ffff00000000) | (n & 0x000000000000ffff);
75    n = ((n << 8) & 0x00ff000000ff0000) | (n & 0x000000ff000000ff);
76    n = ((n << 4) & 0x0f000f000f000f00) | (n & 0x000f000f000f000f);
77
78    // If a digit is 0 to 9 then we need to add `0x30` to convert to an ASCII digit.
79    // For digits from 10 to 15 we need to further add `0x27` to convert to lowercase ASCII.
80    // Steps:
81    // * Add 6 to each digit
82    // * If digit is 10 or higher then the highest bit in each nibble will be set
83    // * Shift this bit to create a mask
84    // * Multiply mask by 0x27 to get ASCII conversion offset
85    // For example mask of `010203040a0b0c0d` is `0000000001010101`.
86
87    let mask = ((n + 0x0606060606060606) >> 4) & 0x0101010101010101;
88    n = n + 0x3030303030303030 + 0x27 * mask;
89    n.to_be_bytes()
90}
91
92#[cfg(not(feature = "simd"))]
93mod scalar {
94    use super::*;
95
96    pub(super) fn worker(shared: &Shared<'_>) {
97        while let Some(n) = shared.iter.next() {
98            // Get the next key to check.
99            let n = n as i32;
100
101            // Calculate the hash.
102            let (mut buffer, size) = format_string(shared.input, n);
103            let mut result = hash(&mut buffer, size);
104
105            if shared.part_two {
106                for _ in 0..2016 {
107                    buffer[0..8].copy_from_slice(&to_ascii(result[0]));
108                    buffer[8..16].copy_from_slice(&to_ascii(result[1]));
109                    buffer[16..24].copy_from_slice(&to_ascii(result[2]));
110                    buffer[24..32].copy_from_slice(&to_ascii(result[3]));
111                    result = hash(&mut buffer, 32);
112                }
113            }
114
115            check(shared, n, result);
116        }
117    }
118
119    /// Check for sequences of 3 or 5 consecutive matching digits.
120    fn check(shared: &Shared<'_>, n: i32, hash: [u32; 4]) {
121        let [a, b, c, d] = hash;
122
123        let mut prev = u32::MAX;
124        let mut same = 1;
125        let mut three = 0;
126        let mut five = 0;
127
128        for mut word in [d, c, b, a] {
129            for _ in 0..8 {
130                let next = word & 0xf;
131
132                if next == prev {
133                    same += 1;
134                } else {
135                    same = 1;
136                }
137
138                if same == 3 {
139                    three = 1 << next;
140                }
141                if same == 5 {
142                    five |= 1 << next;
143                }
144
145                word >>= 4;
146                prev = next;
147            }
148        }
149
150        if three != 0 || five != 0 {
151            let mut exclusive = shared.mutex.lock().unwrap();
152            let mut candidates = Vec::new();
153
154            // Compare against all 5 digit sequences.
155            if three != 0 {
156                exclusive.threes.insert(n, three);
157
158                for (_, mask) in exclusive.fives.range(n + 1..n + 1001) {
159                    if three & mask != 0 {
160                        candidates.push(n);
161                    }
162                }
163            }
164
165            // Compare against all 3 digit sequences.
166            if five != 0 {
167                exclusive.fives.insert(n, five);
168
169                for (&index, &mask) in exclusive.threes.range(n - 1000..n) {
170                    if five & mask != 0 {
171                        candidates.push(index);
172                    }
173                }
174            }
175
176            // Add any matching keys found, finishing once we have at least 64 keys.
177            exclusive.found.extend(candidates);
178
179            if exclusive.found.len() >= 64 {
180                shared.iter.stop();
181            }
182        }
183    }
184}
185
186#[cfg(feature = "simd")]
187mod simd {
188    use super::*;
189    use crate::util::bitset::*;
190    use crate::util::md5::simd::hash_fixed;
191    use std::simd::cmp::SimdPartialEq as _;
192    use std::simd::*;
193
194    /// Use SIMD to compute hashes in parallel in blocks of 32.
195    #[cfg(feature = "simd")]
196    #[expect(clippy::needless_range_loop)]
197    pub(super) fn worker(shared: &Shared<'_>) {
198        let mut result = [Simd::splat(0); 4];
199        let mut buffers = [[0; 64]; 32];
200
201        while let Some(start) = shared.iter.next() {
202            // Get the next key to check.
203            let start = start as i32;
204
205            // Calculate the hash.
206            for i in 0..32 {
207                let (mut buffer, size) = format_string(shared.input, start + i as i32);
208                let [a, b, c, d] = hash(&mut buffer, size);
209
210                result[0][i] = a;
211                result[1][i] = b;
212                result[2][i] = c;
213                result[3][i] = d;
214            }
215
216            if shared.part_two {
217                for _ in 0..2016 {
218                    for i in 0..32 {
219                        buffers[i][0..8].copy_from_slice(&to_ascii(result[0][i]));
220                        buffers[i][8..16].copy_from_slice(&to_ascii(result[1][i]));
221                        buffers[i][16..24].copy_from_slice(&to_ascii(result[2][i]));
222                        buffers[i][24..32].copy_from_slice(&to_ascii(result[3][i]));
223                    }
224                    result = hash_fixed(&mut buffers, 32);
225                }
226            }
227
228            check(shared, start, &result);
229        }
230    }
231
232    /// Check for sequences of 3 or 5 consecutive matching digits.
233    #[inline]
234    fn check(shared: &Shared<'_>, start: i32, hash: &[Simd<u32, 32>; 4]) {
235        let &[a, b, c, d] = hash;
236
237        let mut prev: Simd<u32, 32> = Simd::splat(u32::MAX);
238        let mut same: Simd<u32, 32> = Simd::splat(1);
239        let mut three: Simd<u32, 32> = Simd::splat(0);
240        let mut five: Simd<u32, 32> = Simd::splat(0);
241
242        for mut word in [d, c, b, a] {
243            for _ in 0..8 {
244                let next = word & Simd::splat(0xf);
245                same = next.simd_eq(prev).select(same + Simd::splat(1), Simd::splat(1));
246
247                three = same.simd_eq(Simd::splat(3)).select(Simd::splat(1) << next, three);
248                five |= same.simd_eq(Simd::splat(5)).select(Simd::splat(1) << next, Simd::splat(0));
249
250                word >>= 4;
251                prev = next;
252            }
253        }
254
255        let three_mask = three.simd_ne(Simd::splat(0)).to_bitmask();
256        let five_mask = five.simd_ne(Simd::splat(0)).to_bitmask();
257
258        if three_mask != 0 || five_mask != 0 {
259            let mut exclusive = shared.mutex.lock().unwrap();
260            let mut candidates = Vec::new();
261
262            for i in three_mask.biterator() {
263                let three = three[i];
264                let n = start + i as i32;
265
266                // Compare against all 5 digit sequences.
267                if three != 0 {
268                    exclusive.threes.insert(n, three);
269
270                    for (_, mask) in exclusive.fives.range(n + 1..n + 1001) {
271                        if three & mask != 0 {
272                            candidates.push(n);
273                        }
274                    }
275                }
276            }
277
278            for i in five_mask.biterator() {
279                let five = five[i];
280                let n = start + i as i32;
281
282                // Compare against all 3 digit sequences.
283                if five != 0 {
284                    exclusive.fives.insert(n, five);
285
286                    for (&index, &mask) in exclusive.threes.range(n - 1000..n) {
287                        if five & mask != 0 {
288                            candidates.push(index);
289                        }
290                    }
291                }
292            }
293
294            // Add any matching keys found, finishing once we have at least 64 keys.
295            exclusive.found.extend(candidates);
296
297            if exclusive.found.len() >= 64 {
298                shared.iter.stop();
299            }
300        }
301    }
302}