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);
+}