From 881a530b8c4cff951bc8e609d9e84c4a2baa1dc0 Mon Sep 17 00:00:00 2001 From: monoid Date: Mon, 23 Dec 2024 04:15:04 +0900 Subject: [PATCH] Add solution day 21 --- day_21/input.txt | 5 + day_21/solve2.md | 78 +++++++ day_21/solve_1.test.ts | 270 ++++++++++++++++++++++++ day_21/solve_1.ts | 254 +++++++++++++++++++++++ day_21/solve_2.test.ts | 124 +++++++++++ day_21/solve_2.ts | 460 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1191 insertions(+) create mode 100644 day_21/input.txt create mode 100644 day_21/solve2.md create mode 100644 day_21/solve_1.test.ts create mode 100644 day_21/solve_1.ts create mode 100644 day_21/solve_2.test.ts create mode 100644 day_21/solve_2.ts diff --git a/day_21/input.txt b/day_21/input.txt new file mode 100644 index 0000000..0cad071 --- /dev/null +++ b/day_21/input.txt @@ -0,0 +1,5 @@ +540A +582A +169A +593A +579A \ No newline at end of file diff --git a/day_21/solve2.md b/day_21/solve2.md new file mode 100644 index 0000000..a0b8e84 --- /dev/null +++ b/day_21/solve2.md @@ -0,0 +1,78 @@ + +기본적인 정의를 합니다. + +```my-ml-like-sudo-language +// fg is shortest available pathes function that returns a path from pos to target. +// type cmd :: enum { +// up'^', down'v', right'<', left'>', accept'A' +// } +// type path_set = set of list of cmd; +``` + +이제 헬퍼 함수를 정이합니다. + +``` +// \product is a function that returns a list of all possible combinations of two lists. +// \product :: path_set -> path_set -> path_set +// \product [] ys = [] +// \product xs [] = [] +// \product (x:xs) y = (concat x y) ++ (\product xs y) +// infix 5 \product // associativity is not important. because it commutative. + +// ++ is union of two sets. +// concat :: cmd -> path_set -> path_set +// concat x [] = [x] +// concat x (y:ys) = (x:y) ++ (concat x ys) + +// product :: path_set -> path_set -> path_set +// product a b = a \product b + +``` + +```my-ml-like + +type start_pos = cmd +type target_pos = cmd +type next_pos = cmd +available_paths :: start_pos -> target_pos -> path_set +type Handler 'T :: start_pos -> ('T, next_pos) + +// define unit, mappend. these are monoid. + +// + +// unit :: Handler of path_set +// unit = s -> t -> (t, s) +// mappend :: Handler of path_set -> Handler of path_set -> Handler of path_set +// mappend f g = s -> +// let (t, s') = f s in +// let (a ,s'') = g s' +// in ( (product t a ) , s'') +// +// move :: Handler of (cmd -> set of list of cmd) + +// move s cmds x = available_paths x \product +// accept :: Handler of (list of cmd -> list of cmd) + +// f = fg 'A' +// f :: (list of cmd) -> set of (list of cmd) +// statement execute 'A' (f (x)) = x +// f x = move x; accept; +// f x:tails = (move x; accept; f tails) 'A' +// sl is get shortest path length +// sl :: list of cmd -> Int +// sl cmd = (f cmd) |> (foldr (x,y) -> min(x,y), INF) + +// operator \product is a function that returns a list of all possible combinations of two lists. +// \product :: list of cmd -> list of cmd -> list of cmd +// \product [] ys = ys +// \product xs [] = xs +// \product (x:xs) (y:ys) = x \product (y:ys) ++ xs \product (y:ys) +// infixr 5 \product + +// sl (a \product b) = sl a + sl b +// f /(.*)A/:rest = f '$1A' \product f rest +// sl(f /(.*)A/:rest)) = sl (f '$1A' \product f rest) +// = sl (f '$1A') + sl (f rest) +// +``` \ No newline at end of file diff --git a/day_21/solve_1.test.ts b/day_21/solve_1.test.ts new file mode 100644 index 0000000..06ae2df --- /dev/null +++ b/day_21/solve_1.test.ts @@ -0,0 +1,270 @@ +import { assertEquals, assertThrows } from "jsr:@std/assert"; +import { + Command, + DIRPAD_FIRST_POS, + executeCommandsOnDirpadRobot, + executeCommandsOnNumpadRobot, + getDirpadNumber, + getNextDirpadRobotState, + getNextNumpadRobotState, + getNumpadNumber, + getPosFromDirpadNumber, + getPosFromNumpadNumber, + NUMPAD_FIRST_POS, + Pos, + shortestPathesOnDirpadRobot, + shortestPathesOnNumpadRobot, + solve_1, +} from "./solve_1.ts"; + +Deno.test("getNumbericKeypadNumber", () => { + assertEquals(getNumpadNumber([0, 0]), "7"); + assertEquals(getNumpadNumber([1, 0]), "8"); + assertEquals(getNumpadNumber([2, 0]), "9"); + assertEquals(getNumpadNumber([0, 1]), "4"); + assertEquals(getNumpadNumber([1, 1]), "5"); + assertEquals(getNumpadNumber([2, 1]), "6"); + assertEquals(getNumpadNumber([0, 2]), "1"); + assertEquals(getNumpadNumber([1, 2]), "2"); + assertEquals(getNumpadNumber([2, 2]), "3"); + assertEquals(getNumpadNumber([0, 3]), " "); + assertEquals(getNumpadNumber([1, 3]), "0"); + assertEquals(getNumpadNumber([2, 3]), "A"); +}); + +Deno.test("getPosFromNumpadNumber", () => { + assertEquals(getPosFromNumpadNumber("7"), [0, 0]); + assertEquals(getPosFromNumpadNumber("8"), [1, 0]); + assertEquals(getPosFromNumpadNumber("9"), [2, 0]); + assertEquals(getPosFromNumpadNumber("4"), [0, 1]); + assertEquals(getPosFromNumpadNumber("5"), [1, 1]); + assertEquals(getPosFromNumpadNumber("6"), [2, 1]); + assertEquals(getPosFromNumpadNumber("1"), [0, 2]); + assertEquals(getPosFromNumpadNumber("2"), [1, 2]); + assertEquals(getPosFromNumpadNumber("3"), [2, 2]); + assertEquals(getPosFromNumpadNumber("0"), [1, 3]); + assertEquals(getPosFromNumpadNumber("A"), [2, 3]); +}); + +Deno.test("getNextKeypadRobotState", () => { + assertThrows(() => getNextNumpadRobotState({ pos: [0, 0] }, "^")); + assertEquals(getNextNumpadRobotState({ pos: [0, 0] }, "v"), { + pos: [0, 1], + }); + assertThrows(() => getNextNumpadRobotState({ pos: [0, 0] }, "<")); + assertEquals(getNextNumpadRobotState({ pos: [0, 0] }, ">"), { + pos: [1, 0], + }); + assertEquals(getNextNumpadRobotState({ pos: [0, 0] }, "A"), { + pos: [0, 0], + }); + + assertEquals(getNextNumpadRobotState({ pos: [1, 1] }, "^"), { + pos: [1, 0], + }); + assertEquals(getNextNumpadRobotState({ pos: [1, 1] }, "v"), { + pos: [1, 2], + }); + assertEquals(getNextNumpadRobotState({ pos: [1, 1] }, "<"), { + pos: [0, 1], + }); + assertEquals(getNextNumpadRobotState({ pos: [1, 1] }, ">"), { + pos: [2, 1], + }); + assertEquals(getNextNumpadRobotState({ pos: [1, 1] }, "A"), { + pos: [1, 1], + }); +}); + +Deno.test("getDirKeypadNumber", () => { + assertEquals(getDirpadNumber([0, 0]), " "); + assertEquals(getDirpadNumber([1, 0]), "^"); + assertEquals(getDirpadNumber([2, 0]), "A"); + assertEquals(getDirpadNumber([0, 1]), "<"); + assertEquals(getDirpadNumber([1, 1]), "v"); + assertEquals(getDirpadNumber([2, 1]), ">"); +}); + +Deno.test("getPosFromDirKeypadNumber", () => { + assertEquals(getPosFromDirpadNumber(" "), [0, 0]); + assertEquals(getPosFromDirpadNumber("^"), [1, 0]); + assertEquals(getPosFromDirpadNumber("A"), [2, 0]); + assertEquals(getPosFromDirpadNumber("<"), [0, 1]); + assertEquals(getPosFromDirpadNumber("v"), [1, 1]); + assertEquals(getPosFromDirpadNumber(">"), [2, 1]); +}); + +Deno.test("getNextDirKeypadRobotState", () => { + assertEquals(getNextDirpadRobotState({ pos: [1, 1] }, "^"), { + pos: [1, 0], + }); + assertThrows(() => getNextDirpadRobotState({ pos: [1, 1] }, "v")); + assertEquals(getNextDirpadRobotState({ pos: [1, 1] }, "<"), { + pos: [0, 1], + }); + assertEquals(getNextDirpadRobotState({ pos: [1, 1] }, ">"), { + pos: [2, 1], + }); + assertEquals(getNextDirpadRobotState({ pos: [1, 1] }, "A"), { + pos: [1, 1], + }); +}); + +Deno.test("executeCommandsOnNumpadRobot", () => { + const commands = [ + "^^AvvvA".split("") as Command[], + "^AvvvA".split("") as Command[], + "AvvvA".split("") as Command[], + ]; + for (const command of commands) { + const out: string[] = []; + executeCommandsOnNumpadRobot( + { + pos: NUMPAD_FIRST_POS, + }, + command, + (o) => { + out.push(o); + }, + ); + assertEquals(out, ["0", "2", "9", "A"]); + } +}); + +Deno.test("executeCommandsOnDirKeypadRobot", () => { + const input_result = [ + [ + ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A" + .split("") as Command[], + "v<>^AAvA<^AA>A^A", + ], + ["v<>^AAvA<^AA>A^A".split("") as Command[], "^^AvvvA"], + ] as const; + for (const [input, result] of input_result) { + const out: string[] = []; + executeCommandsOnDirpadRobot( + { + pos: DIRPAD_FIRST_POS, + }, + input, + (o) => { + out.push(o); + }, + ); + assertEquals(out.join(""), result); + } +}); + +Deno.test("shortestPathesOnNumpadRobot", () => { + const testCase: { + pos: Pos; + target: string; + expect: string[][]; + msg: string; + }[] = [ + { + pos: NUMPAD_FIRST_POS, + target: "A", + expect: [[]], + msg: "should return empty array when target is same as pos", + }, + { + pos: [0, 0], + target: "7", + expect: [[]], + msg: "should return empty array when target is same as pos", + }, + { + pos: NUMPAD_FIRST_POS, + target: "9", + expect: [["^", "^", "^"]], + msg: "should return shortest path to target", + }, + { + pos: [0, 0], + target: "5", + expect: [["v", ">"], [">", "v"]], + msg: "should return shortest path to target", + }, + { + pos: [1, 3], + target: "1", + expect: [["^", "<"]], + msg: "should return shortest path to target", + }, + ]; + for (const { pos, target, expect, msg } of testCase) { + assertEquals(shortestPathesOnNumpadRobot({ pos }, target), expect, msg); + } +}); + +Deno.test("shortestPathesOnDirpadRobot", () => { + const testCase: { + pos: Pos; + target: string; + expect: string[][]; + msg: string; + }[] = [ + { + pos: DIRPAD_FIRST_POS, + target: "A", + expect: [["A"]], + msg: "should return empty array when target is same as pos", + }, + { + pos: [1, 0], + target: "^", + expect: [["A"]], + msg: "should return empty array when target is same as pos", + }, + { + pos: DIRPAD_FIRST_POS, + target: ">", + expect: [["v", "A"]], + msg: "should return shortest path to target", + }, + { + pos: [1, 0], + target: ">", + expect: [["v", ">", "A"], [">", "v", "A"]], + msg: "should return shortest path to target", + }, + { + pos: [0, 1], + target: "^", + expect: [[">", "^", "A"]], + msg: "should return shortest path to target", + }, + ]; + for (const { pos, target, expect, msg } of testCase) { + assertEquals(shortestPathesOnDirpadRobot({ pos }, target), expect, msg); + } +}); + +Deno.test("example test", () => { + const testCase: [string, string][] = [ + [ + "029A", + ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A", + ], + [ + "980A", + ">^AAAvA^A>^AvAA<^A>AA>^AAAvA<^A>A^AA", + ], + [ + "179A", + ">^A>^AAvAA<^A>A>^AAvA^A^AAAA>^AAAvA<^A>A", + ], + [ + "456A", + ">^AA>^AAvAA<^A>A^AA^AAA>^AAvA<^A>A", + ], + [ + "379A", + ">^AvA^A>^AAvA<^A>AAvA^A^AAAA>^AAAvA<^A>A", + ], + ]; + for (const [input, expect] of testCase) { + assertEquals(solve_1(input), expect.length); + } +}); diff --git a/day_21/solve_1.ts b/day_21/solve_1.ts new file mode 100644 index 0000000..65dc51f --- /dev/null +++ b/day_21/solve_1.ts @@ -0,0 +1,254 @@ +export type Pos = [number, number]; +export type NumpadRobotState = { + pos: Pos; +}; +export type Command = "^" | "v" | "<" | ">" | "A"; + +export function getNextPos(pos: Pos, command: Command): Pos { + const [x, y] = pos; + switch (command) { + case "^": + return [x, y - 1]; + case "v": + return [x, y + 1]; + case "<": + return [x - 1, y]; + case ">": + return [x + 1, y]; + default: + return pos; + } +} + +const NumbericKeypad = "789\n456\n123\n 0A"; +export const NUMPAD_FIRST_POS: Pos = [2, 3]; +export function getNumpadNumber([x, y]: Pos): string { + return NumbericKeypad[y * 4 + x]; +} +export function getPosFromNumpadNumber(num: string): Pos { + const idx = NumbericKeypad.indexOf(num); + return [idx % 4, Math.floor(idx / 4)]; +} + +export function getNextNumpadRobotState( + state: NumpadRobotState, + command: Command, +): NumpadRobotState { + const nextPos = getNextPos(state.pos, command); + const [nx, ny] = nextPos; + if ( + nx < 0 || nx > 2 || ny < 0 || ny > 3 || + getNumpadNumber(nextPos) === " " + ) { + throw new Error( + `Invalid command: ${command} for pos: ${state.pos.join(",")} -> ${ + nextPos.join(",") + } is out of bound`, + ); + } + return { pos: nextPos }; +} + +const DirectionalKeypad = " ^A\n"; +export const DIRPAD_FIRST_POS: Pos = [2, 0]; +export type DirKeypadRobotState = { + pos: Pos; +}; +export function getDirpadNumber([x, y]: Pos): string { + return DirectionalKeypad[y * 4 + x]; +} +export function getPosFromDirpadNumber(num: string): Pos { + const idx = DirectionalKeypad.indexOf(num); + return [idx % 4, Math.floor(idx / 4)]; +} + +export function getNextDirpadRobotState( + state: DirKeypadRobotState, + command: Command, +): DirKeypadRobotState { + const nextPos = getNextPos(state.pos, command); + const [nx, ny] = nextPos; + if ( + nx < 0 || nx > 2 || ny < 0 || ny > 1 || + getDirpadNumber(nextPos) === " " + ) { + throw new Error( + `Invalid command: ${command} for pos: ${state.pos.join(",")} -> ${ + nextPos.join(",") + } is out of bound`, + ); + } + return { pos: nextPos }; +} + +export function executeCommandsOnNumpadRobot( + state: NumpadRobotState, + commands: Command[], + output: (out: string) => void, +): NumpadRobotState { + return commands.reduce((state, command) => { + if (command === "A") { + output(getNumpadNumber(state.pos)); + } + return getNextNumpadRobotState(state, command); + }, state); +} + +export function executeCommandsOnDirpadRobot( + state: DirKeypadRobotState, + commands: Command[], + output: (out: string) => void, +): DirKeypadRobotState { + return commands.reduce((state, command) => { + if (command === "A") { + output(getDirpadNumber(state.pos)); + } + return getNextDirpadRobotState(state, command); + }, state); +} + +export function shortestPathesOnNumpadRobot( + state: NumpadRobotState, + targetNum: string, +): Command[][] { + const targetPos = getPosFromNumpadNumber(targetNum); + const len = Math.abs(targetPos[0] - state.pos[0]) + + Math.abs(targetPos[1] - state.pos[1]); + const queue: [Command[], NumpadRobotState][] = [[[], state]]; + const paths: Command[][] = []; + while (queue.length > 0) { + const [path, state] = queue.shift()!; + if (path.length === len) { + if ( + state.pos[0] === targetPos[0] && state.pos[1] === targetPos[1] + ) { + paths.push(path); + } + continue; + } + for (const command of ["^", "v", "<", ">"] as Command[]) { + try { + const nextState = getNextNumpadRobotState(state, command); + queue.push([path.concat(command), nextState]); + } catch (e) { + // Ignore invalid command + } + } + } + return paths; +} + +export function shortestPathesOnDirpadRobot( + state: DirKeypadRobotState, + targetNum: string, +): Command[][] { + const targetPos = getPosFromDirpadNumber(targetNum); + const len = Math.abs(targetPos[0] - state.pos[0]) + + Math.abs(targetPos[1] - state.pos[1]); + const queue: [Command[], DirKeypadRobotState][] = [[[], state]]; + const paths: Command[][] = []; + while (queue.length > 0) { + const [path, state] = queue.shift()!; + if (path.length === len) { + if ( + state.pos[0] === targetPos[0] && state.pos[1] === targetPos[1] + ) { + paths.push(path); + } + continue; + } + for (const command of ["^", "v", "<", ">"] as Command[]) { + try { + const nextState = getNextDirpadRobotState(state, command); + queue.push([path.concat(command), nextState]); + } catch (e) { + // Ignore invalid command + } + } + } + return paths.map((p) => p.concat("A")); +} + +export function StepShortestPathesOnDirpadRobot( + state: DirKeypadRobotState, + targetNums: string[], +) { + return targetNums.reduce( (acc, targetNum) => { + const prevPos = acc.prevPos; + const paths = shortestPathesOnDirpadRobot({ pos: prevPos }, targetNum); + if (paths.length === 0) { + throw new Error(`No path found for ${targetNum} from ${prevPos}`); + } + return { + prevPos: getPosFromDirpadNumber(targetNum), + // product of all + paths: acc.paths.flatMap((p) => paths.map((np) => p.concat(np))), + } + }, { + prevPos: state.pos, + paths: [[]] as Command[][], + }).paths; +} + +function filterShortestPathes(paths: Command[][]) { + let len = Number.MAX_SAFE_INTEGER; + for (const path of paths) { + len = Math.min(len, path.length); + } + // len = Math.min(...paths.map((p) => p.length)); // this is call stack overflow... + // because the array is too big, the argument list is too long. + // so we need to calculate the min value by ourself. + return paths.filter((p) => p.length === len); +} + +export function solve_1(code: string) { + const numbers = code.split(""); + let start = NUMPAD_FIRST_POS; + + const finalOuts: string[] = []; + + for (const num of numbers) { + console.log("num", num); + const numpadPaths = shortestPathesOnNumpadRobot({ pos: start }, num).map((p) => p.concat("A")); + if (numpadPaths.length === 0) { + throw new Error(`No path found for ${num} from ${start}`); + } + console.log("avail path", numpadPaths.map((p) => p.join(""))); + const pathes1 = filterShortestPathes(numpadPaths.flatMap((numpadPath) => { + return StepShortestPathesOnDirpadRobot({ pos: DIRPAD_FIRST_POS }, numpadPath); + })); + + console.log("path1",pathes1.map((p) => p.join(""))); + const pathes2 = filterShortestPathes(pathes1.flatMap((path) => { + return StepShortestPathesOnDirpadRobot({ pos: DIRPAD_FIRST_POS }, path); + })); + console.log("path2",pathes2.map((p) => p.join(""))); + const pathes3 = filterShortestPathes(pathes2.flatMap((path) => { + return StepShortestPathesOnDirpadRobot({ pos: DIRPAD_FIRST_POS }, path); + })); + // console.log("path3",pathes3.map((p) => p.join(""))); + const executePath3 = (path: Command[]) => { + const outs: string[] = []; + executeCommandsOnDirpadRobot({ pos: DIRPAD_FIRST_POS }, path, (o) => { outs.push(o); }); + console.log("outs", outs.join("")); + return outs; + }; + finalOuts.push(...executePath3(pathes3[0])); + start = getPosFromNumpadNumber(num); + } + console.log("finalOuts", finalOuts.join(""), finalOuts.length); + return finalOuts.length; +} + + +if (import.meta.main) { + const codes = Deno.readTextFileSync("input.txt").replaceAll("\r","").split("\n"); + let total = 0; + for (const code of codes){ + const len = solve_1(code); + const n = parseInt(code.slice(0,3)); + console.log("len", len, n, len * n); + total += n * len; + } + console.log("total", total); +} \ No newline at end of file diff --git a/day_21/solve_2.test.ts b/day_21/solve_2.test.ts new file mode 100644 index 0000000..4831f0d --- /dev/null +++ b/day_21/solve_2.test.ts @@ -0,0 +1,124 @@ +import { assertEquals } from "jsr:@std/assert/equals"; +import { + availableShortestPaths, + Command, + inputCommand, + inputCommands, + Path, + PathSetHandler, + shortestPathLength, +} from "./solve_2.ts"; + +Deno.test("availableShortestPaths - blocked", () => { + const paths = availableShortestPaths("<", "^"); + assertEquals(paths.map((x) => x.toString()), [">^"]); +}); + +Deno.test("availableShortestPaths - common", () => { + const paths = availableShortestPaths(">", "^"); + assertEquals(paths.map((x) => x.toString()).sort(), ["<^", "^<"].sort()); +}); + +Deno.test("Path - length", () => { + const path = new Path(["^", "v", "<", ">", "A"]); + assertEquals(path.length, 5); +}); + +Deno.test("Path - execute", () => { + const path = new Path(["<", "v", "A", ">", "A"]); + const ret: Command[] = []; + path.execute("A", (c) => { + ret.push(c); + }); + assertEquals(ret.join(""), "v>"); +}); + +Deno.test("Path - concat", () => { + const path = new Path(["<", "v", "A", ">", "A"]); + assertEquals(path.concat("^").toString(), "A^"); +}); + +Deno.test("PathHandler - coproduct", () => { + const lhs = PathSetHandler.fromPathSet([Path.from(["A"])]); + const rhs = PathSetHandler.fromPathSet([Path.from(["v"])]); + const set = lhs.coproduct(rhs); + const [m, _] = set.call(">"); + assertEquals( + m.map((x) => x.toString()).sort(), + [ + "A", + "v", + ].sort(), + ); +}); +Deno.test("PathHandler - product", () => { + const lhs = PathSetHandler.fromPathSet([Path.from(["A"])]); + const rhs = PathSetHandler.fromPathSet([Path.from(["v"])]); + const set = lhs.product(rhs); + const [m, _] = set.call(">"); + assertEquals( + m.map((x) => x.toString()).sort(), + [ + "Av", + ].sort(), + ); +}); + +Deno.test("inputCommand", () => { + const handler = inputCommand("^"); + const [m, a] = handler.call(">"); + assertEquals( + m.map((x) => x.toString()).sort(), + [ + "<^A", + "^ { + const handler = inputCommands(Path.from([">", "A"])); + const [m, a] = handler.call(">"); + assertEquals( + m.map((x) => x.toString()).sort(), + [ + "A^A", + ].sort(), + ); +}); + +Deno.test("shortestPathLength", () => { + const inputcmdsShort = (start: Command, + cmds: Command[]) => shortestPathLength(inputCommands(Path.from(cmds)))(start)[0]; + + assertEquals(inputcmdsShort("A", (["A"])), 1); + assertEquals(inputcmdsShort("A", (["A", ">"])), 3); + assertEquals(inputcmdsShort("A", (["A", ">", "^"])), 6); + + assertEquals(inputcmdsShort("A", (["<"])), 4); + assertEquals(inputcmdsShort("A", (["<", "A"])), 8); +}); + +Deno.test("shortestPathLength - property", () => { + // random path list + const pathes = [ + Path.from(["^", "v", "<", ">", "A"]), + Path.from(["^", "v", "<", ">", "A", "v"]), + Path.from(["^", "v", "<", ">", "A", "v", "<"]), + Path.from(["^", "v", "<", ">", "A", "v", "<", ">"]), + Path.from(["A", "v", "v", ">", ">", "v", ">", "v"]), + ]; + for (const paths of pathes) { + for (const s of ["^", "v", "<", ">", "A"] as Command[]) { + const [x, ...xs] = paths; + const s1 = shortestPathLength(inputCommands(Path.from([x])))(s)[0]; + const s2 = shortestPathLength(inputCommands(Path.from(xs)))(x)[0]; + const s3 = shortestPathLength(inputCommands(paths))(s)[0]; + assertEquals( + s1 + s2, + s3, + `s: ${s}, x: ${x}, xs: ${xs}, s1: ${s1}, s2: ${s2}, s3: ${s3}`, + ); + } + } +}); diff --git a/day_21/solve_2.ts b/day_21/solve_2.ts new file mode 100644 index 0000000..3daac19 --- /dev/null +++ b/day_21/solve_2.ts @@ -0,0 +1,460 @@ +// fg is shortest available pathes function that returns a path from pos to target. +// type cmd :: enum { +// up'^', down'v', right'<', left'>', accept'A' + +import { getPosFromNumpadNumber, NUMPAD_FIRST_POS, shortestPathesOnNumpadRobot } from "./solve_1.ts"; + +// } +export type Command = "^" | "v" | "<" | ">" | "A"; +export type Pos = [number, number]; +// type path = list of cmd; +// type path_set = set of path; +export type PathSet = Path[]; + +// |--|--|--| +// | |^ | A| +// |--|--|--| +// |< |v | >| +// |--|--|--| +const lookupCommandPos: Record = { + "^": [1, 0], + "v": [1, 1], + "<": [0, 1], + ">": [2, 1], + "A": [2, 0], +}; +export function commandToPos(command: Command): Pos { + return lookupCommandPos[command]; +} +export function posToCommand(pos: Pos): Command { + for (const [key, value] of Object.entries(lookupCommandPos)) { + if (value[0] === pos[0] && value[1] === pos[1]) { + return key as Command; + } + } + throw new Error(`invalid pos: ${pos}`); +} + +export class Path { + value: string; + constructor(commands: Command[]) { + this.value = commands.join(""); + } + get length() { + return this.value.length; + } + [Symbol.iterator](): IterableIterator { + return this.value[Symbol.iterator]() as IterableIterator; + } + first() { + return this.value[0] as Command; + } + rest() { + const ret = new Path([]); + ret.value = this.value.slice(1); + return ret; + } + map(fn: (cmd: Command) => T) { + const result: T[] = []; + for (const cmd of this.value) { + result.push(fn(cmd as Command)); + } + return result; + } + flatMap(fn: (cmd: Command) => T[]) { + const result: T[] = []; + for (const cmd of this.value) { + result.push(...fn(cmd as Command)); + } + } + reduce(fn: (acc: T, cmd: Command) => T, init: T) { + let acc = init; + for (const cmd of this.value) { + acc = fn(acc, cmd as Command); + } + return acc; + } + get last() { + return this.value[this.value.length - 1]; + } + toString() { + return this.value; + } + toCommands() { + return this.value.split("") as Command[]; + } + // ++ is union of two sets. + // concat :: cmd -> path_set -> path_set + // concat x [] = [x] + // concat x (y:ys) = (y:x) ++ (concat x ys) + concat(command: Command) { + const ret = new Path([]); + ret.value = this.value + command; + return ret; + } + concatPath(path: Path) { + const ret = new Path([]); + ret.value = this.value + path.value; + return ret; + } + static from(commands: Command[]) { + return new Path(commands); + } + // \product is a function that returns a list of all possible combinations of two lists. + // \product :: path_set -> path_set -> path_set + // \product x [] = [] + // \product (x:xs) y = (concat x y) ++ (\product xs y) + // infix 5 \product // associativity is not important. because it commutative. + // product :: path_set -> path_set -> path_set + // product a b = a \product b + + static product(a: PathSet, b: PathSet): PathSet { + return a.flatMap((x) => b.map((y) => x.concatPath(y))); + } + + // it's not pure function. + // only use for test. + // execute :: (cmd, cmd -> never) -> Option of next_pos + execute(start: Command, onAccept: (accept: Command) => void): Pos | null { + let pos = commandToPos(start); + for (const command of this.toCommands()) { + if (command === "A") { + onAccept(posToCommand(pos)); + continue; + } + const newPos = moveSingle(pos, command); + if (newPos === null) { + return null; + } + pos = newPos; + } + return pos; + } + + equals(other: Path) { + return this.value === other.value; + } +} + +const lookupCommandDelta: Record = { + "^": [0, -1], + "v": [0, 1], + "<": [-1, 0], + ">": [1, 0], + "A": [0, 0], +}; +export function pathToDelta(command: Command): Pos { + return lookupCommandDelta[command]; +} +export function moveSingle(pos: Pos, command: Command): Pos | null { + const [x, y] = pos; + const [dx, dy] = pathToDelta(command); + const newPos: [number, number] = [x + dx, y + dy]; + // out of range + if (newPos[0] < 0 || newPos[0] > 2 || newPos[1] < 0 || newPos[1] > 1) { + return null; + } + // invalid position + if (newPos[0] === 0 && newPos[1] === 0) { + return null; + } + return newPos; +} + +// type Handler 'T :: start_pos -> ('T, next_pos) +export type Handler = (start: Command) => [T, Command]; +export class PathSetHandler { + private fn: Handler; + constructor(fn: Handler) { + this.fn = fn; + } + // call :: Handler of start_pos -> (path_set, next_pos) + // call f = f + call(command: Command) { + return this.fn(command); + } + + // mempty :: Handler of path_set + // mempty = \s -> ([], s) + static empty() { + return new PathSetHandler( + (s: Command) => [[], s], + ); + } + // munit :: Handler of path_set + // munit = \s -> ([[]], s) + static unit() { + return new PathSetHandler( + (s: Command) => [[Path.from([])], s], + ); + } + static fromPathSet(t: PathSet) { + return new PathSetHandler( + (s: Command) => [t, s], + ); + } + + coproduct(f: PathSetHandler) { + // union of two sets. + return new PathSetHandler( + (s: Command) => { + const [t, s1] = this.fn(s); + const [a, s2] = f.call(s1); + // merge two sets t and a. + const set = new Set([...t, ...a]); + const result = Array.from(set); + return [result, s2]; + }, + ); + } + // mproduct :: Handler of path_set -> Handler of path_set -> Handler of path_set + // mproduct f g = s -> + // let (t, s') = f s in + // let (a ,s'') = g s' + // in ( (product t a ) , s'') + // op ; = mproduct + // infixr 5 ; + product(f: PathSetHandler) { + return new PathSetHandler( + (s: Command) => { + const [t, s1] = this.fn(s); + const [a, s2] = f.fn(s1); + return [Path.product(t, a), s2]; + }, + ); + } + + // available_paths :: (start_pos, target_pos) -> path_set +} + +// type start_pos = cmd +// type target_pos = cmd +// type next_pos = cmd +// available_paths :: (start_pos, target_pos) -> path_set +export function availableShortestPaths( + start: Command, + target: Command, +): PathSet { + const startPos = commandToPos(start); + const targetPos = commandToPos(target); + const paths: Path[] = []; + const queue: [Path, Pos][] = [[new Path([]), startPos]]; + const deltaSum = Math.abs(targetPos[0] - startPos[0]) + + Math.abs(targetPos[1] - startPos[1]); + while (queue.length > 0) { + const [path, pos] = queue.shift()!; + if (pos[0] === targetPos[0] && pos[1] === targetPos[1]) { + paths.push(path); + continue; + } + if (path.length >= deltaSum) { + continue; + } + for (const command of ["^", "v", "<", ">"] as Command[]) { + const nextPos = moveSingle(pos, command); + if (nextPos) { + queue.push([path.concat(command), nextPos]); + } + } + } + if (paths.length === 0) { + throw new Error(`no path found from ${start} to ${target}`); + } + return paths; +} + +// move :: cmd -> Handler of path_set +// move cmd = \s -> (available_paths (s, cmd), cmd) +export const move: (cmd: Command) => PathSetHandler = (command: Command) => { + return new PathSetHandler((start: Command) => { + return [availableShortestPaths(start, command), command]; + }); +}; +export const accept: PathSetHandler = PathSetHandler.fromPathSet([ + Path.from(["A"]), +]); +// inputCommand :: cmd -> Handler of path_set +// inputCommand cmd = (move cmd); accept +export const inputCommand = (command: Command) => move(command).product(accept); +// inputCommands :: list of cmd -> Handler of path_set +// inputCommands xs = foldl (x -> y -> x; y) munit xs +// or +// inputCommands [] = munit +// inputCommands (x:xs) = (inputCommand x) ; (inputCommands xs) +export const inputCommands = (commands: Path) => { + return commands.reduce((acc, command) => { + return acc.product(inputCommand(command)); + }, PathSetHandler.unit()); +}; + +// shortest_path_length :: (handler of PathSet) -> handler of Int +// shortest_path_length h = \s -> (h s) |> foldl ((x,y) -> min(len x,len y)) INF +export function shortestPathLength(h: PathSetHandler): Handler { + return (s: Command) => { + const [paths, last] = h.call(s); + return [ + paths.map((x) => x.length).reduce( + (x, y) => Math.min(x, y), + Infinity, + ), + last, + ]; + }; +} + +// shortest_path_length has property of shortest path length. +// shortest_path_length (inputCommands x:xs) s = shortest_path_length (inputCommands [x]) s + shortest_path_length (inputCommands xs) x +/** + * the following assertion is always true. + * ```ts + * for (const s of ["^", "v", "<", ">", "A"] as Command[]) { + * // any path + * const paths = makeRandomSnapshotOfPath(); + * const [x, ...xs] = paths; + * const s1 = shortestPathLengthRaw(inputCommands(Path.from([x])))(s)[0]; + * const s2 = shortestPathLengthRaw(inputCommands(Path.from(xs)))(x)[0]; + * const s3 = shortestPathLengthRaw(inputCommands(paths))(s)[0]; + * assertEquals( + * s1 + s2, + * s3, + * `s: ${s}, x: ${x}, xs: ${xs}, s1: ${s1}, s2: ${s2}, s3: ${s3}`, + * ); + * } + */ + +function double(xs: Path) { + return (s1: Command) => { + return (s2: Command) => { + const [pathset] = inputCommands(xs).call(s1); + return pathset.flatMap((p) => { + const [pathset2] = inputCommands(p).call(s2); + return pathset2; + }); + }; + }; +} +// const data = double(Path.from("^^AvvvA".split("") as Command[]))("A")("A"); +// console.log(data.map((x) => x.toString())); +// double xs = \s1 -> \s2 -> flatMap (\cmd -> fst.inputCommands cmd s2) (fst.inputCommands xs s1) +// = \s1 -> \s2 -> flatMap ((\cmd -> fst.inputCommands cmd s2) (fst.inputCommands x:xs s1)) +// = \s1 -> \s2 -> flatMap (\cmd -> fst.inputCommands cmd s2) ((fst.inputCommand x; fst.inputCommands xs) s1) +// = \s1 -> \s2 -> flatMap (\cmd -> fst.inputCommands cmd s2) ((move x; accept; fst.inputCommands xs) s1) +// = \s1 -> \s2 -> flatMap (\cmd -> fst.inputCommands cmd s2) ( +// let (p,s3) = (move x; accept; s1) in +// p \product (inputCommands xs s3) +// ) +// = \s1 -> \s2 -> flatMap (\cmd -> fst.inputCommands cmd s2) ( +// let (p,s3) = (concat (move x s1) 'A') in +// p \product (inputCommands xs s3) +// ) +// = \s1 -> \s2 -> ( +// let (p,s3) = (concat (move x s1) 'A') in +// flatMap (\cmd -> fst.inputCommands cmd s2) p \product flatMap (\cmd -> fst.inputCommands cmd s2) (fst.inputCommands xs s3) +// ) +// + +// shortest_path_length \s' -> (double xs 'A' s') 'A' +// = shortest_path_length \s' -> ( +// let (p,s3) = (concat (move x 'A') 'A') in +// flatMap (\cmd -> fst.inputCommands cmd s') p \product flatMap (\cmd -> fst.inputCommands cmd 's) (fst.inputCommands xs s3) +// ) 'A' +// = ( +// let (p,s3) = (concat (move x 'A') 'A') in +// let paths = flatMap (\cmd -> fst.inputCommands cmd 'A') p \product flatMap (\cmd -> fst.inputCommands cmd 'A') (fst.inputCommands xs s3) +// in +// paths |> foldl ((x,y) -> min(len x,len y)) INF +// ) +// = ( +// let handler2 = \cmd -> fst.inputCommands cmd 'A' in +// let (p,s3) = (concat (move x 'A') 'A') in +// let paths = flatMap handler2 p \product flatMap handler2 (fst.inputCommands xs s3) in +// paths |> set.min (\x -> len x) +// ) +// = ( +// let handler2 = \cmd -> fst.inputCommands cmd 'A' in +// let (p,s3) = (concat (move x 'A') 'A') in +// set.min (\x -> len x) +// (flatMap handler2 p \product flatMap handler2 (fst.inputCommands xs s3)) +// = ( +// let handler2 = \cmd -> fst.inputCommands cmd 'A' in +// let (p,s3) = (concat (move x 'A') 'A') in +// (set.min (\x -> len x) +// (flatMap handler2 p)) + +// (set.min (\x -> len x) (flatMap handler2 (fst.inputCommands xs s3)) +// ) + +function shortL( + curPos: Command, + command: Command, +): number { + const [n] = shortestPathLength(inputCommands(Path.from([command])))(curPos); + return n; +} + +const memo = new Map(); +function myShortestPathLength( + depth: number, + cmds: Path, +): number { + const key = cmds.toString() + depth; + if (memo.has(key)) { + return memo.get(key)!; + } + if (depth === 1) { + const ret = shortestPathLength(inputCommands(cmds))("A")[0]; + memo.set(key, ret); + return ret; + } + const ret = cmds.reduce((acc, cmd) => { + const handler = inputCommand(cmd); + const [pathset, last] = handler.call(acc.last); + const n = pathset.map((x) => myShortestPathLength(depth - 1, x)).reduce( + (x, y) => Math.min(x, y), + Infinity, + ); + return { + last: last, + sum: acc.sum + n, + }; + }, { + last: "A" as Command, + sum: 0, + }).sum; + memo.set(key, ret); + return ret; +} + +if (import.meta.main) { + const example = Path.from("^^AvvvA".split("") as Command[]); + console.log(myShortestPathLength(1, example)); + + const codes = Deno.readTextFileSync("input.txt").replaceAll("\r", "").split( + "\n", + ); + + let total = 0; + for (const code of codes) { + let start = NUMPAD_FIRST_POS; + for (const num of code.split("")) { + console.log("num", num); + const numpadPaths = shortestPathesOnNumpadRobot({ pos: start }, num) + .map((p) => p.concat("A")); + if (numpadPaths.length === 0) { + throw new Error(`No path found for ${num} from ${start}`); + } + console.log("avail path", numpadPaths.map((p) => p.join(""))); + + let min = Infinity; + for (const path of numpadPaths) { + const len = myShortestPathLength(25, Path.from(path)); + min = Math.min(min, len); + } + const n = parseInt(code.slice(0, 3)); + console.log("len", min, "n", n); + total += n * min; + + start = getPosFromNumpadNumber(num); + } + } + console.log("total", total); +}