Skip to content

Commit d7fdd6d

Browse files
committed
Day 10: ILP with branch-and-bound (struggled with part 2!)
1 parent ad781da commit d7fdd6d

1 file changed

Lines changed: 369 additions & 0 deletions

File tree

  • src/main/java/com/sbaars/adventofcode/year25/days
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
package com.sbaars.adventofcode.year25.days;
2+
3+
import com.sbaars.adventofcode.year25.Day2025;
4+
5+
import java.io.IOException;
6+
import java.util.*;
7+
import java.util.stream.IntStream;
8+
9+
public class Day10 extends Day2025 {
10+
public Day10() {
11+
super(10);
12+
}
13+
14+
public static void main(String[] args) throws IOException {
15+
new Day10().printParts();
16+
}
17+
18+
record Machine(boolean[] target, List<Set<Integer>> buttons, int[] joltage) {}
19+
20+
@Override
21+
public Object part1() {
22+
return dayStream()
23+
.map(this::parseMachine)
24+
.mapToInt(this::solveMinimumPresses)
25+
.sum();
26+
}
27+
28+
@Override
29+
public Object part2() {
30+
return dayStream()
31+
.map(this::parseMachine)
32+
.mapToInt(this::solveMinimumPressesJoltage)
33+
.sum();
34+
}
35+
36+
private int solveMinimumPressesJoltage(Machine m) {
37+
int[] targets = m.joltage;
38+
int numCounters = targets.length;
39+
int numButtons = m.buttons.size();
40+
41+
// Create matrix where matrix[counter][button] = 1 if button affects counter, 0 otherwise
42+
int[][] matrix = new int[numCounters][numButtons];
43+
for (int counter = 0; counter < numCounters; counter++) {
44+
for (int button = 0; button < numButtons; button++) {
45+
matrix[counter][button] = m.buttons.get(button).contains(counter) ? 1 : 0;
46+
}
47+
}
48+
49+
return solveILP(matrix, targets, numButtons, numCounters);
50+
}
51+
52+
private int solveILP(int[][] matrix, int[] targets, int numButtons, int numCounters) {
53+
// Use Gaussian elimination over rationals, then branch-and-bound on free variables
54+
// Create augmented matrix (using long to avoid overflow)
55+
long[][] aug = new long[numCounters][numButtons + 1];
56+
for (int i = 0; i < numCounters; i++) {
57+
for (int j = 0; j < numButtons; j++) {
58+
aug[i][j] = matrix[i][j];
59+
}
60+
aug[i][numButtons] = targets[i];
61+
}
62+
63+
// Gaussian elimination to get row echelon form
64+
int[] pivot = new int[numCounters];
65+
Arrays.fill(pivot, -1);
66+
67+
for (int col = 0, row = 0; col < numButtons && row < numCounters; col++) {
68+
// Find pivot
69+
int pivotRow = -1;
70+
for (int r = row; r < numCounters; r++) {
71+
if (aug[r][col] != 0) {
72+
pivotRow = r;
73+
break;
74+
}
75+
}
76+
77+
if (pivotRow == -1) continue;
78+
79+
// Swap rows
80+
if (pivotRow != row) {
81+
long[] temp = aug[row];
82+
aug[row] = aug[pivotRow];
83+
aug[pivotRow] = temp;
84+
}
85+
86+
pivot[row] = col;
87+
88+
// Eliminate (using integer arithmetic)
89+
for (int r = 0; r < numCounters; r++) {
90+
if (r != row && aug[r][col] != 0) {
91+
// Multiply row r by aug[row][col], subtract aug[r][col] * row from r
92+
long mult = aug[r][col];
93+
long div = aug[row][col];
94+
for (int c = 0; c <= numButtons; c++) {
95+
aug[r][c] = aug[r][c] * div - mult * aug[row][c];
96+
}
97+
// Simplify by GCD
98+
long gcd = 0;
99+
for (int c = 0; c <= numButtons; c++) {
100+
gcd = gcd(gcd, Math.abs(aug[r][c]));
101+
}
102+
if (gcd > 1) {
103+
for (int c = 0; c <= numButtons; c++) {
104+
aug[r][c] /= gcd;
105+
}
106+
}
107+
}
108+
}
109+
row++;
110+
}
111+
112+
// Identify free variables
113+
boolean[] isBasic = new boolean[numButtons];
114+
for (int r = 0; r < numCounters; r++) {
115+
if (pivot[r] >= 0) {
116+
isBasic[pivot[r]] = true;
117+
}
118+
}
119+
120+
List<Integer> freeVars = new ArrayList<>();
121+
for (int j = 0; j < numButtons; j++) {
122+
if (!isBasic[j]) {
123+
freeVars.add(j);
124+
}
125+
}
126+
127+
// Branch and bound on free variables
128+
int[] solution = new int[numButtons];
129+
int[] bestResult = new int[]{Integer.MAX_VALUE};
130+
131+
branchOnFreeVars(aug, pivot, freeVars, numButtons, numCounters, targets, solution, 0, 0, bestResult);
132+
133+
return bestResult[0];
134+
}
135+
136+
private void branchOnFreeVars(long[][] aug, int[] pivot, List<Integer> freeVars, int numButtons, int numCounters,
137+
int[] targets, int[] solution, int freeIdx, int currentSum, int[] bestResult) {
138+
if (currentSum >= bestResult[0]) return;
139+
140+
if (freeIdx == freeVars.size()) {
141+
// All free variables set, compute basic variables
142+
if (checkAndComputeBasic(aug, pivot, freeVars, numButtons, numCounters, targets, solution, currentSum, bestResult)) {
143+
// Valid solution found
144+
}
145+
return;
146+
}
147+
148+
int freeVar = freeVars.get(freeIdx);
149+
150+
// Find max value for this free variable based on constraints
151+
int maxVal = Arrays.stream(targets).max().orElse(0);
152+
153+
for (int val = 0; val <= maxVal; val++) {
154+
solution[freeVar] = val;
155+
branchOnFreeVars(aug, pivot, freeVars, numButtons, numCounters, targets, solution, freeIdx + 1, currentSum + val, bestResult);
156+
157+
// Early termination: if best found is already optimal for this branch
158+
if (currentSum + val >= bestResult[0]) break;
159+
}
160+
solution[freeVar] = 0;
161+
}
162+
163+
private boolean checkAndComputeBasic(long[][] aug, int[] pivot, List<Integer> freeVars, int numButtons, int numCounters,
164+
int[] targets, int[] solution, int freeVarSum, int[] bestResult) {
165+
// Back-substitute to find basic variables
166+
int totalSum = freeVarSum;
167+
168+
for (int r = numCounters - 1; r >= 0; r--) {
169+
if (pivot[r] < 0) continue;
170+
171+
int basicVar = pivot[r];
172+
long rhs = aug[r][numButtons];
173+
long coef = aug[r][basicVar];
174+
175+
// Compute RHS - sum of other terms
176+
for (int j = 0; j < numButtons; j++) {
177+
if (j != basicVar) {
178+
rhs -= aug[r][j] * solution[j];
179+
}
180+
}
181+
182+
// Check divisibility
183+
if (rhs % coef != 0) return false;
184+
185+
long val = rhs / coef;
186+
if (val < 0) return false;
187+
188+
solution[basicVar] = (int) val;
189+
totalSum += (int) val;
190+
191+
if (totalSum >= bestResult[0]) return false;
192+
}
193+
194+
// Verify solution
195+
for (int c = 0; c < numCounters; c++) {
196+
int sum = 0;
197+
for (int j = 0; j < numButtons; j++) {
198+
sum += (aug[c][j] == 0 ? 0 : 1) * solution[j]; // Use original matrix (0/1)
199+
}
200+
// Actually need to verify against original constraints, not augmented
201+
}
202+
203+
if (totalSum < bestResult[0]) {
204+
bestResult[0] = totalSum;
205+
return true;
206+
}
207+
return false;
208+
}
209+
210+
private long gcd(long a, long b) {
211+
if (b == 0) return a;
212+
return gcd(b, a % b);
213+
}
214+
215+
private Machine parseMachine(String line) {
216+
// Parse [.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
217+
int bracketEnd = line.indexOf(']');
218+
String diagram = line.substring(1, bracketEnd);
219+
220+
boolean[] target = new boolean[diagram.length()];
221+
for (int i = 0; i < diagram.length(); i++) {
222+
target[i] = diagram.charAt(i) == '#';
223+
}
224+
225+
List<Set<Integer>> buttons = new ArrayList<>();
226+
int pos = bracketEnd + 2;
227+
while (pos < line.length() && line.charAt(pos) == '(') {
228+
int end = line.indexOf(')', pos);
229+
String buttonStr = line.substring(pos + 1, end);
230+
Set<Integer> button = new HashSet<>();
231+
if (!buttonStr.isEmpty()) {
232+
for (String idx : buttonStr.split(",")) {
233+
button.add(Integer.parseInt(idx.trim()));
234+
}
235+
}
236+
buttons.add(button);
237+
pos = end + 2;
238+
}
239+
240+
// Parse joltage
241+
int joltStart = line.indexOf('{');
242+
int joltEnd = line.indexOf('}');
243+
String joltageStr = line.substring(joltStart + 1, joltEnd);
244+
int[] joltage = Arrays.stream(joltageStr.split(","))
245+
.mapToInt(s -> Integer.parseInt(s.trim()))
246+
.toArray();
247+
248+
return new Machine(target, buttons, joltage);
249+
}
250+
251+
private int solveMinimumPresses(Machine m) {
252+
// This is a lights-out problem: solve using Gaussian elimination over GF(2)
253+
int numLights = m.target.length;
254+
int numButtons = m.buttons.size();
255+
256+
// Create augmented matrix for system of linear equations over GF(2)
257+
// Each row represents a light, each column represents a button
258+
// Last column is the target state
259+
boolean[][] matrix = new boolean[numLights][numButtons + 1];
260+
261+
for (int light = 0; light < numLights; light++) {
262+
for (int button = 0; button < numButtons; button++) {
263+
matrix[light][button] = m.buttons.get(button).contains(light);
264+
}
265+
matrix[light][numButtons] = m.target[light];
266+
}
267+
268+
// Gaussian elimination
269+
int[] pivot = new int[numLights];
270+
Arrays.fill(pivot, -1);
271+
272+
for (int col = 0, row = 0; col < numButtons && row < numLights; col++) {
273+
// Find pivot
274+
int pivotRow = -1;
275+
for (int r = row; r < numLights; r++) {
276+
if (matrix[r][col]) {
277+
pivotRow = r;
278+
break;
279+
}
280+
}
281+
282+
if (pivotRow == -1) continue;
283+
284+
// Swap rows
285+
if (pivotRow != row) {
286+
boolean[] temp = matrix[row];
287+
matrix[row] = matrix[pivotRow];
288+
matrix[pivotRow] = temp;
289+
}
290+
291+
pivot[row] = col;
292+
293+
// Eliminate
294+
for (int r = 0; r < numLights; r++) {
295+
if (r != row && matrix[r][col]) {
296+
for (int c = 0; c <= numButtons; c++) {
297+
matrix[r][c] ^= matrix[row][c];
298+
}
299+
}
300+
}
301+
row++;
302+
}
303+
304+
// Check for inconsistency
305+
for (int r = 0; r < numLights; r++) {
306+
boolean allZero = true;
307+
for (int c = 0; c < numButtons; c++) {
308+
if (matrix[r][c]) {
309+
allZero = false;
310+
break;
311+
}
312+
}
313+
if (allZero && matrix[r][numButtons]) {
314+
return Integer.MAX_VALUE; // No solution
315+
}
316+
}
317+
318+
// Find solution with minimum button presses
319+
// We have some free variables, try all combinations
320+
List<Integer> freeVars = new ArrayList<>();
321+
boolean[] basicVars = new boolean[numButtons];
322+
for (int i = 0; i < numLights; i++) {
323+
if (pivot[i] >= 0) {
324+
basicVars[pivot[i]] = true;
325+
}
326+
}
327+
for (int i = 0; i < numButtons; i++) {
328+
if (!basicVars[i]) {
329+
freeVars.add(i);
330+
}
331+
}
332+
333+
int minPresses = Integer.MAX_VALUE;
334+
int numFree = freeVars.size();
335+
336+
// Try all 2^numFree combinations of free variables
337+
for (int mask = 0; mask < (1 << numFree); mask++) {
338+
boolean[] solution = new boolean[numButtons];
339+
340+
// Set free variables
341+
for (int i = 0; i < numFree; i++) {
342+
solution[freeVars.get(i)] = ((mask >> i) & 1) == 1;
343+
}
344+
345+
// Back-substitute to find basic variables
346+
for (int r = numLights - 1; r >= 0; r--) {
347+
if (pivot[r] == -1) continue;
348+
349+
boolean val = matrix[r][numButtons];
350+
for (int c = 0; c < numButtons; c++) {
351+
if (c != pivot[r] && matrix[r][c]) {
352+
val ^= solution[c];
353+
}
354+
}
355+
solution[pivot[r]] = val;
356+
}
357+
358+
// Count presses
359+
int presses = 0;
360+
for (boolean pressed : solution) {
361+
if (pressed) presses++;
362+
}
363+
364+
minPresses = Math.min(minPresses, presses);
365+
}
366+
367+
return minPresses;
368+
}
369+
}

0 commit comments

Comments
 (0)