aoc-2024/day_13/solve_1.ts
2024-12-13 21:27:30 +09:00

212 lines
5.7 KiB
TypeScript

/**
* Calculate the greatest common divisor of two numbers using the Euclidean algorithm
* @param a
* @param b
* @returns gcd of a and b and the coefficients s and t such that gcd(a, b) = s*a + t*b
*/
export function extended_euclidean_algorithm(a: number, b: number) {
let [old_r, r] = [a, b];
let [old_s, s] = [1, 0];
let [old_t, t] = [0, 1];
while (r !== 0) {
const q = Math.floor(old_r / r);
[old_r, r] = [r, old_r - q * r];
[old_s, s] = [s, old_s - q * s];
[old_t, t] = [t, old_t - q * t];
}
return { gcd: old_r, s: old_s, t: old_t };
}
/**
* solve ax + by = c
* if solution exists, return {x, y, d} where d = gcd(a, b)
* if no solution exists, return null
* @param {number} a
* @param {number} b
* @param {number} c
*/
export function diopantos_solve(
a: number,
b: number,
c: number,
) {
const { gcd, s, t } = extended_euclidean_algorithm(a, b);
if (c % gcd !== 0) {
return null;
}
const x = s * (c / gcd);
const y = t * (c / gcd);
return { x, y, d: gcd };
}
export type Vector2 = {
x: number;
y: number;
};
export type ClawMachineData = {
buttonA: Vector2;
buttonB: Vector2;
prize: Vector2;
};
/**
* parse input data:
* Button A: X+94, Y+34
* Button B: X+22, Y+67
* Prize: X=8400, Y=5400
*/
export async function readData(path: string): Promise<ClawMachineData[]> {
const text = await Deno.readTextFile(path);
// TODO: support windows line endings
const objects = text.split("\n\n"); // split by empty line.
return objects.map((object) => {
const lines = object.split("\n");
const buttonA = lines[0].match(/X\+(\d+), Y\+(\d+)/);
const buttonB = lines[1].match(/X\+(\d+), Y\+(\d+)/);
const prize = lines[2].match(/X=(\d+), Y=(\d+)/);
if (!buttonA || !buttonB || !prize) {
throw new Error("Invalid input format");
}
return {
buttonA: { x: parseInt(buttonA[1]), y: parseInt(buttonA[2]) },
buttonB: { x: parseInt(buttonB[1]), y: parseInt(buttonB[2]) },
prize: { x: parseInt(prize[1]), y: parseInt(prize[2]) },
};
});
}
/**
* Solution space for the diophantine equation ax + by = c
*/
export type SolutionSpace = {
/**
* solution space origin
*/
origin: Vector2;
/**
* difference between two solutions
* you can get other solutions by adding this vector to the origin
*/
dir: Vector2;
};
export class NoSolutionError extends Error {
constructor() {
super("No solution found");
}
}
/**
* find the solution space for the diophantine equation ax + by = c
* @param a
* @param b
* @param c
* @returns solution space
*/
export function find_solution_space(
a: number,
b: number,
c: number,
): SolutionSpace {
const result = diopantos_solve(a, b, c);
if (!result) {
throw new NoSolutionError();
}
const { x, y, d } = result;
const k = Math.ceil(-x / (b / d));
return {
origin: { x: x + k * (b / d), y: y - k * (a / d) },
dir: { x: b / d, y: -a / d },
};
}
export function make_other_solution(
space: SolutionSpace,
k: number,
): { x: number; y: number } {
return {
x: space.origin.x + k * space.dir.x,
y: space.origin.y + k * space.dir.y,
}
}
export function make_all_positive_solution(
space: SolutionSpace,
): { x: number; y: number }[] {
const solutions = [];
const k_begin = Math.floor(space.origin.x / space.dir.x);
const k_end = Math.floor(-space.origin.y / space.dir.y);
for (let k = k_begin; k <= k_end; k++) {
const solution = make_other_solution(space, k);
if (solution.x >= 0 && solution.y >= 0) {
solutions.push(solution);
}
}
return solutions;
}
export function check_solution(data: ClawMachineData, x: number, y: number) {
return data.buttonA.x * x + data.buttonB.x * y === data.prize.x &&
data.buttonA.y * x + data.buttonB.y * y === data.prize.y;
}
if (import.meta.main) {
const costA = 3;
const costB = 1;
const data = await readData("input.txt");
let sum = 0;
data.forEach((clawMachineData) => {
// solution space is the intersection of the two solutions
// find the minimum cost to reach the prize
// cost = costA * x + costB * y
try {
const space = find_solution_space(
clawMachineData.buttonA.x,
clawMachineData.buttonB.x,
clawMachineData.prize.x,
);
const space2 = find_solution_space(
clawMachineData.buttonA.y,
clawMachineData.buttonB.y,
clawMachineData.prize.y,
);
const solutions = make_all_positive_solution(space);
const solutions2 = make_all_positive_solution(space2);
const intersection = solutions.filter((solution) => {
return solutions2.some((solution2) => {
return solution.x === solution2.x &&
solution.y === solution2.y;
});
});
console.log(intersection);
if (intersection.length === 0) {
throw new NoSolutionError();
}
const minCost = Math.min(...intersection.map((solution) => {
return costA * solution.x + costB * solution.y;
}));
console.log(minCost);
sum += minCost;
} catch (error) {
if (error instanceof NoSolutionError) {
console.log("No solution found");
} else {
throw error;
}
}
});
console.log(sum);
}