Skip to content

Commit f370635

Browse files
authored
Merge pull request #62 from monkey0722/feature/20250308
feat: Dijkstra's Algorithm and Topological Sort
2 parents d110b06 + 1696707 commit f370635

File tree

5 files changed

+327
-0
lines changed

5 files changed

+327
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Computer Science in TypeScript
2+
3+
The friendly graveyard where optimistic developers implement algorithms they secretly admire but rarely use!
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {dijkstra, reconstructPath} from './dijkstra';
2+
3+
describe('Dijkstra Algorithm', () => {
4+
test('should find the shortest paths in a simple graph', () => {
5+
const graph = [
6+
[
7+
{to: 1, weight: 1},
8+
{to: 2, weight: 4},
9+
],
10+
[
11+
{to: 2, weight: 2},
12+
{to: 3, weight: 5},
13+
],
14+
[{to: 3, weight: 1}],
15+
[],
16+
];
17+
const {distances, predecessors} = dijkstra(graph, 0);
18+
expect(distances).toEqual([0, 1, 3, 4]);
19+
expect(predecessors).toEqual([-1, 0, 1, 2]);
20+
});
21+
test('should handle unreachable vertices', () => {
22+
const graph = [[{to: 1, weight: 1}], [{to: 2, weight: 2}], [], []];
23+
const {distances, predecessors} = dijkstra(graph, 0);
24+
expect(distances).toEqual([0, 1, 3, Infinity]);
25+
expect(predecessors).toEqual([-1, 0, 1, -1]);
26+
});
27+
test('should throw error for negative edge weights', () => {
28+
const graph = [
29+
[
30+
{to: 1, weight: 1},
31+
{to: 2, weight: -2},
32+
],
33+
[],
34+
[],
35+
];
36+
expect(() => dijkstra(graph, 0)).toThrow(
37+
"Dijkstra's algorithm does not work with negative edge weights",
38+
);
39+
});
40+
test('should handle a graph with a single vertex', () => {
41+
const graph = [[]];
42+
const {distances, predecessors} = dijkstra(graph, 0);
43+
expect(distances).toEqual([0]);
44+
expect(predecessors).toEqual([-1]);
45+
});
46+
test('should handle a graph with multiple paths to the same vertex', () => {
47+
const graph = [
48+
[
49+
{to: 1, weight: 1},
50+
{to: 2, weight: 2},
51+
],
52+
[{to: 3, weight: 4}],
53+
[{to: 3, weight: 3}],
54+
[],
55+
];
56+
const {distances, predecessors} = dijkstra(graph, 0);
57+
expect(distances[3]).toBe(5);
58+
expect(predecessors[3]).toBe(1);
59+
});
60+
});
61+
62+
describe('reconstructPath', () => {
63+
test('should reconstruct the correct path', () => {
64+
const predecessors = [-1, 0, 1, 2];
65+
const path = reconstructPath(0, 3, predecessors);
66+
expect(path).toEqual([0, 1, 2, 3]);
67+
});
68+
test('should handle direct paths', () => {
69+
const predecessors = [-1, 0, 0, 0];
70+
const path = reconstructPath(0, 1, predecessors);
71+
expect(path).toEqual([0, 1]);
72+
});
73+
test('should return null for unreachable vertices', () => {
74+
const predecessors = [-1, 0, 1, -1];
75+
const path = reconstructPath(0, 3, predecessors);
76+
expect(path).toBeNull();
77+
});
78+
test('should handle path from vertex to itself', () => {
79+
const predecessors = [-1, 0, 1, 2];
80+
const path = reconstructPath(0, 0, predecessors);
81+
expect(path).toEqual([0]);
82+
});
83+
test('should handle invalid target vertex', () => {
84+
const predecessors = [-1, 0, 1, 2];
85+
const path = reconstructPath(0, 4, predecessors);
86+
expect(path).toBeNull();
87+
});
88+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Dijkstra's algorithm implementation for finding the shortest path in a weighted graph.
3+
* The algorithm works with non-negative edge weights only.
4+
*/
5+
6+
interface Edge {
7+
to: number;
8+
weight: number;
9+
}
10+
11+
interface DijkstraResult {
12+
distances: number[];
13+
predecessors: number[];
14+
}
15+
16+
/**
17+
* Implements Dijkstra's algorithm to find shortest paths from a source vertex to all other vertices.
18+
* This implementation uses an adjacency list representation of the graph.
19+
*
20+
* @param graph - Adjacency list representation of the graph where graph[i] is an array of edges from vertex i.
21+
* @param source - The source vertex to start the search from.
22+
* @returns An object containing the distances array and predecessors array.
23+
* @throws If any edge has a negative weight.
24+
*/
25+
export function dijkstra(graph: Edge[][], source: number): DijkstraResult {
26+
const n = graph.length;
27+
const distances: number[] = Array(n).fill(Infinity);
28+
distances[source] = 0;
29+
30+
const predecessors: number[] = Array(n).fill(-1);
31+
const visited: boolean[] = Array(n).fill(false);
32+
33+
// Check for negative weights, which are not allowed in Dijkstra's algorithm
34+
for (const edges of graph) {
35+
for (const edge of edges) {
36+
if (edge.weight < 0) {
37+
throw new Error("Dijkstra's algorithm does not work with negative edge weights");
38+
}
39+
}
40+
}
41+
42+
for (let i = 0; i < n; i++) {
43+
const u = minDistanceVertex(distances, visited);
44+
if (u === -1 || distances[u] === Infinity) {
45+
break;
46+
}
47+
// Mark the picked vertex as visited
48+
visited[u] = true;
49+
// Update the distance values of the adjacent vertices
50+
for (const edge of graph[u]) {
51+
const v = edge.to;
52+
53+
// Update if:
54+
// 1. The vertex v is not visited
55+
// 2. There is an edge from u to v
56+
// 3. The total weight of path from source to v through u is smaller than the current value of distances[v]
57+
if (!visited[v] && distances[u] + edge.weight < distances[v]) {
58+
distances[v] = distances[u] + edge.weight;
59+
predecessors[v] = u;
60+
}
61+
}
62+
}
63+
return {distances, predecessors};
64+
}
65+
66+
/**
67+
* Helper function to find the vertex with the minimum distance value,
68+
* from the set of vertices not yet included in the shortest path tree.
69+
*
70+
* @param distances - Array of distances from source to each vertex.
71+
* @param visited - Array indicating which vertices have been visited.
72+
* @returns The index of the vertex with the minimum distance, or -1 if no unvisited vertices remain.
73+
*/
74+
function minDistanceVertex(distances: number[], visited: boolean[]): number {
75+
let min = Infinity;
76+
let minIndex = -1;
77+
for (let v = 0; v < distances.length; v++) {
78+
if (!visited[v] && distances[v] <= min) {
79+
min = distances[v];
80+
minIndex = v;
81+
}
82+
}
83+
return minIndex;
84+
}
85+
86+
/**
87+
* Reconstructs the shortest path from a source vertex to a target vertex.
88+
*
89+
* @param source - The source vertex.
90+
* @param target - The target vertex.
91+
* @param predecessors - Array of predecessor vertices obtained from Dijkstra's algorithm.
92+
* @returns An array representing the path from source to target, or null if no path exists.
93+
*/
94+
export function reconstructPath(
95+
source: number,
96+
target: number,
97+
predecessors: number[],
98+
): number[] | null {
99+
if (
100+
target < 0 ||
101+
target >= predecessors.length ||
102+
(predecessors[target] === -1 && source !== target)
103+
) {
104+
return null;
105+
}
106+
107+
const path: number[] = [];
108+
let current = target;
109+
110+
while (current !== -1) {
111+
path.unshift(current);
112+
if (current === source) {
113+
break;
114+
}
115+
current = predecessors[current];
116+
}
117+
// Check if the path starts with the source vertex
118+
return path[0] === source ? path : null;
119+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {topologicalSort} from './topological-sort';
2+
3+
describe('topologicalSort', () => {
4+
test('should return a valid topological ordering for a simple DAG', () => {
5+
const graph = [[1], [2], [3], []];
6+
const result = topologicalSort(graph);
7+
expect(result).toEqual([0, 1, 2, 3]);
8+
});
9+
test('should return a valid topological ordering for a more complex DAG', () => {
10+
const graph = [[1, 2], [3], [3], []];
11+
const result = topologicalSort(graph);
12+
expect(result).not.toBeNull();
13+
if (result) {
14+
expect(result.indexOf(0)).toBeLessThan(result.indexOf(1));
15+
expect(result.indexOf(0)).toBeLessThan(result.indexOf(2));
16+
expect(result.indexOf(1)).toBeLessThan(result.indexOf(3));
17+
expect(result.indexOf(2)).toBeLessThan(result.indexOf(3));
18+
}
19+
});
20+
test('should return null for a graph with a cycle', () => {
21+
const graph = [[1], [2], [0]];
22+
const result = topologicalSort(graph);
23+
expect(result).toBeNull();
24+
});
25+
test('should return a valid ordering for a disconnected DAG', () => {
26+
const graph = [[1], [], [3], []];
27+
const result = topologicalSort(graph);
28+
expect(result).not.toBeNull();
29+
if (result) {
30+
expect(result.indexOf(0)).toBeLessThan(result.indexOf(1));
31+
expect(result.indexOf(2)).toBeLessThan(result.indexOf(3));
32+
}
33+
});
34+
35+
test('should handle a graph with a single vertex', () => {
36+
const graph = [[]];
37+
const result = topologicalSort(graph);
38+
expect(result).toEqual([0]);
39+
});
40+
41+
test('should handle an empty graph', () => {
42+
const graph: number[][] = [];
43+
const result = topologicalSort(graph);
44+
expect(result).toEqual([]);
45+
});
46+
test('should handle a graph with self-loops', () => {
47+
const graph = [[1], [1, 2], []];
48+
const result = topologicalSort(graph);
49+
expect(result).toBeNull();
50+
});
51+
test('should handle a graph with multiple cycles', () => {
52+
const graph = [[1], [2], [0], [4], [3]];
53+
const result = topologicalSort(graph);
54+
expect(result).toBeNull();
55+
});
56+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Implementation of Topological Sort algorithm.
3+
*
4+
* Topological Sort is an algorithm used to linearly order the vertices of a directed graph
5+
* such that for every directed edge (u, v), vertex u comes before vertex v in the ordering.
6+
*
7+
* This algorithm only works on Directed Acyclic Graphs (DAGs).
8+
* If the graph contains a cycle, no valid topological sort exists.
9+
*/
10+
11+
const VertexState = {
12+
UNVISITED: 0,
13+
VISITING: 1,
14+
VISITED: 2,
15+
} as const;
16+
17+
type VertexState = (typeof VertexState)[keyof typeof VertexState];
18+
19+
/**
20+
* Performs a topological sort on a directed graph represented as an adjacency list.
21+
*
22+
* @param graph - Adjacency list representation of the graph where graph[i] contains
23+
* the vertices that vertex i has edges to.
24+
* @returns An array of vertices in topological order, or null if the graph contains a cycle.
25+
*/
26+
export function topologicalSort(graph: number[][]): number[] | null {
27+
const n = graph.length;
28+
const state: VertexState[] = Array(n).fill(VertexState.UNVISITED);
29+
const result: number[] = [];
30+
31+
const dfs = (vertex: number): boolean => {
32+
if (state[vertex] === VertexState.VISITING) {
33+
return false;
34+
}
35+
36+
// If vertex is already visited, no need to process it again
37+
if (state[vertex] === VertexState.VISITED) {
38+
return true;
39+
}
40+
41+
state[vertex] = VertexState.VISITING;
42+
43+
for (const neighbor of graph[vertex]) {
44+
if (!dfs(neighbor)) {
45+
return false;
46+
}
47+
}
48+
state[vertex] = VertexState.VISITED;
49+
result.unshift(vertex);
50+
return true;
51+
};
52+
53+
// Try to visit each unvisited vertex
54+
for (let i = 0; i < n; i++) {
55+
if (state[i] === VertexState.UNVISITED) {
56+
if (!dfs(i)) {
57+
return null;
58+
}
59+
}
60+
}
61+
return result;
62+
}

0 commit comments

Comments
 (0)