diff --git a/solutions/typescript/2022/12/src/p1.ts b/solutions/typescript/2022/12/src/p1.ts index 0e08f3d43..2e2275be2 100644 --- a/solutions/typescript/2022/12/src/p1.ts +++ b/solutions/typescript/2022/12/src/p1.ts @@ -22,4 +22,4 @@ export const p1 = (input: string): number => { return graph.aStar({ start, end }).path.length - 1; }; -await task(p1, packageJson.aoc); // 534 ~83.84ms +await task(p1, packageJson.aoc); // 534 ~56.09ms diff --git a/solutions/typescript/2022/12/src/p2.ts b/solutions/typescript/2022/12/src/p2.ts index 8f1f6a15b..1b5022f82 100644 --- a/solutions/typescript/2022/12/src/p2.ts +++ b/solutions/typescript/2022/12/src/p2.ts @@ -21,4 +21,4 @@ export const p2 = (input: string): number => { return graph.aStar({ start, end: (n) => n.value === 'a' }).path.length - 1; }; -await task(p2, packageJson.aoc); // 525 ~140.04ms +await task(p2, packageJson.aoc); // 525 ~154.81ms diff --git a/solutions/typescript/2022/16/src/p1.ts b/solutions/typescript/2022/16/src/p1.ts index 389f94e19..640e051ca 100644 --- a/solutions/typescript/2022/16/src/p1.ts +++ b/solutions/typescript/2022/16/src/p1.ts @@ -122,13 +122,13 @@ export const p1 = (input: string): number => { // calc pressure const pressureThisRound = openedValves.map((valve) => valve.flowRate).sum(); pressureReleasedSoFar += pressureThisRound; - console.log(`\n== Minute ${i + 1} ==`); + // console.log(`\n== Minute ${i + 1} ==`); if (openedValves.length > 0) { - console.log( - `Valves ${openedValves - .map((v) => v.name) - .join(', ')} are open, releasing ${pressureThisRound} pressure.`, - ); + // console.log( + // `Valves ${openedValves + // .map((v) => v.name) + // .join(', ')} are open, releasing ${pressureThisRound} pressure.`, + // ); } else { console.log('No valves are open.'); } @@ -138,7 +138,7 @@ export const p1 = (input: string): number => { ?.valve; if (targetValve) { - console.log('TARGET', targetValve.name); + // console.log('TARGET', targetValve.name); movingAlongPath = pathBetweenValves( currentlyAtValve.name, targetValve.name, @@ -151,11 +151,11 @@ export const p1 = (input: string): number => { } if (movingAlongPath?.length) { currentlyAtValve = movingAlongPath.shift()!; - console.log(`You move to valve ${currentlyAtValve.name}`); + // console.log(`You move to valve ${currentlyAtValve.name}`); } else if (currentlyAtValve === targetValve && !openedValves.includes(currentlyAtValve)) { openedValves.push(currentlyAtValve); targetValve = undefined; - console.log(`You open valve ${currentlyAtValve.name}`); + // console.log(`You open valve ${currentlyAtValve.name}`); } } return pressureReleasedSoFar; diff --git a/solutions/typescript/libs/lib/src/model/graph/graph.class.ts b/solutions/typescript/libs/lib/src/model/graph/graph.class.ts index 569c0f427..9dd7d7c1e 100644 --- a/solutions/typescript/libs/lib/src/model/graph/graph.class.ts +++ b/solutions/typescript/libs/lib/src/model/graph/graph.class.ts @@ -1,5 +1,5 @@ +import { aStar } from '../../pathfinding/astar.js'; import { - constructPath, dijkstra, type EdgeCollectionOptions, type PathFindingResult, @@ -292,91 +292,10 @@ export class Graph< * @param h global heuristic function. Should return a monotone value for * better nodes */ - public aStar(options: GraphTraversalOptions): PathFindingResult { - if (!options.start || !options.end) { - return { path: [], distances: new Map() }; - } - - const openSet = new Set([options.start]); // q? - const cameFrom = new Map(); // prev! - const gScore = new Map(); // dist! Infinity - - const h = options?.heuristic ?? (() => 1); - - const isFinished = - typeof options.end === 'function' - ? options.end - : (n: N, _path: N[]) => n === options.end; - // const generateNode = options?.generateNode ?? (() => undefined); - - gScore.set(options.start, 0); - - const fScore = new Map(); // Infinity - fScore.set(options.start, h(options.start, [])); - - let goal: N | undefined; - - while (openSet.size > 0) { - const umin = [...openSet.values()].reduce( - (acc, b) => { - const u = fScore.get(b) ?? Number.POSITIVE_INFINITY; - if (!acc.node || u < acc.dist) { - acc.node = b; - acc.dist = fScore.get(b) ?? Number.POSITIVE_INFINITY; - } - return acc; - }, - { node: undefined as N | undefined, dist: Number.POSITIVE_INFINITY }, - ); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const current = umin.node!; - - const currentPath = constructPath(options.start, current, cameFrom); - - if (isFinished(current, currentPath)) { - goal = current; - break; - } - openSet.delete(current); - - let edges = options?.edgeGenerator?.(this.nodes, current, currentPath) ?? [...current]; - const edgeFilter = options?.edgeFilter; - if (edgeFilter) { - edges = edges.filter((edge) => edgeFilter(edge, currentPath)); - } - - for (const neighbour of edges) { - const tentativegScore = - (gScore.get(current) ?? Number.POSITIVE_INFINITY) + - (options?.currentPathWeighter - ? options.currentPathWeighter( - current, - neighbour.to, - neighbour.direction, - currentPath, - ) - : /*neighbour.currentPathWeighter - ? neighbour.currentPathWeighter( - current, - neighbour.to, - neighbour.direction, - currentPath, - ) - :*/ neighbour.weight ?? 1); - if (tentativegScore < (gScore.get(neighbour.to) ?? Number.POSITIVE_INFINITY)) { - cameFrom.set(neighbour.to, current); - gScore.set(neighbour.to, tentativegScore); - fScore.set(neighbour.to, tentativegScore + h(neighbour.to, currentPath)); - if (!openSet.has(neighbour.to)) { - openSet.add(neighbour.to); - } - } - } - } - - return { - path: constructPath(options.start, goal, cameFrom), - distances: gScore, - }; + public aStar( + options: GraphTraversalOptions & + Omit, 'pathConstructor' | 'allNodes'>, + ): PathFindingResult { + return aStar({ ...options, allNodes: this.nodes }); } } diff --git a/solutions/typescript/libs/lib/src/pathfinding/astar.spec.ts b/solutions/typescript/libs/lib/src/pathfinding/astar.spec.ts new file mode 100644 index 000000000..29d9bb438 --- /dev/null +++ b/solutions/typescript/libs/lib/src/pathfinding/astar.spec.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { describe, expect, it } from 'vitest'; +import { Direction } from '../model/direction/direction.class.js'; +import { GridGraph } from '../model/graph/grid-graph.class.js'; +import { Vec2 } from '../model/vector/vec2.class.js'; +import { aStar } from './astar.js'; + +describe('A Star', () => { + const matrix = [ + ['#', '#', '#', '#', '#', '#', '#'], + ['#', '.', '.', '#', '.', '.', '#'], + ['#', '.', '#', '.', '.', '.', '#'], + ['#', '.', '.', '.', '#', '.', '#'], + ['#', '.', '.', '.', '#', '.', '#'], + ['#', '#', '#', '#', '#', '#', '#'], + ]; + const start = new Vec2(1, 1); + const finish = new Vec2(5, 4); + + it('should be able to generate a graph from a matrix', () => { + const g = GridGraph.fromMatrix(matrix); + expect([...g.nodes.values()].length).toEqual(matrix.length * (matrix[0]?.length ?? 0)); + }); + + it('should find the shortest path', () => { + const g = GridGraph.fromMatrix(matrix); + const goal = g.getNode(finish)!; + const { path } = g.aStar({ + start: g.getNode(start)!, + end: goal, + heuristic: (a) => a.coordinate.manhattan(goal.coordinate), + }); + expect(path.length).toEqual(10); + }); + + it('should find the shortest path with diagonal connections', () => { + const g = GridGraph.fromMatrix(matrix, { + connectionDirections: Direction.allDirections, + }); + const goal = g.getNode(finish)!; + const { path } = aStar({ + allNodes: g.nodes, + start: g.getNode(start)!, + end: goal, + heuristic: (a) => a.coordinate.manhattan(goal.coordinate), + }); + expect(path.length).toEqual(6); + }); +}); diff --git a/solutions/typescript/libs/lib/src/pathfinding/astar.ts b/solutions/typescript/libs/lib/src/pathfinding/astar.ts index a3f1d4a10..ba60580f8 100644 --- a/solutions/typescript/libs/lib/src/pathfinding/astar.ts +++ b/solutions/typescript/libs/lib/src/pathfinding/astar.ts @@ -1,12 +1,112 @@ +import { isNotNullish } from '@alexaegis/common'; +import { PriorityQueue } from 'js-sdsl'; import type { GraphTraversalOptions } from '../model/graph/graph.class.js'; import type { BasicGraphNode } from '../model/graph/node.class.js'; import type { ToString } from '../model/to-string.interface.js'; -import type { EdgeCollectionOptions, PathFindingResult } from './dijkstra.js'; +import { + calculateCostOfTraversal, + collectEdges, + constructPath, + type EdgeCollectionOptions, + type PathConstructor, + type PathFindingResult, +} from './dijkstra.js'; export const aStar = >( options: GraphTraversalOptions & Omit, 'pathConstructor'>, ): PathFindingResult => { - console.log(options); - return { distances: new Map(), path: [] }; + if (!options.start || !options.end) { + return { path: [], distances: new Map() }; + } + + const h = options?.heuristic ?? (() => 1); + + const prev = new Map(); // prev! + const gScore = new Map(); // dist! Infinity + const fScore = new Map(); // Infinity + const pathLengthMap = new Map(); // How many nodes there are to reach the end + + gScore.set(options.start, 0); + fScore.set(options.start, h(options.start, [])); + pathLengthMap.set(options.start, 0); + // This is used to support weights of 0 + const orderOfDiscovery = [options.start]; + + const pq = new PriorityQueue([options.start], (a, b) => { + const aScore = fScore.get(a) ?? Number.POSITIVE_INFINITY; + const bScore = fScore.get(b) ?? Number.POSITIVE_INFINITY; + + if (aScore === bScore) { + let aPathLength = orderOfDiscovery.indexOf(a); + let bPathLength = orderOfDiscovery.indexOf(b); + if (aPathLength < 0) { + aPathLength = Number.POSITIVE_INFINITY; + } + if (bPathLength < 0) { + bPathLength = Number.POSITIVE_INFINITY; + } + return aPathLength - bPathLength; + } else { + return aScore - bScore; + } + }); + + const pathConstructor: PathConstructor = (to) => constructPath(options.start, to, prev); + + const isFinished = isNotNullish(options.end) + ? (n: N) => + typeof options.end === 'function' + ? options.end(n, pathConstructor(n)) + : n === options.end + : (_n: N) => false; + + let goal: N | undefined; + + while (pq.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const current = pq.pop()!; // u, closest yet + + orderOfDiscovery.removeItem(current); + + if (isFinished(current)) { + goal = current; + break; + } + + const uDist = gScore.get(current) ?? Number.POSITIVE_INFINITY; + + for (const neighbour of collectEdges(current, { + pathConstructor, + allNodes: options.allNodes, + edgeFilter: options.edgeFilter, + edgeGenerator: options.edgeGenerator, + })) { + const weight = calculateCostOfTraversal( + neighbour, + options.currentPathWeighter, + pathConstructor, + ); + const tentativegScore = uDist + weight; + const currentScore = gScore.get(neighbour.to) ?? Number.POSITIVE_INFINITY; + if (tentativegScore < currentScore) { + const currentPath = constructPath(options.start, current, prev); + + prev.set(neighbour.to, current); + gScore.set(neighbour.to, tentativegScore); + fScore.set(neighbour.to, tentativegScore + h(neighbour.to, currentPath)); + pathLengthMap.set(neighbour.to, (pathLengthMap.get(neighbour.to) ?? 0) + 1); + + if (!pq.updateItem(neighbour.to)) { + pq.push(neighbour.to); + orderOfDiscovery.push(neighbour.to); + } + } + } + } + + return { + path: constructPath(options.start, goal, prev), + distances: pathLengthMap, + }; }; diff --git a/solutions/typescript/libs/lib/src/pathfinding/dijkstra.ts b/solutions/typescript/libs/lib/src/pathfinding/dijkstra.ts index 5b73c7469..f394f8119 100644 --- a/solutions/typescript/libs/lib/src/pathfinding/dijkstra.ts +++ b/solutions/typescript/libs/lib/src/pathfinding/dijkstra.ts @@ -95,8 +95,8 @@ export const dijkstra = < const pathLengthMap = new Map(); // How many nodes there are to reach the end const prev = new Map(); const pq = new PriorityQueue(options.allNodes, (a, b) => { - const bDist = dist.get(b) ?? Number.POSITIVE_INFINITY; const aDist = dist.get(a) ?? Number.POSITIVE_INFINITY; + const bDist = dist.get(b) ?? Number.POSITIVE_INFINITY; return aDist - bDist; }); @@ -135,10 +135,10 @@ export const dijkstra = < options.currentPathWeighter, pathConstructor, ); - const alt = uDist + weight; // alt + const tentativegScore = uDist + weight; const currentCost = dist.get(neighbour.to) ?? Number.POSITIVE_INFINITY; - if (alt < currentCost) { - dist.set(neighbour.to, alt); + if (tentativegScore < currentCost) { + dist.set(neighbour.to, tentativegScore); prev.set(neighbour.to, u); pathLengthMap.set(neighbour.to, (pathLengthMap.get(u) ?? 0) + 1); pq.updateItem(neighbour.to);