From 28d028cd7bac2deaca3d866224379a3ed224dd33 Mon Sep 17 00:00:00 2001 From: Astolfo Date: Sat, 12 Feb 2022 21:05:36 -0300 Subject: [PATCH] feat/ allPaths from A to B --- data-structures/graph/Graph.js | 372 ++++++++++++------- data-structures/graph/__test__/Graph.test.js | 33 +- server.js | 12 +- utils/arrays/arrays.js | 34 ++ 4 files changed, 312 insertions(+), 139 deletions(-) diff --git a/data-structures/graph/Graph.js b/data-structures/graph/Graph.js index 9a7ee71c..4fe938fa 100755 --- a/data-structures/graph/Graph.js +++ b/data-structures/graph/Graph.js @@ -5,10 +5,10 @@ import stronglyConnectedComponents from '../../algorithms/strongly-connected-com import eulerianPath from '../../algorithms/eulerian-path/eulerianPath.js'; import depthFirstSearch from '../../algorithms/depth-first-search/depthFirstSearch.js'; import VisitMetadata from './VisitMetadata.js'; +import { arraysEqual, extendedVenn, removeArrayDuplicates } from '../../utils/arrays/arrays.js' export default class Graph { #cycles; - #density; /** @@ -26,7 +26,7 @@ export default class Graph { * @returns {Array} Graph cycles */ get cycles() { - return this.cyclicPaths(); + return this.cyclicCircuits(); } /** @@ -76,6 +76,15 @@ export default class Graph { return this.vertices[vertexKey]; } + /** + * @param {integer} vertexIndex + * @returns GraphVertex + */ + getVertexByIndex(vertexIndex) { + let adjList=this.getAdjacencyList(); + return this.vertices[adjList[vertexIndex]]; + } + /** * @param {integer} vertexIndex * @returns GraphVertex @@ -235,7 +244,7 @@ export default class Graph { this.addVertex(edge.endVertex); endVertex = this.getVertexByKey(edge.endVertex.getKey()); } - + // Check if edge has been already added. if (this.edges[edge.getKey()]) { throw new Error('Edge has already been added before. Please, choose other key!'); @@ -620,15 +629,15 @@ export default class Graph { // Step 3: Create a reversed graph let gr = this.copy().reverse(); - + // Step 4: Mark all the vertices as not visited (For second DFS) for (let i = 0; i < n_vertices; i++) visited[i] = false; - + // Step 5: Do DFS for reversed graph starting from first vertex. // Starting Vertex must be same starting point of first DFS gr.DFSUtil(0, visited); - + // If all vertices are not visited in second DFS, then // return false for (let i = 0; i < n_vertices; i++) @@ -638,11 +647,17 @@ export default class Graph { return true; } - /* The function returns one of the following values - 0 --> If graph is not Eulerian - 1 --> If graph has an Euler path (Semi-Eulerian) - 2 --> If graph has an Euler Circuit (Eulerian) - */ + /* + The function returns one of the following values + If the graph is directed: + 0 --> If graph is not Eulerian + 1 --> If graph has an Euler path (Semi-Eulerian) + 2 --> If graph has an Euler Circuit (Eulerian) + + If the graph is undirected: + false --> If graph is not Eulerian + true --> If graph has an Euler Circuit (Eulerian) + */ isEulerian(){ const adjList = this.getAdjacencyList(); const n_vertices = this.getNumVertices(); @@ -717,8 +732,7 @@ export default class Graph { } } - // DFS based function to find all bridges. It uses recursive - // function bridgeUtil() + // DFS based function to find all bridges. It uses recursive function bridgeUtil() bridges() { const graph_ = this.isDirected ? this.retrieveUndirected() : _.cloneDeep(this); @@ -741,6 +755,108 @@ export default class Graph { return bridges; } + /** + * Tarjan's algorithm for finding articulation points in graph. + * + * @return {Object} + */ + articulationVertices() { + // Set of vertices we've already visited during DFS. + const visitedSet = {}; + + // Object of articulation points. + const articulationPointsSet = {}; + + // Set of articulation points + const articulation_vertices = new Set([]); + + // Time needed to discover to the current vertex. + let discoveryTime = 0; + + // Peek the start vertex for DFS traversal. + const startVertex = this.getAllVertices()[0]; + + const dfsCallbacks = { + /** + * @param {GraphVertex} currentVertex + * @param {GraphVertex} previousVertex + */ + enterVertex: ({ currentVertex, previousVertex }) => { + // Tick discovery time. + discoveryTime += 1; + + // Put current vertex to visited set. + visitedSet[currentVertex.getKey()] = new VisitMetadata({ + discoveryTime, + lowDiscoveryTime: discoveryTime, + }); + + if (previousVertex) { + // Update children counter for previous vertex. + visitedSet[previousVertex.getKey()].independentChildrenCount += 1; + } + }, + /** + * @param {GraphVertex} currentVertex + * @param {GraphVertex} previousVertex + */ + leaveVertex: ({ currentVertex, previousVertex }) => { + if (previousVertex === null) { + // Don't do anything for the root vertex if it is already current (not previous one) + return; + } + + // Update the low time with the smallest time of adjacent vertices. + // Get minimum low discovery time from all neighbors. + /** @param {GraphVertex} neighbor */ + visitedSet[currentVertex.getKey()].lowDiscoveryTime = currentVertex.getNeighbors() + .filter((earlyNeighbor) => earlyNeighbor.getKey() !== previousVertex.getKey()) + /** + * @param {number} lowestDiscoveryTime + * @param {GraphVertex} neighbor + */ + .reduce( + (lowestDiscoveryTime, neighbor) => { + const neighborLowTime = visitedSet[neighbor.getKey()].lowDiscoveryTime; + return neighborLowTime < lowestDiscoveryTime ? neighborLowTime : lowestDiscoveryTime; + }, + visitedSet[currentVertex.getKey()].lowDiscoveryTime, + ); + + // Detect whether previous vertex is articulation point or not. + // To do so we need to check two [OR] conditions: + // 1. Is it a root vertex with at least two independent children. + // 2. If its visited time is <= low time of adjacent vertex. + if (previousVertex === startVertex) { + // Check that root vertex has at least two independent children. + if (visitedSet[previousVertex.getKey()].independentChildrenCount >= 2) { + articulationPointsSet[previousVertex.getKey()] = previousVertex; + articulation_vertices.add(previousVertex.getKey()); + } + } else { + // Get current vertex low discovery time. + let currentLowDiscoveryTime = visitedSet[currentVertex.getKey()].lowDiscoveryTime; + + // Compare current vertex low discovery time with parent discovery time. Check if there + // are any short path (back edge) exists. If we can't get to current vertex other then + // via parent then the parent vertex is articulation point for current one. + const parentDiscoveryTime = visitedSet[previousVertex.getKey()].discoveryTime; + if (parentDiscoveryTime <= currentLowDiscoveryTime) { + articulationPointsSet[previousVertex.getKey()] = previousVertex; + articulation_vertices.add(previousVertex.getKey()); + } + } + }, + + allowTraversal: ({ nextVertex }) => !visitedSet[nextVertex.getKey()], + }; + + // Do Depth First Search traversal over submitted graph. + depthFirstSearch(this, startVertex, dfsCallbacks); + + return [...articulation_vertices]; + } + /** * @abstract Tarjan method for cycles enumeration * Extracted from: https://stackoverflow.com/questions/25898100/enumerating-cycles-in-a-graph-using-tarjans-algorithm @@ -748,7 +864,7 @@ export default class Graph { * @param {GraphVertex} to_index * @return {Array[Array]} cycle */ - #tarjanCycleMethod(origin_index, curr_index, f, points, cycles, marked_stack, marked, is_finish) { + #tarjanCycleMethod(origin_index, curr_index, f, points, marked_stack, marked, is_finish) { const adjList = this.getAdjacencyList(); const n_vertices = this.getNumVertices(); @@ -793,7 +909,6 @@ export default class Graph { w, is_finish, points, - cycles, marked_stack, marked, is_finish, @@ -823,7 +938,7 @@ export default class Graph { * @abstract Returns all cycles within a graph * @return {Array[Array]} cycle */ - cyclicPaths() { + cyclicCircuits() { if (!this.isCyclic()) { return []; } @@ -840,7 +955,7 @@ export default class Graph { let cycles = []; for (let i = 0; i < n_vertices; i += 1) { const points = []; - this.#tarjanCycleMethod(i, i, false, points, cycles, marked_stack, marked); + this.#tarjanCycleMethod(i, i, false, points, marked_stack, marked); while (marked_stack.length > 0) { const u = marked_stack.pop(); @@ -854,21 +969,30 @@ export default class Graph { /** * @return {object} */ - getCycleIndices() { + getCycleIndices(recalculate=false) { let cycles_indices = {}; let cycles = []; - - if (this.#cycles.length === 0) { - cycles = this.cyclicPaths(); + + if (this.#cycles.length === 0 || recalculate) { + cycles = this.cyclicCircuits(); } + + cycles=this.#cycles for (let i = 0; i < cycles.length; i += 1) { cycles_indices[i] = cycles[i]; } - + return cycles_indices; } + /** + * @return {object} + */ + getCyclesVenn() { + return extendedVenn(this.getCycleIndices()) + } + #recurAcyclicPaths(from_index, to_index, is_visited, local_path_list, paths) { const adj_list = this.getAdjacencyList(); const adj_len = adj_list[from_index].length; @@ -911,118 +1035,17 @@ export default class Graph { is_visited[from_index] = false; } - /** - * Tarjan's algorithm for finding articulation points in graph. - * - * @return {Object} - */ - articulationVertices() { - // Set of vertices we've already visited during DFS. - const visitedSet = {}; - - // Object of articulation points. - const articulationPointsSet = {}; - - // Set of articulation points - const articulation_vertices = new Set([]); - - // Time needed to discover to the current vertex. - let discoveryTime = 0; - - // Peek the start vertex for DFS traversal. - const startVertex = this.getAllVertices()[0]; - - const dfsCallbacks = { - /** - * @param {GraphVertex} currentVertex - * @param {GraphVertex} previousVertex - */ - enterVertex: ({ currentVertex, previousVertex }) => { - // Tick discovery time. - discoveryTime += 1; - - // Put current vertex to visited set. - visitedSet[currentVertex.getKey()] = new VisitMetadata({ - discoveryTime, - lowDiscoveryTime: discoveryTime, - }); - - if (previousVertex) { - // Update children counter for previous vertex. - visitedSet[previousVertex.getKey()].independentChildrenCount += 1; - } - }, - /** - * @param {GraphVertex} currentVertex - * @param {GraphVertex} previousVertex - */ - leaveVertex: ({ currentVertex, previousVertex }) => { - if (previousVertex === null) { - // Don't do anything for the root vertex if it is already current (not previous one) - return; - } - - // Update the low time with the smallest time of adjacent vertices. - // Get minimum low discovery time from all neighbors. - /** @param {GraphVertex} neighbor */ - visitedSet[currentVertex.getKey()].lowDiscoveryTime = currentVertex.getNeighbors() - .filter((earlyNeighbor) => earlyNeighbor.getKey() !== previousVertex.getKey()) - /** - * @param {number} lowestDiscoveryTime - * @param {GraphVertex} neighbor - */ - .reduce( - (lowestDiscoveryTime, neighbor) => { - const neighborLowTime = visitedSet[neighbor.getKey()].lowDiscoveryTime; - return neighborLowTime < lowestDiscoveryTime ? neighborLowTime : lowestDiscoveryTime; - }, - visitedSet[currentVertex.getKey()].lowDiscoveryTime, - ); - - // Detect whether previous vertex is articulation point or not. - // To do so we need to check two [OR] conditions: - // 1. Is it a root vertex with at least two independent children. - // 2. If its visited time is <= low time of adjacent vertex. - if (previousVertex === startVertex) { - // Check that root vertex has at least two independent children. - if (visitedSet[previousVertex.getKey()].independentChildrenCount >= 2) { - articulationPointsSet[previousVertex.getKey()] = previousVertex; - articulation_vertices.add(previousVertex.getKey()); - } - } else { - // Get current vertex low discovery time. - let currentLowDiscoveryTime = visitedSet[currentVertex.getKey()].lowDiscoveryTime; - - // Compare current vertex low discovery time with parent discovery time. Check if there - // are any short path (back edge) exists. If we can't get to current vertex other then - // via parent then the parent vertex is articulation point for current one. - const parentDiscoveryTime = visitedSet[previousVertex.getKey()].discoveryTime; - if (parentDiscoveryTime <= currentLowDiscoveryTime) { - articulationPointsSet[previousVertex.getKey()] = previousVertex; - articulation_vertices.add(previousVertex.getKey()); - } - } - }, - - allowTraversal: ({ nextVertex }) => !visitedSet[nextVertex.getKey()], - }; - - // Do Depth First Search traversal over submitted graph. - depthFirstSearch(this, startVertex, dfsCallbacks); - - return [...articulation_vertices]; - } - /** * @param {GraphVertex} from * @param {GraphVertex} to * @return {Array[Integer]} paths - */ + **/ acyclicPaths(from, to) { const verticesIndices = this.getVerticesIndices(); const from_index = verticesIndices[from]; const to_index = verticesIndices[to]; + const n_vertices = this.getNumVertices(); let is_visited = new Array(this.v); @@ -1042,6 +1065,101 @@ export default class Graph { return paths; } + #allPathsUtil(acyclic_path, cycle_nodes){ + let intersect_nodes=_.intersection(acyclic_path, cycle_nodes); + let forward_star=this.getAdjacencyList(0); + let reverse_star=this.getAdjacencyList(1); + let inflow_nodes=[]; + let outflow_nodes=[]; + let new_routes=[]; + let only_cycle_nodes=_.difference(cycle_nodes, acyclic_path); + let all_nodes=_.union(cycle_nodes, acyclic_path); + + // Cartesian product of arrays + let f = (a, b) => [].concat(...a.map(a => b.map(b => [].concat(a, b)))); + let cartesian = (a, b, ...c) => b ? cartesian(f(a, b), ...c) : a; + + // Cycles intercept the main path + for(let intersect_node of intersect_nodes){ + if(_.intersection(forward_star[intersect_node], only_cycle_nodes).length!=0){ + outflow_nodes.push(intersect_node); + } + + if(_.intersection(reverse_star[intersect_node], only_cycle_nodes).length!=0){ + inflow_nodes.push(intersect_node); + } + } + + // New routes comes from outflow-inflow cycles + let new_route=[] + for(let combination of cartesian(outflow_nodes, inflow_nodes)){ + let start_node_id=combination[0]; + let finish_node_id=combination[1]; + + let startVertex=this.getVertexByIndex(start_node_id) + let finishVertex=this.getVertexByIndex(finish_node_id) + + for(let out_inflow of this.acyclicPaths(startVertex, finishVertex)){ + // Nodes of outgoing route MUST only belong to cycle_nodes + if(out_inflow.length===_.intersection(out_inflow, cycle_nodes).length) { + let start_node_index = acyclic_path.indexOf(start_node_id); + let finish_node_index = acyclic_path.indexOf(finish_node_id); + + if(_.intersection(acyclic_path, out_inflow).length===2 && + start_node_index >= finish_node_index){ + new_route = [].concat(acyclic_path.slice(0, start_node_index+1), + out_inflow.slice(1, -1), + acyclic_path.slice(finish_node_index)) + + new_routes.push(new_route); + } + } + } + } + + return new_routes + } + + allPaths(from, to) { + let cycles_venn = this.getCyclesVenn(); + let cycle_indices = this.getCycleIndices(); + + let acyclic_paths = this.acyclicPaths(from, to); + let cyclic_paths = []; + let cycles_connections = Object.keys(cycles_venn); + let cycle_nodes_arr=[] + let connected_cycles_indexes=[]; + + // For each acyclic path, it finds if a cyclic connection + // brings new paths + for(let acyclic_path of acyclic_paths){ + for(let cycles_connection of cycles_connections) { + connected_cycles_indexes = _.split(cycles_connection, ','); + + let cycle_nodes=new Set(); + + connected_cycles_indexes.forEach((cycle_index) => { + cycle_index=Number(cycle_index) + + cycle_indices[cycle_index].forEach(cycle_nodes.add, cycle_nodes) + }); + + cycle_nodes_arr=[...cycle_nodes]; + + let cyclic_paths_i=[]; + if(_.intersection(acyclic_path, cycle_nodes_arr).length!=0){ + cyclic_paths_i=this.#allPathsUtil(acyclic_path, cycle_nodes_arr) + + cyclic_paths=cyclic_paths.concat(cyclic_paths_i); + } + } + } + + cyclic_paths=removeArrayDuplicates(cyclic_paths) + + return acyclic_paths.concat(cyclic_paths); + } + /** * @param {GraphVertex} from * @param {GraphVertex} to @@ -1049,7 +1167,7 @@ export default class Graph { */ getVertexCycles() { const n_vertices = this.getNumVertices(); - let cycles = this.cyclicPaths(); + let cycles = this.cyclicCircuits(); const nodes_to_cycles = {}; for (let i = 0; i < n_vertices; i += 1) { @@ -1093,7 +1211,7 @@ export default class Graph { articulation_nodes: this.articulationVertices(), bridges: this.bridges(), is_cyclic, - ...is_cyclic && { all_cycles: this.cyclicPaths() }, + ...is_cyclic && { all_cycles: this.cyclicCircuits() }, is_eulerian: is_eulerian, ...is_eulerian && { eulerian_path: this.getEulerianPath() }, is_connected: this.isConnected(), diff --git a/data-structures/graph/__test__/Graph.test.js b/data-structures/graph/__test__/Graph.test.js index 8e96795c..6bc09f31 100755 --- a/data-structures/graph/__test__/Graph.test.js +++ b/data-structures/graph/__test__/Graph.test.js @@ -141,7 +141,7 @@ describe('Graph', () => { // Add edges graph.addEdges([AB, BC, CD, CE, EB, CF, FB]); - expect(graph.cyclicPaths()).toStrictEqual([[1, 2, 4], [1, 2, 5]]); + expect(graph.cyclicCircuits()).toStrictEqual([[1, 2, 4], [1, 2, 5]]); }); it('Cycles in a finite graph must be finite', () => { @@ -159,7 +159,7 @@ describe('Graph', () => { // Add edges graph.addEdges([AB, BC, CD, CE, EB, CF, FB]); - expect(graph.cyclicPaths()).toStrictEqual([[1, 2, 4], [1, 2, 5]]); + expect(graph.cyclicCircuits()).toStrictEqual([[1, 2, 4], [1, 2, 5]]); }); it('should find Eulerian Circuit in graph', () => { @@ -277,7 +277,7 @@ describe('Graph', () => { const edgeDE = new GraphEdge(vertexD, vertexE); const edgeEF = new GraphEdge(vertexE, vertexF); const edgeFA= new GraphEdge(vertexF, vertexA); - + const graph = new Graph(true); graph.addEdges([edgeAB, edgeBC, edgeCD, @@ -341,7 +341,7 @@ describe('Graph', () => { // Add edges graph.addEdges([AB, BC, CD, CE, EB, CF, FB]); - expect(graph.cyclicPaths()).toStrictEqual([[1, 2, 4], [1, 2, 5]]); + expect(graph.cyclicCircuits()).toStrictEqual([[1, 2, 4], [1, 2, 5]]); }); it('Bridges in graph', () => { @@ -387,7 +387,7 @@ describe('Graph', () => { // Add edges graph.addEdges([AB, BC]); - expect(graph.cyclicPaths()).toEqual([]); + expect(graph.cyclicCircuits()).toEqual([]); }); it('should find edge by vertices in undirected graph', () => { @@ -813,6 +813,29 @@ describe('Graph', () => { expect(graph_.acyclicPaths(A, F)).toEqual([[0, 1, 2, 5]]); }); + it('should return all paths from from_key to to_key', () => { + // A directed graph + const graph_ = new Graph(true); + + // Nodes + const node_labels = ['A', 'B', 'C', 'D', 'E', 'F']; + const [A, B, C, D, E, F] = createVertices(node_labels); + + // Vertices + const edge_vertices = [[A, B], [B, C], [C, D], [C, E], [E, B], [C, F], [F, B]]; + + // Add edges + graph_.addEdges(createEdges(edge_vertices)); + + console.log(graph_.allPaths(A, D)) + + expect(graph_.allPaths(A, D)).toStrictEqual([ + [0, 1, 2, 3], + [0, 1, 2, 4, 1, 2, 3], + [0, 1, 2, 5, 1, 2, 3] + ]); + }); + it('should return cycles of vertices', () => { const graph_ = new Graph(true); diff --git a/server.js b/server.js index f0415abc..8493c625 100755 --- a/server.js +++ b/server.js @@ -4,10 +4,7 @@ import { createRequire } from 'module'; import fs from 'fs'; import { - startAndFinishNodes, - nodeToLane, parseBlueprintToGraph, - fromStartToFinishAcyclicPaths, fromStartToFinishCombsAcyclicPaths, } from './utils/workflow/parsers.js'; @@ -34,11 +31,11 @@ app.get('/', (req, res) => { const blueprints_fnames = fs.readdirSync(bps_root); const READ_ALL_BPS = false; + const TEST_USE_CASE = true; const blueprint_fname = 'precious.json'; - - const graphs = []; + const descriptions = []; - + if (READ_ALL_BPS) { for (let i = 0; i < blueprints_fnames.length; i++) { const fname = bps_root + blueprints_fnames[i]; @@ -56,7 +53,8 @@ app.get('/', (req, res) => { const graph = parseBlueprintToGraph(blueprint_i); const route_describe = fromStartToFinishCombsAcyclicPaths(blueprint_i); - res.send(graph.describe()); + res.send(graph.allPaths()); } + }); // [END app] diff --git a/utils/arrays/arrays.js b/utils/arrays/arrays.js index 6c690d18..b70e41d0 100755 --- a/utils/arrays/arrays.js +++ b/utils/arrays/arrays.js @@ -43,6 +43,40 @@ export const isCyclicEqual = (control_, treatment_) => { return false; }; +export const arraysEqual = (a, b) => { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + // If you don't care about the order of the elements inside + // the array, you should sort both arrays here. + // Please note that calling sort on an array will modify that array. + // you might want to clone your array first. + + for (var i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; +} + +export const removeArrayDuplicates = (list) => { + let unique = []; + + list.forEach((item) => { + let has_item=false + + unique.forEach((unique_item) => { + has_item=has_item || arraysEqual(item, unique_item) + }) + + if(!has_item){ + unique.push(item); + } + }); + + return unique +} + export const getUniques = (vec) => Array.from(new Set(vec)); export const extendedVenn = (sets) => {