Skip to content

Commit ee1d6d3

Browse files
committed
2025: day 10 optimized
1 parent 9a8c200 commit ee1d6d3

File tree

3 files changed

+190
-9
lines changed

3 files changed

+190
-9
lines changed

Cargo.lock

Lines changed: 56 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ geo-types = "*"
4545
bytecount = "*"
4646
divisors = "*"
4747
sha2 = "0.10.8"
48+
rayon = "*"
49+
good_lp = { version = "*", features = ["microlp"], default-features = false }
4850
# rusqlite = "0.37.0"
4951

5052
[lints.clippy]

src/year2025/day10/day10.rs

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,70 @@
11
//! [Day 10: Factory](https://adventofcode.com/2025/day/10)
22
3+
use good_lp::{Expression, Solution, SolverModel, default_solver, variable, variables};
4+
use rayon::prelude::*;
35
use z3::ast::Int;
46

7+
// this is only to make clippy happy... cf. https://stackoverflow.com/a/74629224
8+
const F64_BITS: u64 = 64;
9+
const F64_EXPONENT_BITS: u64 = 11;
10+
const F64_EXPONENT_MAX: u64 = (1 << F64_EXPONENT_BITS) - 1;
11+
const F64_EXPONENT_BIAS: u64 = 1023;
12+
const F64_FRACTION_BITS: u64 = 52;
13+
14+
pub fn f64_to_u64(f: f64) -> Option<u64> {
15+
let bits = f.to_bits();
16+
let sign = bits & (1 << (F64_EXPONENT_BITS + F64_FRACTION_BITS)) != 0;
17+
let exponent = (bits >> F64_FRACTION_BITS) & ((1 << F64_EXPONENT_BITS) - 1);
18+
let fraction = bits & ((1 << F64_FRACTION_BITS) - 1);
19+
20+
match (sign, exponent, fraction) {
21+
(_, 0, 0) => {
22+
debug_assert!(f == 0.0);
23+
Some(0)
24+
}
25+
(true, _, _) => {
26+
debug_assert!(f < 0.0);
27+
None
28+
}
29+
(_, F64_EXPONENT_MAX, 0) => {
30+
debug_assert!(f.is_infinite());
31+
None
32+
}
33+
(_, F64_EXPONENT_MAX, _) => {
34+
debug_assert!(f.is_nan());
35+
None
36+
}
37+
(_, 0, _) => {
38+
debug_assert!(f.is_subnormal());
39+
None
40+
}
41+
_ => {
42+
if exponent < F64_EXPONENT_BIAS {
43+
debug_assert!(f < 1.0);
44+
None
45+
} else {
46+
let mantissa = fraction | (1 << F64_FRACTION_BITS);
47+
let left_shift =
48+
exponent.cast_signed() - (F64_EXPONENT_BIAS + F64_FRACTION_BITS).cast_signed();
49+
if left_shift < 0 {
50+
let right_shift = (-left_shift).cast_unsigned();
51+
if mantissa & (1 << (right_shift - 1)) != 0 {
52+
debug_assert!(f.fract() != 0.0);
53+
None
54+
} else {
55+
Some(mantissa >> right_shift)
56+
}
57+
} else if left_shift > (F64_BITS - F64_FRACTION_BITS - 1).cast_signed() {
58+
debug_assert!(f > 2.0f64.powi(63));
59+
None
60+
} else {
61+
Some(mantissa << left_shift)
62+
}
63+
}
64+
}
65+
}
66+
}
67+
568
struct Machine {
669
lights: u32,
770
wirings: Vec<u32>,
@@ -33,8 +96,51 @@ impl Machine {
3396
.min()
3497
}
3598

36-
/// Return the fewest button presses required to correctly configure the joltage level counters .
37-
fn optimize(&self) -> u64 {
99+
/// Return the fewest button presses required to correctly configure the joltage level counters.
100+
/// Use Integer Linear Programming solver.
101+
fn optimize_lp(&self) -> u64 {
102+
let n = self.wirings.len();
103+
104+
// 1) Create the container for problem variables
105+
let mut vars = variables!();
106+
107+
// 2) Create `n` integer variables and collect them
108+
let presses: Vec<_> = (0..n)
109+
.map(|_| vars.add(variable().integer().min(0)))
110+
.collect();
111+
112+
// 3) Build the objective: minimize sum(w_i)
113+
let objective: Expression = presses.iter().sum();
114+
115+
// 4) Create the problem with that objective
116+
let mut problem = vars.minimise(objective).using(default_solver);
117+
118+
// 5) Add the equations.
119+
for (i, &target) in self.joltages.iter().enumerate() {
120+
let mut lhs = Expression::from(0);
121+
122+
for (j, w) in self.wirings.iter().enumerate() {
123+
if w & (1 << i) != 0 {
124+
lhs += presses[j];
125+
}
126+
}
127+
128+
problem = problem.with(lhs.eq(target));
129+
}
130+
131+
// 6) Solve with the default solver
132+
// 7) Extract integer values (solution.value returns f64)
133+
problem.solve().map_or(0, |solution| {
134+
presses
135+
.iter()
136+
.map(|&v| f64_to_u64(solution.value(v).round()).unwrap())
137+
.sum()
138+
})
139+
}
140+
141+
/// Return the fewest button presses required to correctly configure the joltage level counters.
142+
/// Use Z3 solver (complicates parallelism).
143+
fn optimize_z3(&self) -> u64 {
38144
let solver = z3::Optimize::new();
39145

40146
let vars: Vec<_> = (0..self.wirings.len())
@@ -63,10 +169,12 @@ impl Machine {
63169

64170
let sum_vars = z3::ast::Int::add(&vars.iter().collect::<Vec<_>>());
65171
solver.minimize(&sum_vars);
172+
solver.minimize(&sum_vars); // Sometimes, the solver fails to find the optimal solution on the first attempt.
66173

67174
match solver.check(&[]) {
68175
z3::SatResult::Sat => {
69176
let model = solver.get_model().unwrap();
177+
70178
vars.iter()
71179
.filter_map(|v| {
72180
let val = model.get_const_interp(v).unwrap();
@@ -142,25 +250,41 @@ impl Puzzle {
142250

143251
/// Solve part one.
144252
fn part1(&self) -> usize {
145-
self.machines.iter().filter_map(Machine::score).sum()
253+
self.machines.par_iter().filter_map(Machine::score).sum()
146254
}
147255

148256
/// Solve part two.
149-
fn part2(&self) -> u64 {
150-
self.machines.iter().map(Machine::optimize).sum()
257+
fn part2_lp(&self) -> u64 {
258+
self.machines.par_iter().map(Machine::optimize_lp).sum()
259+
}
260+
261+
/// Solve part two too.
262+
fn part2_z3(&self) -> u64 {
263+
self.machines.par_iter().map(Machine::optimize_z3).sum()
151264
}
152265
}
153266

154267
/// # Panics
155268
#[must_use]
156269
pub fn solve(data: &str) -> (usize, u64) {
157270
let puzzle = Puzzle::new(data);
158-
(puzzle.part1(), puzzle.part2())
271+
(puzzle.part1(), puzzle.part2_lp())
272+
}
273+
/// # Panics
274+
#[must_use]
275+
pub fn solve_z3(data: &str) -> (usize, u64) {
276+
let puzzle = Puzzle::new(data);
277+
(puzzle.part1(), puzzle.part2_z3())
159278
}
160279

161280
pub fn main() {
162281
let args = aoc::parse_args();
163-
args.run(solve);
282+
283+
if args.has_option("--z3") {
284+
args.run(solve_z3);
285+
} else {
286+
args.run(solve);
287+
}
164288
}
165289

166290
#[cfg(test)]
@@ -178,6 +302,6 @@ mod test {
178302
#[test]
179303
fn part2() {
180304
let puzzle = Puzzle::new(TEST_INPUT);
181-
assert_eq!(puzzle.part2(), 33);
305+
assert_eq!(puzzle.part2_lp(), 33);
182306
}
183307
}

0 commit comments

Comments
 (0)