diff --git a/README.md b/README.md index 3be218bf..1ca21bd6 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ * [Dijkstra Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/dijkstra) - finding shortest path to all graph vertices * [Bellman-Ford Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/bellman-ford) - finding shortest path to all graph vertices * [Detect Cycle](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/detect-cycle) - for both directed and undirected graphs (DFS and Disjoint Set based versions) - * [Prim’s Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/prim) - finding Minimum Spanning Tree (MST) + * [Prim’s Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/prim) - finding Minimum Spanning Tree (MST) for weighted undirected graph * Kruskal’s Algorithm - finding Minimum Spanning Tree (MST) * Topological Sorting * Eulerian path, Eulerian circuit diff --git a/src/algorithms/graph/prim/README.md b/src/algorithms/graph/prim/README.md index 908629c2..d9012c94 100644 --- a/src/algorithms/graph/prim/README.md +++ b/src/algorithms/graph/prim/README.md @@ -44,3 +44,4 @@ are two possibilities of minimum spanning tree of the given graph. - [Minimum Spanning Tree on Wikipedia](https://en.wikipedia.org/wiki/Minimum_spanning_tree) - [Prim's Algorithm on Wikipedia](https://en.wikipedia.org/wiki/Prim%27s_algorithm) - [Prim's Algorithm on YouTube by Tushar Roy](https://www.youtube.com/watch?v=oP2-8ysT3QQ) +- [Prim's Algorithm on YouTube by Michael Sambol](https://www.youtube.com/watch?v=cplfcGZmX7I) diff --git a/src/algorithms/graph/prim/__test__/prim.test.js b/src/algorithms/graph/prim/__test__/prim.test.js new file mode 100644 index 00000000..797f5f5b --- /dev/null +++ b/src/algorithms/graph/prim/__test__/prim.test.js @@ -0,0 +1,91 @@ +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import Graph from '../../../../data-structures/graph/Graph'; +import prim from '../prim'; + +describe('prim', () => { + it('should fire an error for directed graph', () => { + function applyPrimToDirectedGraph() { + const graph = new Graph(true); + + prim(graph); + } + + expect(applyPrimToDirectedGraph).toThrowError(); + }); + + it('should find minimum spanning tree', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + const vertexD = new GraphVertex('D'); + const vertexE = new GraphVertex('E'); + const vertexF = new GraphVertex('F'); + const vertexG = new GraphVertex('G'); + + const edgeAB = new GraphEdge(vertexA, vertexB, 2); + const edgeAD = new GraphEdge(vertexA, vertexD, 3); + const edgeAC = new GraphEdge(vertexA, vertexC, 3); + const edgeBC = new GraphEdge(vertexB, vertexC, 4); + const edgeBE = new GraphEdge(vertexB, vertexE, 3); + const edgeDF = new GraphEdge(vertexD, vertexF, 7); + const edgeEC = new GraphEdge(vertexE, vertexC, 1); + const edgeEF = new GraphEdge(vertexE, vertexF, 8); + const edgeFG = new GraphEdge(vertexF, vertexG, 9); + const edgeFC = new GraphEdge(vertexF, vertexC, 6); + + const graph = new Graph(); + + graph + .addEdge(edgeAB) + .addEdge(edgeAD) + .addEdge(edgeAC) + .addEdge(edgeBC) + .addEdge(edgeBE) + .addEdge(edgeDF) + .addEdge(edgeEC) + .addEdge(edgeEF) + .addEdge(edgeFC) + .addEdge(edgeFG); + + expect(graph.getWeight()).toEqual(46); + + const minimumSpanningTree = prim(graph); + + expect(minimumSpanningTree.getWeight()).toBe(24); + expect(minimumSpanningTree.getAllVertices().length).toBe(graph.getAllVertices().length); + expect(minimumSpanningTree.getAllEdges().length).toBe(graph.getAllVertices().length - 1); + expect(minimumSpanningTree.toString()).toBe('A,B,D,C,E,F,G'); + }); + + it('should find minimum spanning tree for simple graph', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + const vertexD = new GraphVertex('D'); + + const edgeAB = new GraphEdge(vertexA, vertexB, 1); + const edgeAD = new GraphEdge(vertexA, vertexD, 3); + const edgeBC = new GraphEdge(vertexB, vertexC, 1); + const edgeBD = new GraphEdge(vertexB, vertexD, 3); + const edgeCD = new GraphEdge(vertexC, vertexD, 1); + + const graph = new Graph(); + + graph + .addEdge(edgeAB) + .addEdge(edgeAD) + .addEdge(edgeBC) + .addEdge(edgeBD) + .addEdge(edgeCD); + + expect(graph.getWeight()).toEqual(9); + + const minimumSpanningTree = prim(graph); + + expect(minimumSpanningTree.getWeight()).toBe(3); + expect(minimumSpanningTree.getAllVertices().length).toBe(graph.getAllVertices().length); + expect(minimumSpanningTree.getAllEdges().length).toBe(graph.getAllVertices().length - 1); + expect(minimumSpanningTree.toString()).toBe('A,B,C,D'); + }); +}); diff --git a/src/algorithms/graph/prim/prim.js b/src/algorithms/graph/prim/prim.js new file mode 100644 index 00000000..1940e305 --- /dev/null +++ b/src/algorithms/graph/prim/prim.js @@ -0,0 +1,73 @@ +import Graph from '../../../data-structures/graph/Graph'; +import PriorityQueue from '../../../data-structures/priority-queue/PriorityQueue'; + +/** + * @param {Graph} graph + * @return {Graph} + */ +export default function prim(graph) { + // It should fire error if graph is directed since the algorithm works only + // for undirected graphs. + if (graph.isDirected) { + throw new Error('Prim\'s algorithms works only for undirected graphs'); + } + + // Init new graph that will contain minimum spanning tree of original graph. + const minimumSpanningTree = new Graph(); + + // This priority queue will contain all the edges that are starting from + // visited nodes and they will be ranked by edge weight - so that on each step + // we would always pick the edge with minimal edge weight. + const edgesQueue = new PriorityQueue(); + + // Set of vertices that has been already visited. + const visitedVertices = {}; + + // Vertex from which we will start graph traversal. + const startVertex = graph.getAllVertices()[0]; + + // Add start vertex to the set of visited ones. + visitedVertices[startVertex.getKey()] = startVertex; + + // Add all edges of start vertex to the queue. + startVertex.getEdges().forEach((graphEdge) => { + edgesQueue.add(graphEdge, graphEdge.weight); + }); + + // Now let's explore all queued edges. + while (!edgesQueue.isEmpty()) { + // Fetch next queued edge with minimal weight. + /** @var {GraphEdge} currentEdge */ + const currentMinEdge = edgesQueue.poll(); + + // Find out the next unvisited minimal vertex to traverse. + let nextMinVertex = null; + if (!visitedVertices[currentMinEdge.startVertex.getKey()]) { + nextMinVertex = currentMinEdge.startVertex; + } else if (!visitedVertices[currentMinEdge.endVertex.getKey()]) { + nextMinVertex = currentMinEdge.endVertex; + } + + // If all vertices of current edge has been already visited then skip this round. + if (nextMinVertex) { + // Add current min edge to MST. + minimumSpanningTree.addEdge(currentMinEdge); + + // Add vertex to the set of visited ones. + visitedVertices[nextMinVertex.getKey()] = nextMinVertex; + + // Add all current vertex's edges to the queue. + nextMinVertex.getEdges().forEach((graphEdge) => { + // Add only vertices that link to unvisited nodes. + if ( + !visitedVertices[graphEdge.startVertex.getKey()] || + !visitedVertices[graphEdge.endVertex.getKey()] + ) { + edgesQueue.add(graphEdge, graphEdge.weight); + } + }); + } + } + + return minimumSpanningTree; +} diff --git a/src/data-structures/graph/Graph.js b/src/data-structures/graph/Graph.js index ad6b886b..48a1171f 100644 --- a/src/data-structures/graph/Graph.js +++ b/src/data-structures/graph/Graph.js @@ -110,12 +110,18 @@ export default class Graph { return null; } + /** + * @return {number} + */ getWeight() { return this.getAllEdges().reduce((weight, graphEdge) => { return weight + graphEdge.weight; }, 0); } + /** + * @return {string} + */ toString() { return Object.keys(this.vertices).toString(); } diff --git a/src/data-structures/graph/GraphEdge.js b/src/data-structures/graph/GraphEdge.js index 9da70171..618fb9e2 100644 --- a/src/data-structures/graph/GraphEdge.js +++ b/src/data-structures/graph/GraphEdge.js @@ -19,4 +19,11 @@ export default class GraphEdge { return `${startVertexKey}_${endVertexKey}`; } + + /** + * @return {string} + */ + toString() { + return this.getKey(); + } } diff --git a/src/data-structures/graph/GraphVertex.js b/src/data-structures/graph/GraphVertex.js index c0f99122..06c7d67d 100644 --- a/src/data-structures/graph/GraphVertex.js +++ b/src/data-structures/graph/GraphVertex.js @@ -1,6 +1,9 @@ import LinkedList from '../linked-list/LinkedList'; export default class GraphVertex { + /** + * @param {*} value + */ constructor(value) { if (value === undefined) { throw new Error('Graph vertex must have a value'); @@ -37,6 +40,13 @@ export default class GraphVertex { return edges.map(neighborsConverter); } + /** + * @return {GraphEdge[]} + */ + getEdges() { + return this.edges.toArray().map(linkedListNode => linkedListNode.value); + } + /** * @param {GraphEdge} requiredEdge * @returns {boolean} diff --git a/src/data-structures/graph/__test__/GraphEdge.test.js b/src/data-structures/graph/__test__/GraphEdge.test.js index 347dbca0..765b6156 100644 --- a/src/data-structures/graph/__test__/GraphEdge.test.js +++ b/src/data-structures/graph/__test__/GraphEdge.test.js @@ -8,6 +8,7 @@ describe('GraphEdge', () => { const edge = new GraphEdge(startVertex, endVertex); expect(edge.getKey()).toBe('A_B'); + expect(edge.toString()).toBe('A_B'); expect(edge.startVertex).toEqual(startVertex); expect(edge.endVertex).toEqual(endVertex); expect(edge.weight).toEqual(0); diff --git a/src/data-structures/graph/__test__/GraphVertex.test.js b/src/data-structures/graph/__test__/GraphVertex.test.js index 4851215b..679f587a 100644 --- a/src/data-structures/graph/__test__/GraphVertex.test.js +++ b/src/data-structures/graph/__test__/GraphVertex.test.js @@ -21,17 +21,20 @@ describe('GraphVertex', () => { expect(vertex.toString()).toBe('A'); expect(vertex.getKey()).toBe('A'); expect(vertex.edges.toString()).toBe(''); + expect(vertex.getEdges()).toEqual([]); }); it('should add edges to vertex and check if it exists', () => { const vertexA = new GraphVertex('A'); - const vertexB = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); const edgeAB = new GraphEdge(vertexA, vertexB); vertexA.addEdge(edgeAB); expect(vertexA.hasEdge(edgeAB)).toBeTruthy(); expect(vertexB.hasEdge(edgeAB)).toBeFalsy(); + expect(vertexA.getEdges().length).toBe(1); + expect(vertexA.getEdges()[0].toString()).toBe('A_B'); }); it('should return vertex neighbors in case if current node is start one', () => {