Skip to content

Commit 1e4ea37

Browse files
author
punit1009
committed
fix(graph): return ordered cycle path and add early exit in detectUndirectedCycle (#2057)
1 parent cbda7c4 commit 1e4ea37

File tree

2 files changed

+59
-33
lines changed

2 files changed

+59
-33
lines changed

src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,40 @@ describe('detectUndirectedCycle', () => {
2727
.addEdge(edgeBC)
2828
.addEdge(edgeCD);
2929

30+
// no cycle yet
3031
expect(detectUndirectedCycle(graph)).toBeNull();
3132

33+
// add the final edge that closes cycle B-C-D-E-B
3234
graph.addEdge(edgeDE);
3335

34-
expect(detectUndirectedCycle(graph)).toEqual({
35-
B: vertexC,
36-
C: vertexD,
37-
D: vertexE,
38-
E: vertexB,
36+
const cycle = detectUndirectedCycle(graph);
37+
38+
// should return ordered array of vertices representing cycle (first === last)
39+
expect(Array.isArray(cycle)).toBe(true);
40+
expect(cycle.length).toBeGreaterThanOrEqual(3);
41+
expect(cycle[0].getKey()).toBe(cycle[cycle.length - 1].getKey());
42+
43+
// Extract keys for easier assertions
44+
const keys = cycle.map((v) => v.getKey());
45+
46+
// The expected cycle is B -> C -> D -> E -> B (but the returned cycle may be a rotation),
47+
// so accept any rotation of that sequence.
48+
const allowedRotations = [
49+
['B', 'C', 'D', 'E', 'B'],
50+
['C', 'D', 'E', 'B', 'C'],
51+
['D', 'E', 'B', 'C', 'D'],
52+
['E', 'B', 'C', 'D', 'E'],
53+
];
54+
55+
// Check that keys match one of the allowed rotations
56+
const matchesRotation = allowedRotations.some((rot) => {
57+
if (rot.length !== keys.length) return false;
58+
for (let i = 0; i < rot.length; i += 1) {
59+
if (rot[i] !== keys[i]) return false;
60+
}
61+
return true;
3962
});
63+
64+
expect(matchesRotation).toBe(true);
4065
});
4166
});

src/algorithms/graph/detect-cycle/detectUndirectedCycle.js

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,57 @@ import depthFirstSearch from '../depth-first-search/depthFirstSearch';
44
* Detect cycle in undirected graph using Depth First Search.
55
*
66
* @param {Graph} graph
7+
* @returns {Vertex[] | null} ordered array of vertices forming the cycle (first === last), or null
78
*/
89
export default function detectUndirectedCycle(graph) {
9-
let cycle = null;
10+
let cycle = null; // will hold ordered array once found
1011

11-
// List of vertices that we have visited.
12-
const visitedVertices = {};
12+
const visitedVertices = {}; // visited vertices
13+
const parents = {}; // parent for every visited vertex
1314

14-
// List of parents vertices for every visited vertex.
15-
const parents = {};
16-
17-
// Callbacks for DFS traversing.
1815
const callbacks = {
1916
allowTraversal: ({ currentVertex, nextVertex }) => {
20-
// Don't allow further traversal in case if cycle has been detected.
21-
if (cycle) {
22-
return false;
23-
}
17+
if (cycle) return false; // stop traversal once cycle is found
2418

25-
// Don't allow traversal from child back to its parent.
2619
const currentVertexParent = parents[currentVertex.getKey()];
27-
const currentVertexParentKey = currentVertexParent ? currentVertexParent.getKey() : null;
20+
const currentVertexParentKey = currentVertexParent
21+
? currentVertexParent.getKey()
22+
: null;
2823

2924
return currentVertexParentKey !== nextVertex.getKey();
3025
},
26+
3127
enterVertex: ({ currentVertex, previousVertex }) => {
3228
if (visitedVertices[currentVertex.getKey()]) {
33-
// Compile cycle path based on parents of previous vertices.
34-
cycle = {};
35-
36-
let currentCycleVertex = currentVertex;
37-
let previousCycleVertex = previousVertex;
38-
39-
while (previousCycleVertex.getKey() !== currentVertex.getKey()) {
40-
cycle[currentCycleVertex.getKey()] = previousCycleVertex;
41-
currentCycleVertex = previousCycleVertex;
42-
previousCycleVertex = parents[previousCycleVertex.getKey()];
29+
// Build ordered cycle array
30+
const startKey = currentVertex.getKey();
31+
const cycleArr = [currentVertex];
32+
33+
let walker = previousVertex;
34+
while (walker && walker.getKey() !== startKey) {
35+
cycleArr.push(walker);
36+
walker = parents[walker.getKey()];
4337
}
4438

45-
cycle[currentCycleVertex.getKey()] = previousCycleVertex;
39+
cycleArr.push(currentVertex); // close the cycle
40+
cycle = cycleArr;
4641
} else {
47-
// Add next vertex to visited set.
4842
visitedVertices[currentVertex.getKey()] = currentVertex;
4943
parents[currentVertex.getKey()] = previousVertex;
5044
}
5145
},
5246
};
5347

54-
// Start DFS traversing.
55-
const startVertex = graph.getAllVertices()[0];
56-
depthFirstSearch(graph, startVertex, callbacks);
48+
const allVertices = graph.getAllVertices();
49+
for (let i = 0; i < allVertices.length; i += 1) {
50+
const startVertex = allVertices[i];
51+
52+
if (!visitedVertices[startVertex.getKey()]) {
53+
depthFirstSearch(graph, startVertex, callbacks);
54+
55+
if (cycle) break; // early exit once cycle is found
56+
}
57+
}
5758

5859
return cycle;
5960
}

0 commit comments

Comments
 (0)