Skip to content

Commit ebadbd3

Browse files
committed
AoC2025: day 10 part 2
1 parent fafca3a commit ebadbd3

2 files changed

Lines changed: 267 additions & 8 deletions

File tree

2024-2025/src/2025/day10/day10.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('2025 Day 10', () => {
77
});
88

99
test('Part 2', async () => {
10-
// expect(await part2('testInput1')).toEqual(31);
11-
// expect(await part2('input')).toEqual(29379307);
10+
expect(await part2('testInput1')).toEqual(33);
11+
expect(await part2('input')).toEqual(16613);
1212
});
1313
});

2024-2025/src/2025/day10/day10.ts

Lines changed: 265 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,30 @@ import path from "node:path";
22
import {readInputLineByLine} from "@utils/io";
33

44
export async function part1(inputFile: string) {
5-
return await day10(inputFile, calcButtonPresses);
5+
return await day10(inputFile, parseMachines, calcButtonPresses);
66
}
77

88
export async function part2(inputFile: string) {
9-
return await day10(inputFile);
9+
return await day10(inputFile, parseJoltageMachines, calcJoltagePresses);
1010
}
1111

12-
async function day10(inputFile: string, calcFn?: (machines: Machine[]) => number) {
12+
async function day10<T>(inputFile: string, parseFn: (lines: string[]) => T, calcFn: (parsed: T) => number) {
1313
const inputPath = path.join(__dirname, inputFile);
1414
const lines = await readInputLineByLine(inputPath);
15-
const machines = lines.map(parseMachine);
16-
return calcFn?.(machines);
15+
const parsed = parseFn(lines);
16+
return calcFn(parsed);
1717
}
1818

1919
type Machine = { target: bigint; buttons: bigint[]; };
2020

21-
function parseMachine(line: string): Machine {
21+
function parseMachines(lines: string[]): Machine[] {
22+
return lines
23+
.map(line => line.trim())
24+
.filter(line => line.length > 0)
25+
.map(parseMachineLine);
26+
}
27+
28+
function parseMachineLine(line: string): Machine {
2229
const diagramMatch = line.match(/\[([.#]+)\]/);
2330
const diagram = diagramMatch![1];
2431
let target = 0n;
@@ -53,6 +60,39 @@ function calcButtonPresses(machines: Machine[]): number {
5360
return total;
5461
}
5562

63+
type JoltageMachine = { targets: number[]; buttons: number[][]; };
64+
65+
function parseJoltageMachines(lines: string[]): JoltageMachine[] {
66+
return lines
67+
.map(line => line.trim())
68+
.filter(line => line.length > 0)
69+
.map(parseJoltageLine);
70+
}
71+
72+
function parseJoltageLine(line: string): JoltageMachine {
73+
const targetMatch = line.match(/\{([^}]*)\}/);
74+
if (!targetMatch) {
75+
throw new Error(`Invalid line (missing joltage): ${line}`);
76+
}
77+
const targets = targetMatch[1]
78+
.split(',')
79+
.map(item => Number.parseInt(item.trim(), 10));
80+
81+
const buttons: number[][] = [];
82+
const buttonMatches = line.matchAll(/\(([^)]*)\)/g);
83+
for (const match of buttonMatches) {
84+
const content = match[1].trim();
85+
if (content.length === 0) continue;
86+
const indices = content
87+
.split(',')
88+
.map(item => Number.parseInt(item.trim(), 10))
89+
.filter(Number.isFinite);
90+
buttons.push(indices);
91+
}
92+
93+
return {targets, buttons};
94+
}
95+
5696
function minButtonPresses(target: bigint, buttons: bigint[]): number {
5797
if (target === 0n) return 0;
5898
if (buttons.length === 0) return -1;
@@ -101,3 +141,222 @@ function buildSubsetMinWeights(buttons: bigint[]): Map<bigint, number> {
101141
visit(0, 0n, 0);
102142
return map;
103143
}
144+
145+
function calcJoltagePresses(machines: JoltageMachine[]): number {
146+
let total = 0;
147+
for (const machine of machines) {
148+
total += minJoltagePresses(machine.targets, machine.buttons);
149+
}
150+
return total;
151+
}
152+
153+
function minJoltagePresses(targets: number[], buttons: number[][]): number {
154+
if (targets.every(value => value === 0)) return 0;
155+
156+
const upperBounds = buttons.map((indices) => {
157+
if (indices.length === 0) return 0;
158+
let bound = Number.POSITIVE_INFINITY;
159+
for (const idx of indices) {
160+
bound = Math.min(bound, targets[idx]);
161+
}
162+
return Number.isFinite(bound) ? bound : 0;
163+
});
164+
165+
const keep = upperBounds
166+
.map((bound, idx) => ({bound, idx}))
167+
.filter(item => item.bound > 0);
168+
169+
if (keep.length === 0) return -1;
170+
171+
const keptButtons = keep.map(item => buttons[item.idx]);
172+
const keptBounds = keep.map(item => item.bound);
173+
174+
const rowCount = targets.length;
175+
const colCount = keptButtons.length;
176+
177+
const matrix: Fraction[][] = Array.from({length: rowCount}, () =>
178+
Array.from({length: colCount}, () => fracFromInt(0))
179+
);
180+
181+
keptButtons.forEach((indices, col) => {
182+
for (const row of indices) {
183+
matrix[row][col] = fracFromInt(1);
184+
}
185+
});
186+
187+
const rhs = targets.map(fracFromInt);
188+
const {pivotCols, rrefMatrix, rrefRhs, inconsistent} = rrefSystem(matrix, rhs);
189+
if (inconsistent) return -1;
190+
191+
const freeCols: number[] = [];
192+
for (let col = 0; col < colCount; col++) {
193+
if (!pivotCols.includes(col)) freeCols.push(col);
194+
}
195+
196+
const freeOrder = freeCols
197+
.map(col => ({col, bound: keptBounds[col]}))
198+
.sort((a, b) => a.bound - b.bound);
199+
200+
const freeColsOrdered = freeOrder.map(item => item.col);
201+
const freeBounds = freeOrder.map(item => item.bound);
202+
203+
const pivotRows = new Map<number, number>();
204+
pivotCols.forEach((col, idx) => {
205+
pivotRows.set(col, idx);
206+
});
207+
208+
const pivotInfos = pivotCols.map((col) => {
209+
const row = pivotRows.get(col)!;
210+
const coeffs = freeColsOrdered.map(freeCol => rrefMatrix[row][freeCol]);
211+
return {
212+
col,
213+
rhs: rrefRhs[row],
214+
coeffs
215+
};
216+
});
217+
218+
let best = Number.POSITIVE_INFINITY;
219+
const freeValues = new Array<number>(freeColsOrdered.length).fill(0);
220+
221+
const evaluate = () => {
222+
let sum = 0;
223+
for (const value of freeValues) sum += value;
224+
if (sum >= best) return;
225+
226+
for (const pivot of pivotInfos) {
227+
let value = pivot.rhs;
228+
for (let i = 0; i < freeValues.length; i++) {
229+
const coeff = pivot.coeffs[i];
230+
if (coeff.num === 0n || freeValues[i] === 0) continue;
231+
value = fracSub(value, fracMulInt(coeff, freeValues[i]));
232+
}
233+
if (value.num % value.den !== 0n) return;
234+
const pivotValue = Number(value.num / value.den);
235+
if (pivotValue < 0) return;
236+
if (pivotValue > keptBounds[pivot.col]) return;
237+
sum += pivotValue;
238+
if (sum >= best) return;
239+
}
240+
best = Math.min(best, sum);
241+
};
242+
243+
const dfs = (index: number, sum: number) => {
244+
if (sum >= best) return;
245+
if (index === freeValues.length) {
246+
evaluate();
247+
return;
248+
}
249+
const bound = freeBounds[index];
250+
for (let value = 0; value <= bound; value++) {
251+
freeValues[index] = value;
252+
dfs(index + 1, sum + value);
253+
}
254+
};
255+
256+
dfs(0, 0);
257+
return Number.isFinite(best) ? best : -1;
258+
}
259+
260+
type Fraction = { num: bigint; den: bigint };
261+
262+
function fracFromInt(value: number): Fraction {
263+
return {num: BigInt(value), den: 1n};
264+
}
265+
266+
function fracNormalize(num: bigint, den: bigint): Fraction {
267+
if (num === 0n) return {num: 0n, den: 1n};
268+
if (den < 0n) {
269+
num = -num;
270+
den = -den;
271+
}
272+
const g = gcdBigInt(num < 0n ? -num : num, den);
273+
return {num: num / g, den: den / g};
274+
}
275+
276+
function fracAdd(a: Fraction, b: Fraction): Fraction {
277+
return fracNormalize(a.num * b.den + b.num * a.den, a.den * b.den);
278+
}
279+
280+
function fracSub(a: Fraction, b: Fraction): Fraction {
281+
return fracNormalize(a.num * b.den - b.num * a.den, a.den * b.den);
282+
}
283+
284+
function fracMul(a: Fraction, b: Fraction): Fraction {
285+
return fracNormalize(a.num * b.num, a.den * b.den);
286+
}
287+
288+
function fracMulInt(a: Fraction, value: number): Fraction {
289+
return fracNormalize(a.num * BigInt(value), a.den);
290+
}
291+
292+
function fracDiv(a: Fraction, b: Fraction): Fraction {
293+
if (b.num === 0n) throw new Error("Division by zero fraction");
294+
return fracNormalize(a.num * b.den, a.den * b.num);
295+
}
296+
297+
function gcdBigInt(a: bigint, b: bigint): bigint {
298+
let x = a;
299+
let y = b;
300+
while (y !== 0n) {
301+
const t = x % y;
302+
x = y;
303+
y = t;
304+
}
305+
return x;
306+
}
307+
308+
function rrefSystem(matrix: Fraction[][], rhs: Fraction[]) {
309+
const rowCount = matrix.length;
310+
const colCount = matrix[0]?.length ?? 0;
311+
let row = 0;
312+
const pivotCols: number[] = [];
313+
314+
for (let col = 0; col < colCount && row < rowCount; col++) {
315+
let pivot = row;
316+
while (pivot < rowCount && matrix[pivot][col].num === 0n) {
317+
pivot++;
318+
}
319+
if (pivot === rowCount) continue;
320+
321+
if (pivot !== row) {
322+
[matrix[pivot], matrix[row]] = [matrix[row], matrix[pivot]];
323+
[rhs[pivot], rhs[row]] = [rhs[row], rhs[pivot]];
324+
}
325+
326+
const pivotVal = matrix[row][col];
327+
for (let j = col; j < colCount; j++) {
328+
matrix[row][j] = fracDiv(matrix[row][j], pivotVal);
329+
}
330+
rhs[row] = fracDiv(rhs[row], pivotVal);
331+
332+
for (let i = 0; i < rowCount; i++) {
333+
if (i === row) continue;
334+
const factor = matrix[i][col];
335+
if (factor.num === 0n) continue;
336+
for (let j = col; j < colCount; j++) {
337+
matrix[i][j] = fracSub(matrix[i][j], fracMul(factor, matrix[row][j]));
338+
}
339+
rhs[i] = fracSub(rhs[i], fracMul(factor, rhs[row]));
340+
}
341+
342+
pivotCols.push(col);
343+
row++;
344+
}
345+
346+
let inconsistent = false;
347+
for (let i = 0; i < rowCount; i++) {
348+
let allZero = true;
349+
for (let j = 0; j < colCount; j++) {
350+
if (matrix[i][j].num !== 0n) {
351+
allZero = false;
352+
break;
353+
}
354+
}
355+
if (allZero && rhs[i].num !== 0n) {
356+
inconsistent = true;
357+
break;
358+
}
359+
}
360+
361+
return {pivotCols, rrefMatrix: matrix, rrefRhs: rhs, inconsistent};
362+
}

0 commit comments

Comments
 (0)