Graph Cycle Detection
Graph is a data structure that contains a collection of nodes (vertices) connected by edges, used to represent relationships and connections between objects.
Today I will talk about one of the classic problem about the Graph: how to detect the cycle in graph.
Let’s use a leetcode question 207. Course Schedule as an example to see how to solve such a problem.
207. Course Schedule
There are a total of
numCoursescourses you have to take, labeled from0tonumCourses - 1. You are given an arrayprerequisiteswhereprerequisites[i] = [ai, bi]indicates that you must take coursebifirst if you want to take courseai.For example, the pair
[0, 1], indicates that to take course0you have to first take course1. Returntrueif you can finish all courses. Otherwise, returnfalse.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func canFinish(_ numCourses: Int, _ prerequisites: [[Int]]) -> Bool {
var graph: [Int: [Int]] = [:]
for prerequisite in prerequisites {
let a = prerequisite[0]
let b = prerequisite[1]
graph[b, default: []].append(a)
}
// 0: unvisited, 1: visiting, 2: visited
var states = [Int](repeating: 0, count: numCourses)
// Return whether can finish for course `node`
func dfs(_ node: Int) -> Bool {
if states[node] == 1 {
return false
}
if states[node] == 2 {
return true
}
states[node] = 1
let neighbors = graph[node] ?? []
for neighbor in neighbors {
if !dfs(neighbor) {
return false
}
}
states[node] = 2
return true
}
for course in 0..<numCourses {
if !dfs(course) {
return false
}
}
return true
}
- In line 2, we created a dictionary (hashmap)
graphto represent the data. - In line 3-7, we iterated over the
prerequisitesto build the graph. - In line 10, we defined a
statesarray to store the node’s state.- 0 for unvisited
- 1 for visiting
- 2 for visited
- In line 13-33, we used a nested function called
dfs(dfs stands for “depth-first search”) to check whether we can finish the courses starting from a specific coursenode.- In line 14-16: If the course
nodeis visiting, we can returnfalseimmediately due to cycle detected. - In line 18-20: If the course
nodeis visited, it means no cycle for this coursenode. - In line 22: We marked the current course
nodeas visiting. - In line 24-29: We recursively visited all the courses depend on current course
node. - In line 31: We marked the current course
nodeas visited since we can finish courses starting from it.
- In line 14-16: If the course
- In line 35-39: We checked each course. If any
dfscall returnsfalse(cycle detected), returnfalse. Otherwise, returntrue.
Let’s write some tests to verify the solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let tests = [
(3, [[1, 0], [2, 1]]),
(3, [[1, 0], [2, 1], [0, 2]]),
]
for test in tests {
let numCourses = test.0
let prerequisites = test.1
let result = canFinish(numCourses, prerequisites)
print("canFinish(\(numCourses), \(prerequisites)) == \(result)")
}
// 0 -> 1 -> 2 => true
// canFinish(3, [[1, 0], [2, 1]]) == true
// 0 -> 1 -> 2 -> 0 => false
// canFinish(3, [[1, 0], [2, 1], [0, 2]]) == false
