CS61B 课程笔记(DISC 11 Graphs)

1. 图的表示

1.1 邻接矩阵和邻接表

对于给定的图,可以用邻接矩阵和邻接表来表示。

邻接矩阵:

1
2
3
4
5
6
7
8
     A B C D E F G
A [ 0 1 0 1 0 0 0 ]
B [ 0 0 1 0 0 0 0 ]
C [ 0 0 0 0 0 1 0 ]
D [ 0 1 0 0 1 1 0 ]
E [ 0 0 0 0 0 1 0 ]
F [ 0 0 0 0 0 0 0 ]
G [ 0 0 0 0 0 1 0 ]

邻接表:

1
2
3
4
5
6
7
A: {B, D}
B: {C}
C: {F}
D: {B, E, F}
E: {F}
F: {}
G: {F}

无向图的变化:

  • 对于无向图,邻接矩阵和邻接表的每个边都要双向记录。例如,若有边 (A, B),则在矩阵中 A 和 B 的位置均为 1。

2. 图的遍历

1.2 深度优先搜索(DFS)和广度优先搜索(BFS)

  • DFS 前序遍历: ABCFDE
  • DFS 后序遍历: FCBEDA
  • BFS 遍历: ABDCEF

3. 拓扑排序

1.3 有效拓扑排序

根据提示,可以使用逆后序遍历得到一个有效的拓扑排序。例如:

1
G - A - D - E - B - C - F
  • 在这个排序中,G可以放在任何地方,但不能放在F之后,因为F有一个入边。

4. 图算法设计

2.1 判断图是否为二分图

算法:

  • 使用DFS或BFS进行遍历,标记起始顶点为U,其子节点为V,然后继续标记。
  • 如果在遍历中发现一个节点应该被标记为U但已标记为V,说明图不是二分图。
  • 对于不连通的图,需对每个连通分量重复这一过程。

时间复杂度: \(O(E + V)\)

2.2 查找有向图中的最短回路

算法:

  • 对于源顶点s,运行BFS以找到每个顶点的最短路径。
  • 遍历所有顶点,如果某个顶点v有边返回到s,则回路长度为distTo(v) + 1
  • 对于整个图,需对每个顶点调用此子程序。

时间复杂度: \(O(EV)\)空间复杂度\(O(E)\)

2.3 DFS实现中的错误示例

错误实现:

  • 在遍历邻接点之前就标记顶点,可能导致不按照DFS顺序遍历。例如,访问顺序可能为 A - B - C - D,而不是深度优先的顺序。
  • 正确的做法是先访问节点再标记。

Java实现代码

以下是DFS、BFS和拓扑排序的Java代码示例:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import java.util.*;

class Graph {
private Map<String, List<String>> adjList;

public Graph() {
this.adjList = new HashMap<>();
}

public void addEdge(String source, String destination) {
this.adjList.putIfAbsent(source, new ArrayList<>());
this.adjList.putIfAbsent(destination, new ArrayList<>());
this.adjList.get(source).add(destination);
}

// 深度优先搜索
public void dfs(String start) {
Set<String> visited = new HashSet<>();
dfsUtil(start, visited);
}

private void dfsUtil(String vertex, Set<String> visited) {
visited.add(vertex);
System.out.print(vertex + " ");
for (String neighbor : adjList.get(vertex)) {
if (!visited.contains(neighbor)) {
dfsUtil(neighbor, visited);
}
}
}

// 广度优先搜索
public void bfs(String start) {
Set<String> visited = new HashSet<>();
Queue<String> queue = new LinkedList<>();
visited.add(start);
queue.add(start);
while (!queue.isEmpty()) {
String vertex = queue.poll();
System.out.print(vertex + " ");
for (String neighbor : adjList.get(vertex)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.add(neighbor);
}
}
}
}

// 拓扑排序
public void topologicalSort() {
Map<String, Integer> inDegree = new HashMap<>();
for (String vertex : adjList.keySet()) {
inDegree.put(vertex, 0);
}
for (List<String> neighbors : adjList.values()) {
for (String neighbor : neighbors) {
inDegree.put(neighbor, inDegree.get(neighbor) + 1);
}
}

Queue<String> queue = new LinkedList<>();
for (Map.Entry<String, Integer> entry : inDegree.entrySet()) {
if (entry.getValue() == 0) {
queue.add(entry.getKey());
}
}

while (!queue.isEmpty()) {
String vertex = queue.poll();
System.out.print(vertex + " ");
for (String neighbor : adjList.get(vertex)) {
inDegree.put(neighbor, inDegree.get(neighbor) - 1);
if (inDegree.get(neighbor) == 0) {
queue.add(neighbor);
}
}
}
}

public static void main(String[] args) {
Graph graph = new Graph();
graph.addEdge("A", "B");
graph.addEdge("A", "D");
graph.addEdge("B", "C");
graph.addEdge("C", "F");
graph.addEdge("D", "B");
graph.addEdge("D", "E");
graph.addEdge("D", "F");
graph.addEdge("E", "F");
graph.addEdge("G", "F");

System.out.println("DFS:");
graph.dfs("A");

System.out.println("\nBFS:");
graph.bfs("A");

System.out.println("\nTopological Sort:");
graph.topologicalSort();
}
}

代码解释

  1. 图的表示: 使用Map<String, List<String>>来表示邻接表。
  2. 深度优先搜索: 使用递归的方式进行DFS,先访问节点再标记。
  3. 广度优先搜索: 使用队列来实现BFS,确保按层次访问。
  4. 拓扑排序: 计算每个节点的入度,使用队列进行拓扑排序。