|
1 | 1 | //! # The Ideal Stocking Stuffer
|
2 | 2 | //!
|
3 |
| -//! This solution relies on brute forcing combinations as quickly as possible using our own internal |
| 3 | +//! This solution relies on brute forcing combinations as quickly as possible using an internal |
4 | 4 | //! implementation of the [`MD5`] hashing algorithm.
|
5 | 5 | //!
|
6 |
| -//! Using the [`write!`] macro to join the secret key to the number is quite slow. To speed things |
7 |
| -//! up we reuse the same `u8` buffer, incrementing digits one at a time. If a carry occurs we |
8 |
| -//! propagate from right to left. Hitting the start of the secret key means that we have |
9 |
| -//! transitioned to a new power of ten, for example from 9 to 10 or 99 to 100, so we increase the |
10 |
| -//! size of the buffer by one. |
| 6 | +//! Each number's hash is independent of the others, so we speed things up by using threading |
| 7 | +//! to search in parallel in blocks of 1000 numbers at a time. |
| 8 | +//! |
| 9 | +//! Using the [`format!`] macro to join the secret key to the number is quite slow. To go faster |
| 10 | +//! we reuse the same `u8` buffer, incrementing digits one at a time. |
| 11 | +//! The numbers from 1 to 999 are handled specially. |
11 | 12 | //!
|
12 | 13 | //! Interestingly the total time to solve this problem is *extremely* sensitive to the secret key
|
13 | 14 | //! provided as input. For example my key required ~10⁷ iterations to find the answer to part two.
|
14 | 15 | //! However for unit testing, I was able to randomly find a value that takes only 455 iterations,
|
15 | 16 | //! about 22,000 times faster!
|
16 | 17 | //!
|
17 | 18 | //! [`MD5`]: crate::util::md5
|
18 |
| -//! [`write!`]: std::write |
| 19 | +//! [`format!`]: std::format |
19 | 20 | use crate::util::md5::hash;
|
| 21 | +use std::sync::atomic::Ordering::Relaxed; |
| 22 | +use std::sync::atomic::{AtomicBool, AtomicU32}; |
| 23 | +use std::sync::mpsc::{channel, Sender}; |
| 24 | +use std::sync::Arc; |
| 25 | +use std::thread; |
20 | 26 |
|
21 |
| -pub fn parse(input: &str) -> &[u8] { |
22 |
| - input.trim().as_bytes() |
23 |
| -} |
| 27 | +type Input = (u32, u32); |
24 | 28 |
|
25 |
| -pub fn part1(input: &[u8]) -> u32 { |
26 |
| - find(input, 0xfffff000) |
| 29 | +enum Found { |
| 30 | + First(u32), |
| 31 | + Second(u32), |
27 | 32 | }
|
28 | 33 |
|
29 |
| -pub fn part2(input: &[u8]) -> u32 { |
30 |
| - find(input, 0xffffff00) |
31 |
| -} |
| 34 | +pub fn parse(input: &str) -> Input { |
| 35 | + let prefix = input.trim().to_string(); |
| 36 | + let done = Arc::new(AtomicBool::new(false)); |
| 37 | + let counter = Arc::new(AtomicU32::new(1000)); |
| 38 | + let (tx, rx) = channel::<Found>(); |
32 | 39 |
|
33 |
| -fn find(input: &[u8], mask: u32) -> u32 { |
34 |
| - let mut count = 0; |
35 |
| - let mut size = input.len(); |
36 |
| - let mut buffer = [b'0'; 32]; |
37 |
| - |
38 |
| - buffer[..size].copy_from_slice(input); |
39 |
| - |
40 |
| - loop { |
41 |
| - count += 1; |
42 |
| - |
43 |
| - let mut index = size; |
44 |
| - loop { |
45 |
| - if buffer[index] < b'9' { |
46 |
| - buffer[index] += 1; |
47 |
| - break; |
48 |
| - } else if index == input.len() { |
49 |
| - buffer[index] = b'1'; |
50 |
| - size += 1; |
51 |
| - break; |
| 40 | + // Handle the first 999 numbers specially as the number of digits varies. |
| 41 | + for n in 1..1000 { |
| 42 | + let string = format!("{prefix}{n}"); |
| 43 | + check_hash(string.as_bytes(), n, &tx); |
| 44 | + } |
| 45 | + |
| 46 | + // Use as many cores as possible to parallelize the search. |
| 47 | + for _ in 0..thread::available_parallelism().unwrap().get() { |
| 48 | + let prefix = prefix.clone(); |
| 49 | + let done = done.clone(); |
| 50 | + let counter = counter.clone(); |
| 51 | + let tx = tx.clone(); |
| 52 | + thread::spawn(move || worker(&prefix, &done, &counter, &tx)); |
| 53 | + } |
| 54 | + |
| 55 | + // Explicitly drop the reference to the sender object so that when all search threads finish, |
| 56 | + // there will be no remaining references. When this happens `rx.recv` will return |
| 57 | + // `Error` and exit the loop below. This ensures we wait to receive results from all threads, |
| 58 | + // to handle the edge case where two values could be close together and found out of order. |
| 59 | + drop(tx); |
| 60 | + |
| 61 | + // We could potentially find multiple values, keep only the first occurence of each one. |
| 62 | + let mut first = u32::MAX; |
| 63 | + let mut second = u32::MAX; |
| 64 | + |
| 65 | + while let Ok(message) = rx.recv() { |
| 66 | + match message { |
| 67 | + Found::First(value) => { |
| 68 | + first = first.min(value); |
| 69 | + } |
| 70 | + Found::Second(value) => { |
| 71 | + second = second.min(value); |
| 72 | + done.store(true, Relaxed); |
52 | 73 | }
|
53 |
| - buffer[index] = b'0'; |
54 |
| - index -= 1; |
55 | 74 | }
|
| 75 | + } |
| 76 | + |
| 77 | + (first, second) |
| 78 | +} |
| 79 | + |
| 80 | +pub fn part1(input: &Input) -> u32 { |
| 81 | + input.0 |
| 82 | +} |
| 83 | + |
| 84 | +pub fn part2(input: &Input) -> u32 { |
| 85 | + input.1 |
| 86 | +} |
| 87 | + |
| 88 | +fn check_hash(buffer: &[u8], n: u32, tx: &Sender<Found>) { |
| 89 | + let (result, ..) = hash(buffer); |
| 90 | + |
| 91 | + if result & 0xffffff00 == 0 { |
| 92 | + let _ = tx.send(Found::Second(n)); |
| 93 | + } else if result & 0xfffff000 == 0 { |
| 94 | + let _ = tx.send(Found::First(n)); |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +fn worker(prefix: &str, done: &Arc<AtomicBool>, counter: &Arc<AtomicU32>, tx: &Sender<Found>) { |
| 99 | + while !done.load(Relaxed) { |
| 100 | + let start = counter.fetch_add(1000, Relaxed); |
| 101 | + let string = format!("{prefix}{start}"); |
| 102 | + let size = string.len() - 3; |
| 103 | + let mut buffer = string.as_bytes().to_vec(); |
56 | 104 |
|
57 |
| - if hash(&buffer[..(size + 1)]).0 & mask == 0 { |
58 |
| - return count; |
| 105 | + for n in 0..1000 { |
| 106 | + // Format macro is very slow, so update digits directly |
| 107 | + buffer[size] = b'0' + (n / 100) as u8; |
| 108 | + buffer[size + 1] = b'0' + ((n / 10) % 10) as u8; |
| 109 | + buffer[size + 2] = b'0' + (n % 10) as u8; |
| 110 | + check_hash(&buffer, start + n, tx); |
59 | 111 | }
|
60 | 112 | }
|
61 | 113 | }
|
0 commit comments