aoc/year2025/
day10.rs

1//! # Factory
2use crate::util::bitset::*;
3use crate::util::parse::*;
4use crate::util::thread::*;
5use std::collections::BTreeSet;
6
7type Machine = (usize, Vec<usize>, Vec<i32>);
8type Input = (Vec<i32>, Vec<i32>);
9
10/// Each machine can be processed independently, parallelizing the work over multiple threads.
11pub fn parse(input: &str) -> Input {
12    let items: Vec<_> = input.lines().collect();
13
14    let result: Vec<Vec<_>> = spawn_parallel_iterator(&items, |iter| {
15        iter.map(|line| {
16            let machine = parse_machine(line);
17            (configure_lights(&machine), configure_joltages(&machine))
18        })
19        .collect()
20    });
21
22    result.into_iter().flatten().unzip()
23}
24
25pub fn part1(input: &Input) -> i32 {
26    let (presses, _) = input;
27    presses.iter().sum()
28}
29
30pub fn part2(input: &Input) -> i32 {
31    let (_, presses) = input;
32    presses.iter().sum()
33}
34
35/// Convert light patterns and buttons to bitmasks to speed up part one.
36fn parse_machine(line: &str) -> Machine {
37    let tokens: Vec<_> = line.split_ascii_whitespace().collect();
38    let last = tokens.len() - 1;
39
40    let lights = tokens[0]
41        .bytes()
42        .skip(1)
43        .enumerate()
44        .fold(0, |light, (i, b)| light | (usize::from(b == b'#') << i));
45    let buttons = tokens[1..last]
46        .iter()
47        .map(|token| token.iter_unsigned::<usize>().fold(0, |button, i| button | (1 << i)))
48        .collect();
49    let joltages = tokens[last].iter_signed::<i32>().collect();
50
51    (lights, buttons, joltages)
52}
53
54/// Check all patterns with one set bit, then pattern with two sets bits and so on, until we find
55/// a match.
56fn configure_lights((lights, buttons, _): &Machine) -> i32 {
57    let limit = 1 << buttons.len();
58    let mut set = 0;
59
60    loop {
61        set += 1;
62        let mut n = (1 << set) - 1;
63
64        while n < limit {
65            if *lights == n.biterator().fold(0, |acc, i| acc ^ buttons[i]) {
66                return set;
67            }
68            n = next_same_bits(n);
69        }
70    }
71}
72
73/// Find the next highest integer with the same number of one bits as the previous integer,
74/// for example 1011 => 1110.
75fn next_same_bits(n: i32) -> i32 {
76    let smallest = n & -n;
77    let ripple = n + smallest;
78    let ones = n ^ ripple;
79    let next = (ones >> 2) / smallest;
80    ripple | next
81}
82
83/// Convert the buttons and joltages to equations, then use
84/// [Gaussian Elimination](https://en.wikipedia.org/wiki/Gaussian_elimination) to reduce the
85/// dimensioanlity of the problem to only the free variables.
86fn configure_joltages((_, buttons, joltages): &Machine) -> i32 {
87    let width = buttons.len();
88    let height = joltages.len();
89    let mut equations = vec![vec![0; width + 1]; height];
90    let mut limit = vec![i32::MAX; width];
91    let mut previous: BTreeSet<_> = (0..width).collect();
92    let mut current: BTreeSet<_> = BTreeSet::new();
93
94    // If a button can increment a joltage counter then it get a coefficent of 1, otherwise zero.
95    // Using the first example machine and labelling the button a..f from left to right:
96    //
97    // * [.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
98    // * d + f = 3
99    // * b + f = 5
100    // * c + d + e = 4
101    // * a + b + d = 7
102    for (row, &joltage) in joltages.iter().enumerate() {
103        equations[row][width] = joltage;
104
105        for col in 0..width {
106            if buttons[col] & (1 << row) != 0 {
107                equations[row][col] = 1;
108                limit[col] = limit[col].min(joltage);
109            }
110        }
111    }
112
113    // Gaussian elimination to reduce the matrix to row echelon form. For example the previous
114    // equations form a matrix:
115    //
116    // [ 0  0  0  1  0  1 | 3 ]
117    // [ 0  1  0  0  0  1 | 4 ]
118    // [ 0  0  1  1  1  0 | 5 ]
119    // [ 1  0  0  1  0  0 | 7 ]
120    while previous != current {
121        previous = current;
122        current = (0..width).collect();
123
124        let mut pivot_row = 0;
125        let mut pivot_col = 0;
126
127        while pivot_row < height && pivot_col < width {
128            let Some(found) = (pivot_row..height).find(|&row| {
129                let coefficient = equations[row][pivot_col];
130                coefficient != 0 && equations[row].iter().all(|c| c % coefficient == 0)
131            }) else {
132                pivot_col += 1;
133                continue;
134            };
135
136            equations.swap(pivot_row, found);
137            let coefficient = equations[pivot_row][pivot_col];
138            equations[pivot_row].iter_mut().for_each(|c| *c /= coefficient);
139
140            for row in 0..height {
141                if row != pivot_row {
142                    let coefficient = equations[row][pivot_col];
143                    let [from, to] = equations.get_disjoint_mut([pivot_row, row]).unwrap();
144                    from.iter().zip(to).for_each(|(f, t)| *t -= coefficient * f);
145                }
146            }
147
148            current.remove(&pivot_col);
149            pivot_row += 1;
150            pivot_col += 1;
151        }
152    }
153
154    if current.is_empty() {
155        return (0..height).map(|row| equations[row][width]).sum();
156    }
157
158    // Brute force search over the free variables.
159    let free = current.len();
160    let fixed = width - current.len();
161    let presses = (0..fixed).map(|row| equations[row][width]).sum::<i32>();
162    let mut cost = vec![0; free];
163    let mut coefficients = vec![vec![0; height]; free];
164    let mut rhs = vec![vec![0; height]; free];
165    let mut ordered_limit: Vec<_> = vec![0; free];
166
167    for (to, &from) in current.iter().enumerate() {
168        cost[to] = 1 - (0..fixed).map(|row| equations[row][from]).sum::<i32>();
169        ordered_limit[to] = limit[from];
170
171        for row in 0..height {
172            coefficients[to][row] = equations[row][from];
173        }
174    }
175
176    for row in 0..height {
177        rhs[0][row] = equations[row][width];
178    }
179
180    recurse(&cost, &ordered_limit, &coefficients, &mut rhs, fixed, presses, 0).unwrap()
181}
182
183fn recurse(
184    cost: &[i32],
185    limit: &[i32],
186    coefficients: &[Vec<i32>],
187    rhs: &mut [Vec<i32>],
188    fixed: usize,
189    presses: i32,
190    depth: usize,
191) -> Option<i32> {
192    let height = rhs[depth].len();
193
194    if depth == coefficients.len() - 1 {
195        // For the last free variables, we can use the remaining inequalities (and possibily and
196        // equalities to find the answer immediately) without needed another search. This
197        // reduces the dimensions of the search space by one.
198        let mut lower = 0;
199        let mut upper = limit[depth];
200
201        // Check inequalites
202        for (&coef, &rhs) in coefficients[depth].iter().zip(&rhs[depth]) {
203            if rhs >= 0 {
204                if coef > 0 {
205                    upper = upper.min(rhs / coef);
206                }
207            } else if coef < 0 {
208                let floor = (rhs + coef + 1) / coef;
209                lower = lower.max(floor);
210            } else {
211                upper = -1;
212            }
213        }
214
215        // Check equalities (if they exist)
216        for row in fixed..height {
217            let c = coefficients[depth][row];
218            let r = rhs[depth][row];
219
220            if c != 0 {
221                if r % c == 0 {
222                    upper = upper.min(r / c);
223                    lower = lower.max(r / c);
224                } else {
225                    upper = -1;
226                }
227            }
228        }
229
230        let presses = presses + cost[depth] * if cost[depth] >= 0 { lower } else { upper };
231        (lower <= upper).then_some(presses)
232    } else {
233        (0..=limit[depth])
234            .filter_map(|x| {
235                let next_presses = presses + x * cost[depth];
236
237                for row in 0..height {
238                    rhs[depth + 1][row] = rhs[depth][row] - x * coefficients[depth][row];
239                }
240
241                recurse(cost, limit, coefficients, rhs, fixed, next_presses, depth + 1)
242            })
243            .min()
244    }
245}