Skip to content

Commit bb4eb0b

Browse files
committed
Solve day 10 part two using Gaussian elimination
1 parent 2fc398c commit bb4eb0b

File tree

2 files changed

+226
-46
lines changed

2 files changed

+226
-46
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
8686
| 7 | [Laboratories](https://adventofcode.com/2025/day/7) | [Source](src/year2025/day07.rs) | 5 |
8787
| 8 | [Playground](https://adventofcode.com/2025/day/8) | [Source](src/year2025/day08.rs) | 527 |
8888
| 9 | [Movie Theater](https://adventofcode.com/2025/day/9) | [Source](src/year2025/day09.rs) | 668 |
89-
| 10 | [Factory](https://adventofcode.com/2025/day/10) | [Source](src/year2025/day10.rs) | 117* |
89+
| 10 | [Factory](https://adventofcode.com/2025/day/10) | [Source](src/year2025/day10.rs) | 296 |
9090
| 11 | [Reactor](https://adventofcode.com/2025/day/11) | [Source](src/year2025/day11.rs) | 75 |
9191
| 12 | [Christmas Tree Farm](https://adventofcode.com/2025/day/12) | [Source](src/year2025/day12.rs) | 25 |
9292

src/year2025/day10.rs

Lines changed: 225 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,245 @@
11
//! # Factory
22
use crate::util::bitset::*;
33
use crate::util::parse::*;
4+
use crate::util::thread::*;
5+
use std::collections::BTreeSet;
46

5-
type Machine = (u64, Vec<u64>, Vec<u64>);
6-
7-
pub fn parse(input: &str) -> Vec<Machine> {
8-
let mut tokens = Vec::new();
9-
10-
input
11-
.lines()
12-
.map(|line| {
13-
tokens.clear();
14-
tokens.extend(line.split_ascii_whitespace());
15-
16-
let last = tokens.len() - 1;
17-
let lights = tokens[0]
18-
.bytes()
19-
.skip(1)
20-
.enumerate()
21-
.fold(0, |acc, (i, b)| acc | (u64::from(b == b'#') << i));
22-
let buttons = tokens[1..last]
23-
.iter()
24-
.map(|token| token.iter_unsigned::<u64>().fold(0, |acc, i| acc | (1 << i)))
25-
.collect();
26-
let joltages = tokens[last].iter_unsigned::<u64>().collect();
27-
28-
(lights, buttons, joltages)
7+
type Machine = (usize, Vec<usize>, Vec<i32>);
8+
type Input = (Vec<i32>, Vec<i32>);
9+
10+
/// Each machine can be processed independently, parallelizing the work over multiple threads.
11+
pub 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))
2918
})
3019
.collect()
20+
});
21+
22+
result.into_iter().flatten().unzip()
23+
}
24+
25+
pub fn part1(input: &Input) -> i32 {
26+
let (presses, _) = input;
27+
presses.iter().sum()
28+
}
29+
30+
pub fn part2(input: &Input) -> i32 {
31+
let (_, presses) = input;
32+
presses.iter().sum()
3133
}
3234

33-
pub fn part1(input: &[Machine]) -> u32 {
34-
input
35+
/// Convert light patterns and buttons to bitmasks to speed up part one.
36+
fn 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]
3546
.iter()
36-
.map(|(lights, buttons, _)| {
37-
let limit = 1 << buttons.len();
38-
let mut set = 0;
39-
40-
loop {
41-
set += 1;
42-
let mut n = (1 << set) - 1;
43-
44-
while n < limit {
45-
if *lights == n.biterator().fold(0, |acc, i| acc ^ buttons[i]) {
46-
return set;
47-
}
48-
n = next_same_bits(n);
49-
}
50-
}
51-
})
52-
.sum()
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)
5352
}
5453

55-
pub fn part2(_input: &[Machine]) -> u32 {
56-
123456789
54+
/// Check all patterns with one set bit, then pattern with two sets bits and so on, until we find
55+
/// a match.
56+
fn 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+
}
5771
}
5872

73+
/// Find the next highest integer with the same number of one bits as the previous integer,
74+
/// for example 1011 => 1110.
5975
fn next_same_bits(n: i32) -> i32 {
6076
let smallest = n & -n;
6177
let ripple = n + smallest;
6278
let ones = n ^ ripple;
6379
let next = (ones >> 2) / smallest;
6480
ripple | next
6581
}
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.
86+
fn 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+
183+
fn 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+
}

0 commit comments

Comments
 (0)