Skip to content

Commit 2e77006

Browse files
committed
Add solution for puzzle 14 (both)
1 parent 59b21da commit 2e77006

File tree

2 files changed

+240
-0
lines changed

2 files changed

+240
-0
lines changed

src/core/math.mts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
export function modulo(a: number, b: number): number {
2+
return (a % b + b) % b;
3+
}
4+
5+
export function lcm(a: number, b: number): number {
6+
return (a / gcd(a, b)) * b;
7+
}
8+
9+
export function gcd(a: number, b: number): number {
10+
let x = a;
11+
let y = b;
12+
if (x < y) {
13+
[x, y] = [y, x];
14+
}
15+
while (y !== 0) {
16+
const reminder = x % y;
17+
x = y;
18+
y = reminder;
19+
}
20+
return x;
21+
}
22+
23+
export function extendedEuclidean(a: number, b: number): [s: number, t: number] {
24+
if (a <= 0 || b <= 0) {
25+
throw new Error('Arguments to extended Euclidean algorithm must be positive');
26+
}
27+
28+
let sa = 1;
29+
let sb = 0;
30+
let ta = 0;
31+
let tb = 1;
32+
33+
while (b > 0) {
34+
const q = Math.floor(a / b);
35+
36+
const r = a - b * q;
37+
const sr = sa - sb * q;
38+
const tr = ta - tb * q;
39+
40+
[a, b] = [b, r];
41+
[sa, sb] = [sb, sr];
42+
[ta, tb] = [tb, tr];
43+
}
44+
45+
return [sa, ta];
46+
}
47+
48+
export function chineseRemainderTheorem(
49+
input: ReadonlyArray<readonly [modulus: number, remainder: number]>
50+
): number {
51+
for (let i = 0; i < input.length; i++) {
52+
for (let j = i + 1; j < input.length; j++) {
53+
const [modulusA] = input[i];
54+
const [modulusB] = input[j];
55+
if (gcd(modulusA, modulusB) !== 1) {
56+
throw new Error(`Non-coprime mod for CTR: ${modulusA}, ${modulusB}`);
57+
}
58+
}
59+
}
60+
61+
let solution = 0n;
62+
63+
const grandMod = input.reduce((acc, [modulus]) => acc * BigInt(modulus), 1n);
64+
for (let i = 0; i < input.length; i++) {
65+
const [m, r] = input[i];
66+
if (!(m > 0 && r >= 0)) {
67+
throw new Error(`CTR modulus must be > 0 and remainder >= 0`);
68+
}
69+
const coMod = Number(grandMod / BigInt(m));
70+
let [inverse] = extendedEuclidean(coMod, m);
71+
if (inverse < 0) {
72+
inverse += m;
73+
}
74+
solution += BigInt(coMod) * BigInt(inverse) * BigInt(r);
75+
}
76+
77+
return Number(solution % grandMod);
78+
}
79+
80+
export function gaussArea(points: ReadonlyArray<readonly [number, number]>): number {
81+
let total = 0;
82+
for (let i = 0; i < points.length; i++) {
83+
const [x0, y0] = i === 0 ? points[points.length - 1] : points[i - 1];
84+
const [x1, y1] = points[i];
85+
total += x0 * y1 - x1 * y0;
86+
}
87+
return Math.abs(total) / 2;
88+
}

src/puzzle14.mts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { readFile, writeFile } from 'node:fs/promises';
2+
3+
import { NumericGrid, CharGrid } from './core/grid.mjs';
4+
import { modulo } from './core/math.mjs';
5+
import { Stopwatch } from './core/performance.mjs';
6+
import { getDataPath } from './core/project.mjs';
7+
8+
export async function solvePuzzleBasic() {
9+
const content = await readFile(getDataPath('input/puzzle14.txt'), {encoding: 'utf8'});
10+
const robots = parseRobots(content);
11+
12+
// const room = NumericGrid.empty(7, 11);
13+
const room = NumericGrid.empty(103, 101);
14+
15+
const roomSize: Vector = [room.columns, room.rows];
16+
for (const robot of robots) {
17+
const [x, y] = robotPositionAfterTime(robot, 100, roomSize);
18+
room.set(y, x, room.get(y, x) + 1);
19+
}
20+
21+
await writeFile(
22+
getDataPath('output/puzzle14_after100.txt'),
23+
Array.from(CharGrid.from(room, countToChar).lines()).join(''),
24+
{encoding: 'utf8'}
25+
);
26+
27+
const factor = computeSafetyFactor(room);
28+
console.log(`Puzzle 14 (basic): ${factor}`);
29+
}
30+
31+
export async function solvePuzzleAdvanced() {
32+
const content = await readFile(getDataPath('input/puzzle14.txt'), {encoding: 'utf8'});
33+
const robots = parseRobots(content);
34+
35+
const room = NumericGrid.empty(103, 101);
36+
37+
const roomSize: Vector = [room.columns, room.rows];
38+
const factors: number[] = [];
39+
let targetTime: number | undefined;
40+
for (let i = 0; i < 10_000; i++) {
41+
room.fill(0);
42+
for (const robot of robots) {
43+
const [x, y] = robotPositionAfterTime(robot, i, roomSize);
44+
room.set(y, x, room.get(y, x) + 1);
45+
}
46+
const factor = computeSafetyFactor(room);
47+
factors.push(factor);
48+
if (targetTime === undefined && factor < 50_000_000) {
49+
targetTime = i;
50+
}
51+
}
52+
53+
await writeFile(
54+
getDataPath('output/puzzle14_factors.txt'),
55+
factors.map(f => String(f)).join('\n'),
56+
{encoding: 'utf8'}
57+
);
58+
59+
if (targetTime !== undefined) {
60+
room.fill(0);
61+
for (const robot of robots) {
62+
const [x, y] = robotPositionAfterTime(robot, targetTime, roomSize);
63+
room.set(y, x, room.get(y, x) + 1);
64+
}
65+
await writeFile(
66+
getDataPath('output/puzzle14_easterEgg.txt'),
67+
Array.from(CharGrid.from(room, countToChar).lines()).join(''),
68+
{encoding: 'utf8'}
69+
);
70+
}
71+
72+
console.log(`Puzzle 14 (advanced): ${targetTime}`);
73+
}
74+
75+
type Vector = readonly [x: number, y: number];
76+
77+
interface Robot {
78+
readonly index: number;
79+
readonly position: Vector;
80+
readonly velocity: Vector;
81+
}
82+
83+
function parseRobots(content: string): Robot[] {
84+
const result: Robot[] = [];
85+
86+
for (const line of content.split(/\r?\n/)) {
87+
if (!line) {
88+
continue;
89+
}
90+
const match = /^p=(-?\d+),(-?\d+) v=(-?\d+),(-?\d+)$/.exec(line);
91+
if (!match) {
92+
throw new Error(`Invalid robot line: ${line}`);
93+
}
94+
const [, x, y, vx, vy] = Array.from(match, n => Number(n));
95+
result.push({
96+
index: result.length,
97+
position: [x, y],
98+
velocity: [vx, vy],
99+
});
100+
}
101+
102+
return result;
103+
}
104+
105+
function robotPositionAfterTime(robot: Robot, seconds: number, roomSize: Vector): Vector {
106+
let [x, y] = robot.position;
107+
const [vx, vy] = robot.velocity;
108+
const [width, height] = roomSize;
109+
const rx = modulo(x + vx * seconds, width);
110+
const ry = modulo(y + vy * seconds, height);
111+
return [rx, ry];
112+
}
113+
114+
function computeSafetyFactor(room: NumericGrid): number {
115+
const halfRow = Math.floor(room.rows / 2);
116+
const halfColumn = Math.floor(room.columns / 2);
117+
let totalTL = 0;
118+
let totalTR = 0;
119+
let totalBL = 0;
120+
let totalBR = 0;
121+
for (let i = 0; i < room.rows; i++) {
122+
for (let j = 0; j < room.columns; j++) {
123+
const count = room.get(i, j);
124+
if (i < halfRow) {
125+
if (j < halfColumn) {
126+
totalTL += count;
127+
} else if (j > halfColumn) {
128+
totalBL += count;
129+
}
130+
} else if (i > halfRow) {
131+
if (j < halfColumn) {
132+
totalTR += count;
133+
} else if (j > halfColumn) {
134+
totalBR += count;
135+
}
136+
}
137+
}
138+
}
139+
return totalTL * totalTR * totalBL * totalBR;
140+
}
141+
142+
const COUNT_TO_CHAR = '.123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
143+
144+
function countToChar(n: number): string {
145+
return COUNT_TO_CHAR[n] ?? '!';
146+
}
147+
148+
(async function main() {
149+
using _ = Stopwatch.start();
150+
await solvePuzzleBasic();
151+
await solvePuzzleAdvanced();
152+
})();

0 commit comments

Comments
 (0)