Skip to content

Commit

Permalink
refactor: migrated astar to use a priorityqueue
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexAegis committed Dec 20, 2023
1 parent 4494e21 commit ca54ef6
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 105 deletions.
2 changes: 1 addition & 1 deletion solutions/typescript/2022/12/src/p1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion solutions/typescript/2022/12/src/p2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 9 additions & 9 deletions solutions/typescript/2022/16/src/p1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand All @@ -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,
Expand All @@ -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;
Expand Down
93 changes: 6 additions & 87 deletions solutions/typescript/libs/lib/src/model/graph/graph.class.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { aStar } from '../../pathfinding/astar.js';
import {
constructPath,
dijkstra,
type EdgeCollectionOptions,
type PathFindingResult,
Expand Down Expand Up @@ -292,91 +292,10 @@ export class Graph<
* @param h global heuristic function. Should return a monotone value for
* better nodes
*/
public aStar(options: GraphTraversalOptions<T, Dir, N>): PathFindingResult<N> {
if (!options.start || !options.end) {
return { path: [], distances: new Map() };
}

const openSet = new Set<N>([options.start]); // q?
const cameFrom = new Map<N, N>(); // prev!
const gScore = new Map<N, number>(); // 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<N, number>(); // 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<N>(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<N>(options.start, goal, cameFrom),
distances: gScore,
};
public aStar(
options: GraphTraversalOptions<T, Dir, N> &
Omit<EdgeCollectionOptions<T, Dir, N>, 'pathConstructor' | 'allNodes'>,
): PathFindingResult<N> {
return aStar({ ...options, allNodes: this.nodes });
}
}
49 changes: 49 additions & 0 deletions solutions/typescript/libs/lib/src/pathfinding/astar.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
106 changes: 103 additions & 3 deletions solutions/typescript/libs/lib/src/pathfinding/astar.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends ToString, Dir extends ToString, N extends BasicGraphNode<T, Dir>>(
options: GraphTraversalOptions<T, Dir, N> &
Omit<EdgeCollectionOptions<T, Dir, N>, 'pathConstructor'>,
): PathFindingResult<N> => {
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<N, N>(); // prev!
const gScore = new Map<N, number>(); // dist! Infinity
const fScore = new Map<N, number>(); // Infinity
const pathLengthMap = new Map<N, number>(); // 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<N>([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<N> = (to) => constructPath<N>(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<T, Dir, N>(current, {
pathConstructor,
allNodes: options.allNodes,
edgeFilter: options.edgeFilter,
edgeGenerator: options.edgeGenerator,
})) {
const weight = calculateCostOfTraversal<T, Dir, N>(
neighbour,
options.currentPathWeighter,
pathConstructor,
);
const tentativegScore = uDist + weight;
const currentScore = gScore.get(neighbour.to) ?? Number.POSITIVE_INFINITY;
if (tentativegScore < currentScore) {
const currentPath = constructPath<N>(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<N>(options.start, goal, prev),
distances: pathLengthMap,
};
};
8 changes: 4 additions & 4 deletions solutions/typescript/libs/lib/src/pathfinding/dijkstra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export const dijkstra = <
const pathLengthMap = new Map<N, number>(); // How many nodes there are to reach the end
const prev = new Map<N, N>();
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;
});

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit ca54ef6

Please sign in to comment.