From c00ad9cc542576dd773698b053c60b32d1e12613 Mon Sep 17 00:00:00 2001 From: Joshua Cao Date: Fri, 9 Jun 2023 01:34:58 -0700 Subject: [PATCH 1/4] feat: add dijkstra with adjacency matrix --- graph/dijkstra.ts | 50 ++++++++++++++++++++++++++++ graph/test/dijkstra.test.ts | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 graph/dijkstra.ts create mode 100644 graph/test/dijkstra.test.ts diff --git a/graph/dijkstra.ts b/graph/dijkstra.ts new file mode 100644 index 00000000..4272d8a9 --- /dev/null +++ b/graph/dijkstra.ts @@ -0,0 +1,50 @@ +/** + * @function dijkstra + * @description Compute the shortest path from a source node to all other nodes. The input graph is an adjacency matrix where `0` represents no edge, and a positive value represents the weight of the edge. All weights are positive. + * @Complexity_Analysis + * Time complexity: O(V^2) + * Space Complexity: O(V) + * @param {number[][]} graph - The adjacency matrix graph + * @param {number} start - The source node + * @return {number[]} - The shortest path to each node + * @see https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm + */ +export const dijkstra = (graph: number[][], start: number): number[] => { + let visited = Array(graph.length).fill(false); + let paths = Array(graph.length).fill(Number.MAX_SAFE_INTEGER); + paths[start] = 0; + + let node = start; + while (node !== -1) { + visited[node] = true; + graph[node].forEach((weight, child) => { + if (child == node || weight === 0) { + return; + } + let new_path = paths[node] + weight; + if (new_path < paths[child]) { + paths[child] = new_path; + } + }); + node = extract_min(paths, visited); + } + + return paths; +} + +const extract_min = (paths: number[], visited: boolean[]): number => { + let min = Number.MAX_SAFE_INTEGER; + let node = -1; + + for (let i = 0; i < paths.length; ++i) { + if (visited[i]) { + continue; + } + if (paths[i] < min) { + min = paths[i]; + node = i; + } + } + return node; +} + diff --git a/graph/test/dijkstra.test.ts b/graph/test/dijkstra.test.ts new file mode 100644 index 00000000..8272370a --- /dev/null +++ b/graph/test/dijkstra.test.ts @@ -0,0 +1,65 @@ +import { dijkstra } from "../dijkstra"; + +describe("dijkstra", () => { + + const init_graph = (N: number): number[][] => { + let graph = Array(N); + for (let i = 0; i < N; ++i) { + graph[i] = Array(N).fill(0); + } + return graph; + } + + const add_edge = (graph: number[][], a: number, b: number, weight: number) => { + graph[a][b] = weight; + graph[b][a] = weight; + } + + it("should return the correct value", () => { + // Example from https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/ + let graph = init_graph(9); + add_edge(graph, 0, 1, 4); + add_edge(graph, 0, 7, 8); + add_edge(graph, 1, 2, 8); + add_edge(graph, 1, 7, 11); + add_edge(graph, 2, 3, 7); + add_edge(graph, 2, 5, 4); + add_edge(graph, 2, 8, 2); + add_edge(graph, 3, 4, 9); + add_edge(graph, 3, 5, 14); + add_edge(graph, 4, 5, 10); + add_edge(graph, 5, 6, 2); + add_edge(graph, 6, 7, 1); + add_edge(graph, 6, 8, 6); + add_edge(graph, 7, 8, 7); + + expect(dijkstra(graph, 0)).toStrictEqual([0, 4, 12, 19, 21, 11, 9, 8, 14]); + }); + + it("should return the correct value for single element graph", () => { + expect(dijkstra([[0]], 0)).toStrictEqual([0]); + expect(dijkstra([[10]], 0)).toStrictEqual([0]); + }); + + let linear_graph = init_graph(4); + add_edge(linear_graph, 0, 1, 1); + add_edge(linear_graph, 1, 2, 2); + add_edge(linear_graph, 2, 3, 3); + test.each([[0, [0, 1, 3, 6]], [1, [1, 0, 2, 5]], [2, [3, 2, 0, 3]], [3, [6, 5, 3, 0]]])( + "correct result for linear graph with source node %i", + (source, result) => { + expect(dijkstra(linear_graph, source)).toStrictEqual(result); + } + ); + + let unreachable_graph = init_graph(3); + add_edge(unreachable_graph, 0, 1, 1); + const m = Number.MAX_SAFE_INTEGER + test.each([[0, [0, 1, m]], [1, [1, 0, m]], [2, [m, m, 0]]])( + "correct result for graph with unreachable nodes with source node %i", + (source, result) => { + expect(dijkstra(unreachable_graph, source)).toStrictEqual(result); + } + ); +}) + From d5a20b89480f82dfddfdd5f1a7e2f886f8ef70bd Mon Sep 17 00:00:00 2001 From: Joshua Cao Date: Fri, 9 Jun 2023 21:29:21 -0700 Subject: [PATCH 2/4] Renames, initialize distances to Infinity, remove external link --- graph/dijkstra.ts | 14 +++++++------- graph/test/dijkstra.test.ts | 4 +--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/graph/dijkstra.ts b/graph/dijkstra.ts index 4272d8a9..adbc9d63 100644 --- a/graph/dijkstra.ts +++ b/graph/dijkstra.ts @@ -11,8 +11,8 @@ */ export const dijkstra = (graph: number[][], start: number): number[] => { let visited = Array(graph.length).fill(false); - let paths = Array(graph.length).fill(Number.MAX_SAFE_INTEGER); - paths[start] = 0; + let distances = Array(graph.length).fill(Infinity); + distances[start] = 0; let node = start; while (node !== -1) { @@ -21,15 +21,15 @@ export const dijkstra = (graph: number[][], start: number): number[] => { if (child == node || weight === 0) { return; } - let new_path = paths[node] + weight; - if (new_path < paths[child]) { - paths[child] = new_path; + let new_path = distances[node] + weight; + if (new_path < distances[child]) { + distances[child] = new_path; } }); - node = extract_min(paths, visited); + node = extract_min(distances, visited); } - return paths; + return distances; } const extract_min = (paths: number[], visited: boolean[]): number => { diff --git a/graph/test/dijkstra.test.ts b/graph/test/dijkstra.test.ts index 8272370a..22423449 100644 --- a/graph/test/dijkstra.test.ts +++ b/graph/test/dijkstra.test.ts @@ -16,7 +16,6 @@ describe("dijkstra", () => { } it("should return the correct value", () => { - // Example from https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/ let graph = init_graph(9); add_edge(graph, 0, 1, 4); add_edge(graph, 0, 7, 8); @@ -54,8 +53,7 @@ describe("dijkstra", () => { let unreachable_graph = init_graph(3); add_edge(unreachable_graph, 0, 1, 1); - const m = Number.MAX_SAFE_INTEGER - test.each([[0, [0, 1, m]], [1, [1, 0, m]], [2, [m, m, 0]]])( + test.each([[0, [0, 1, Infinity]], [1, [1, 0, Infinity]], [2, [Infinity, Infinity, 0]]])( "correct result for graph with unreachable nodes with source node %i", (source, result) => { expect(dijkstra(unreachable_graph, source)).toStrictEqual(result); From 227cffb6b50de3b430cca229fab65e7bfe4cc5e9 Mon Sep 17 00:00:00 2001 From: Joshua Cao Date: Sat, 10 Jun 2023 11:12:47 -0700 Subject: [PATCH 3/4] dijkstra implementation uses adjacency list and min heap --- graph/dijkstra.ts | 51 +++++++++++++------------------------ graph/test/dijkstra.test.ts | 14 +++++----- 2 files changed, 24 insertions(+), 41 deletions(-) diff --git a/graph/dijkstra.ts b/graph/dijkstra.ts index adbc9d63..02c494f1 100644 --- a/graph/dijkstra.ts +++ b/graph/dijkstra.ts @@ -1,50 +1,35 @@ +import { MinHeap } from '../data_structures/heap/min_heap'; /** * @function dijkstra - * @description Compute the shortest path from a source node to all other nodes. The input graph is an adjacency matrix where `0` represents no edge, and a positive value represents the weight of the edge. All weights are positive. + * @description Compute the shortest path from a source node to all other nodes. The input graph is in adjacency list form. It is a multidimensional array of edges. graph[i] holds the edges for the i'th node. Each edge is a 2-tuple where the 0'th item is the destination node, and the 1'th item is the edge weight. * @Complexity_Analysis - * Time complexity: O(V^2) + * Time complexity: O((V+E)*log(V)). For fully connected graphs, it is O(E*log(V)). * Space Complexity: O(V) - * @param {number[][]} graph - The adjacency matrix graph + * @param {[number, number][][]} graph - The graph in adjacency list form * @param {number} start - The source node * @return {number[]} - The shortest path to each node * @see https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm */ -export const dijkstra = (graph: number[][], start: number): number[] => { - let visited = Array(graph.length).fill(false); +export const dijkstra = (graph: [number, number][][], start: number): number[] => { + // We use a priority queue to make sure we always visit the closest node. By using a MinHeap, + // finding the next node is O(log(V)) + let priorityQueue = new MinHeap([start]); + // We save the shortest distance to each node in `distances`. If a node is + // unreachable from the start node, its distance is Infinity. let distances = Array(graph.length).fill(Infinity); distances[start] = 0; - let node = start; - while (node !== -1) { - visited[node] = true; - graph[node].forEach((weight, child) => { - if (child == node || weight === 0) { - return; - } - let new_path = distances[node] + weight; - if (new_path < distances[child]) { - distances[child] = new_path; + while (priorityQueue.size() > 0) { + const node = priorityQueue.extract(); + graph[node].forEach(([child, weight]) => { + let new_distance = distances[node] + weight; + if (new_distance < distances[child]) { + // Found a new shortest path to child node. Record its distance and add child to the queue. + distances[child] = new_distance; + priorityQueue.insert(child); } }); - node = extract_min(distances, visited); } return distances; } - -const extract_min = (paths: number[], visited: boolean[]): number => { - let min = Number.MAX_SAFE_INTEGER; - let node = -1; - - for (let i = 0; i < paths.length; ++i) { - if (visited[i]) { - continue; - } - if (paths[i] < min) { - min = paths[i]; - node = i; - } - } - return node; -} - diff --git a/graph/test/dijkstra.test.ts b/graph/test/dijkstra.test.ts index 22423449..eacf2e68 100644 --- a/graph/test/dijkstra.test.ts +++ b/graph/test/dijkstra.test.ts @@ -2,17 +2,17 @@ import { dijkstra } from "../dijkstra"; describe("dijkstra", () => { - const init_graph = (N: number): number[][] => { + const init_graph = (N: number): [number, number][][] => { let graph = Array(N); for (let i = 0; i < N; ++i) { - graph[i] = Array(N).fill(0); + graph[i] = []; } return graph; } - const add_edge = (graph: number[][], a: number, b: number, weight: number) => { - graph[a][b] = weight; - graph[b][a] = weight; + const add_edge = (graph: [number, number][][], a: number, b: number, weight: number) => { + graph[a].push([b, weight]); + graph[b].push([a, weight]); } it("should return the correct value", () => { @@ -31,13 +31,11 @@ describe("dijkstra", () => { add_edge(graph, 6, 7, 1); add_edge(graph, 6, 8, 6); add_edge(graph, 7, 8, 7); - expect(dijkstra(graph, 0)).toStrictEqual([0, 4, 12, 19, 21, 11, 9, 8, 14]); }); it("should return the correct value for single element graph", () => { - expect(dijkstra([[0]], 0)).toStrictEqual([0]); - expect(dijkstra([[10]], 0)).toStrictEqual([0]); + expect(dijkstra([[]], 0)).toStrictEqual([0]); }); let linear_graph = init_graph(4); From c070c72f1859610d6d855ef750d648055d374db8 Mon Sep 17 00:00:00 2001 From: Joshua Cao Date: Tue, 13 Jun 2023 00:13:58 -0700 Subject: [PATCH 4/4] Implement heap increasePriority and let Dijkstra use it --- data_structures/heap/heap.ts | 84 ++++++++++++++++------ data_structures/heap/test/max_heap.test.ts | 5 +- data_structures/heap/test/min_heap.test.ts | 34 +++++++-- graph/dijkstra.ts | 14 ++-- 4 files changed, 105 insertions(+), 32 deletions(-) diff --git a/data_structures/heap/heap.ts b/data_structures/heap/heap.ts index a5def710..28915849 100644 --- a/data_structures/heap/heap.ts +++ b/data_structures/heap/heap.ts @@ -12,16 +12,13 @@ */ export abstract class Heap { - private heap: T[]; + protected heap: T[]; // A comparison function. Returns true if a should be the parent of b. - private compare: (a: any, b: any) => boolean; + private compare: (a: T, b: T) => boolean; - constructor(elements: T[] = [], compare: (a: T, b: T) => boolean) { + constructor(compare: (a: T, b: T) => boolean) { this.heap = []; this.compare = compare; - for (let element of elements) { - this.insert(element); - } } /** @@ -68,17 +65,20 @@ export abstract class Heap { return this.size() === 0; } - private bubbleUp(): void { - let index = this.size() - 1; + protected swap(a: number, b: number) { + [this.heap[a], this.heap[b]] = [ + this.heap[b], + this.heap[a], + ]; + } + + protected bubbleUp(index = this.size() - 1): void { let parentIndex; while (index > 0) { parentIndex = Math.floor((index - 1) / 2); if (this.isRightlyPlaced(index, parentIndex)) break; - [this.heap[parentIndex], this.heap[index]] = [ - this.heap[index], - this.heap[parentIndex], - ]; + this.swap(parentIndex, index); index = parentIndex; } } @@ -95,10 +95,7 @@ export abstract class Heap { rightChildIndex ); if (this.isRightlyPlaced(childIndexToSwap, index)) break; - [this.heap[childIndexToSwap], this.heap[index]] = [ - this.heap[index], - this.heap[childIndexToSwap], - ]; + this.swap(childIndexToSwap, index); index = childIndexToSwap; leftChildIndex = this.getLeftChildIndex(index); rightChildIndex = this.getRightChildIndex(index); @@ -140,13 +137,60 @@ export abstract class Heap { } export class MinHeap extends Heap { - constructor(elements: T[] = [], compare = (a: T, b: T) => { return a < b }) { - super(elements, compare); + constructor(compare = (a: T, b: T) => { return a < b }) { + super(compare); } } export class MaxHeap extends Heap { - constructor(elements: T[] = [], compare = (a: T, b: T) => { return a > b }) { - super(elements, compare); + constructor(compare = (a: T, b: T) => { return a > b }) { + super(compare); + } +} + +// Priority queue that supports increasePriority() in O(log(n)). The limitation is that there can only be a single element for each key, and the max number or keys must be specified at heap construction. Most of the functions are wrappers around MinHeap functions and update the keys array. +export class PriorityQueue extends MinHeap { + // Maps from the n'th node to its index within the heap. + private keys: number[]; + // Maps from element to its index with keys. + private keys_index: (a: T) => number; + + constructor(keys_index: (a: T) => number, num_keys: number, compare = (a: T, b: T) => { return a < b }) { + super(compare); + this.keys = Array(num_keys).fill(-1); + this.keys_index = keys_index; + } + + protected swap(a: number, b: number) { + let akey = this.keys_index(this.heap[a]); + let bkey = this.keys_index(this.heap[b]); + [this.keys[akey], this.keys[bkey]] = [this.keys[bkey], this.keys[akey]]; + super.swap(a, b); + } + + public insert(value: T) { + this.keys[this.keys_index(value)] = this.size(); + super.insert(value); + } + + public extract(): T { + // Unmark the the highest priority element and set key to zero for the last element in the heap. + this.keys[this.keys_index(this.heap[0])] = -1; + if (this.size() > 1) { + this.keys[this.keys_index(this.heap[this.size() - 1])] = 0; + } + return super.extract(); + } + + public increasePriority(idx: number, value: T) { + if (this.keys[idx] == -1) { + // If the key does not exist, insert the value. + this.insert(value); + return; + } + let key = this.keys[idx]; + // Increase the priority and bubble it up the heap. + this.heap[key] = value; + this.bubbleUp(key); } } diff --git a/data_structures/heap/test/max_heap.test.ts b/data_structures/heap/test/max_heap.test.ts index 8ba7c398..a94739e6 100644 --- a/data_structures/heap/test/max_heap.test.ts +++ b/data_structures/heap/test/max_heap.test.ts @@ -7,7 +7,10 @@ describe("MaxHeap", () => { ]; beforeEach(() => { - heap = new MaxHeap(elements); + heap = new MaxHeap(); + for (let element of elements) { + heap.insert(element); + } }); it("should initialize a heap from input array", () => { diff --git a/data_structures/heap/test/min_heap.test.ts b/data_structures/heap/test/min_heap.test.ts index 4401d5c0..e7fa82ad 100644 --- a/data_structures/heap/test/min_heap.test.ts +++ b/data_structures/heap/test/min_heap.test.ts @@ -1,4 +1,4 @@ -import { MinHeap } from "../heap"; +import { MinHeap, PriorityQueue } from "../heap"; describe("MinHeap", () => { let heap: MinHeap; @@ -7,7 +7,10 @@ describe("MinHeap", () => { ]; beforeEach(() => { - heap = new MinHeap(elements); + heap = new MinHeap(); + for (let element of elements) { + heap.insert(element); + } }); it("should initialize a heap from input array", () => { @@ -27,7 +30,7 @@ describe("MinHeap", () => { heap.check(); }); - const extract_all = (heap: MinHeap) => { + const extract_all = (heap: MinHeap, elements: number[]) => { [...elements].sort((a, b) => a - b).forEach((element: number) => { expect(heap.extract()).toEqual(element); }); @@ -36,7 +39,7 @@ describe("MinHeap", () => { } it("should remove and return the min elements in order", () => { - extract_all(heap); + extract_all(heap, elements); }); it("should insert all, then remove and return the min elements in order", () => { @@ -46,6 +49,27 @@ describe("MinHeap", () => { }); heap.check(); expect(heap.size()).toEqual(elements.length); - extract_all(heap); + extract_all(heap, elements); + }); + + it("should increase priority", () => { + let heap = new PriorityQueue((a: number) => { return a; }, elements.length); + elements.forEach((element: number) => { + heap.insert(element); + }); + heap.check(); + expect(heap.size()).toEqual(elements.length); + + heap.increasePriority(55, 14); + heap.increasePriority(18, 16); + heap.increasePriority(81, 72); + heap.increasePriority(9, 0); + heap.increasePriority(43, 33); + heap.check(); + // Elements after increasing priority + const newElements: number[] = [ + 12, 4, 33, 42, 0, 7, 39, 16, 14, 1, 51, 34, 72, 16, + ]; + extract_all(heap, newElements); }); }); diff --git a/graph/dijkstra.ts b/graph/dijkstra.ts index 02c494f1..1d5c05ae 100644 --- a/graph/dijkstra.ts +++ b/graph/dijkstra.ts @@ -1,4 +1,4 @@ -import { MinHeap } from '../data_structures/heap/min_heap'; +import { MinHeap, PriorityQueue } from '../data_structures/heap/heap'; /** * @function dijkstra * @description Compute the shortest path from a source node to all other nodes. The input graph is in adjacency list form. It is a multidimensional array of edges. graph[i] holds the edges for the i'th node. Each edge is a 2-tuple where the 0'th item is the destination node, and the 1'th item is the edge weight. @@ -11,22 +11,24 @@ import { MinHeap } from '../data_structures/heap/min_heap'; * @see https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm */ export const dijkstra = (graph: [number, number][][], start: number): number[] => { - // We use a priority queue to make sure we always visit the closest node. By using a MinHeap, - // finding the next node is O(log(V)) - let priorityQueue = new MinHeap([start]); + // We use a priority queue to make sure we always visit the closest node. The + // queue makes comparisons based on path weights. + let priorityQueue = new PriorityQueue((a: [number, number]) => { return a[0] }, graph.length, (a: [number, number], b: [number, number]) => { return a[1] < b[1] }); + priorityQueue.insert([start, 0]); // We save the shortest distance to each node in `distances`. If a node is // unreachable from the start node, its distance is Infinity. let distances = Array(graph.length).fill(Infinity); distances[start] = 0; while (priorityQueue.size() > 0) { - const node = priorityQueue.extract(); + const [node, _] = priorityQueue.extract(); graph[node].forEach(([child, weight]) => { let new_distance = distances[node] + weight; if (new_distance < distances[child]) { // Found a new shortest path to child node. Record its distance and add child to the queue. + // If the child already exists in the queue, the priority will be updated. This will make sure the queue will be at most size V (number of vertices). + priorityQueue.increasePriority(child, [child, weight]); distances[child] = new_distance; - priorityQueue.insert(child); } }); }