Skip to content

Commit 9615377

Browse files
committed
补充「图的最小生成树」相关内容
1 parent fe267b2 commit 9615377

File tree

1 file changed

+137
-7
lines changed

1 file changed

+137
-7
lines changed

Contents/08.Graph/03.Graph-Spanning-Tree/01.Graph-Minimum-Spanning-Tree.md

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
在了解「最小生成树」之前,我们需要要先理解 「生成树」的概念。
44

5-
> **图的生成树(Spanning Tree)**:如果无向连通图 G 的一个子图是一棵包含图 G 所有顶点的树,则称该子图为 G 的生成树。生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。
5+
> **生成树(Spanning Tree)**:如果无向连通图 G 的一个子图是一棵包含图 G 所有顶点的树,则称该子图为 G 的生成树。生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。
66
77
换句话说,生成树是原图 G 的一个子图,它包含了原图 G 的所有顶点,并且通过选择图中一部分边连接这些顶点,使得子图中没有环。
88

@@ -51,12 +51,73 @@
5151
### 2.3 Prim 算法的实现代码
5252

5353
```python
54-
54+
class Solution:
55+
# graph 为图的邻接矩阵,start 为起始顶点
56+
def Prim(self, graph, start):
57+
size = len(graph)
58+
vis = set()
59+
dist = [float('inf') for _ in range(size)]
60+
61+
ans = 0 # 最小生成树的边权和
62+
dist[start] = 0 # 初始化起始顶点到起始顶点的边权值为 0
63+
64+
for i in range(1, size): # 初始化起始顶点到其他顶点的边权值
65+
dist[i] = graph[start][i]
66+
vis.add(start) # 将 start 顶点标记为已访问
67+
68+
for _ in range(size - 1):
69+
min_dis = float('inf')
70+
min_dis_pos = -1
71+
for i in range(size):
72+
if i not in vis and dist[i] < min_dis:
73+
min_dis = dist[i]
74+
min_dis_pos = i
75+
if min_dis_pos == -1: # 没有顶点可以加入 MST,图 G 不连通
76+
return -1
77+
ans += min_dis # 将顶点加入 MST,并将边权值加入到答案中
78+
vis.add(min_dis_pos)
79+
for i in range(size):
80+
if i not in vis and dist[i] > graph[min_dis_pos][i]:
81+
dist[i] = graph[min_dis_pos][i]
82+
return ans
83+
84+
points = [[0,0]]
85+
graph = dict()
86+
size = len(points)
87+
for i in range(size):
88+
x1, y1 = points[i]
89+
for j in range(size):
90+
x2, y2 = points[j]
91+
dist = abs(x2 - x1) + abs(y2 - y1)
92+
if i not in graph:
93+
graph[i] = dict()
94+
if j not in graph:
95+
graph[j] = dict()
96+
graph[i][j] = dist
97+
graph[j][i] = dist
98+
99+
100+
print(Solution().Prim(graph))
55101
```
56102

57-
### 2.3 Prim 算法
103+
### 2.4 Prim 算法复杂度分析
104+
105+
Prim 算法的时间复杂度主要取决于以下几个因素:
106+
107+
1. **初始化阶段**
108+
- 初始化距离数组和访问数组的时间复杂度为 $O(V)$,其中 $V$ 是图中的顶点数。
58109

59-
## 03. Kruskal 算法
110+
2. **主循环阶段**
111+
- 外层循环需要执行 $V-1$ 次,用于选择 $V-1$ 条边。
112+
- 每次循环中需要:
113+
- 找到未访问顶点中距离最小的顶点,时间复杂度为 $O(V)$。
114+
- 更新相邻顶点的距离,时间复杂度为 $O(V)$。
115+
116+
因此,Prim 算法的总体复杂度为:
117+
- 时间复杂度:$O(V^2)$,其中 $V$ 是图中的顶点数。
118+
- 空间复杂度:$O(V)$,主要用于存储距离数组和访问数组。
119+
120+
## 3. Kruskal 算法
60121

61122
### 3.1 Kruskal 算法的算法思想
62123

@@ -70,12 +131,81 @@
70131
2. 将每个顶点看做是一个单独集合,即初始时每个顶点自成一个集合。
71132
3. 按照排好序的边顺序,按照权重从小到大,依次遍历每一条边。
72133
4. 对于每条边,检查其连接的两个顶点所属的集合:
73-
1. 如果两个顶点属于同一个集合,则跳过这条边,以免形成环路。
74-
2. 如果两个顶点不属于同一个集合,则将这条边加入到最小生成树中,同时合并这两个顶点所属的集合。
134+
1. 如果两个顶点属于同一个集合,则跳过这条边,以免形成环路。
135+
2. 如果两个顶点不属于同一个集合,则将这条边加入到最小生成树中,同时合并这两个顶点所属的集合。
75136
5. 重复第 $3 \sim 4$ 步,直到最小生成树中的变数等于所有节点数减 $1$ 为止。
76137

77138
### 3.3 Kruskal 算法的实现代码
78139

79140
```python
141+
class UnionFind:
142+
143+
def __init__(self, n):
144+
self.parent = [i for i in range(n)]
145+
self.count = n
146+
147+
def find(self, x):
148+
while x != self.parent[x]:
149+
self.parent[x] = self.parent[self.parent[x]]
150+
x = self.parent[x]
151+
return x
152+
153+
def union(self, x, y):
154+
root_x = self.find(x)
155+
root_y = self.find(y)
156+
if root_x == root_y:
157+
return
158+
159+
self.parent[root_x] = root_y
160+
self.count -= 1
161+
162+
def is_connected(self, x, y):
163+
return self.find(x) == self.find(y)
164+
165+
166+
class Solution:
167+
def Kruskal(self, edges, size):
168+
union_find = UnionFind(size)
169+
170+
edges.sort(key=lambda x: x[2])
171+
172+
res, cnt = 0, 1
173+
for x, y, dist in edges:
174+
if union_find.is_connected(x, y):
175+
continue
176+
ans += dist
177+
cnt += 1
178+
union_find.union(x, y)
179+
if cnt == size - 1:
180+
return ans
181+
return ans
182+
183+
def minCostConnectPoints(self, points: List[List[int]]) -> int:
184+
size = len(points)
185+
edges = []
186+
for i in range(size):
187+
xi, yi = points[i]
188+
for j in range(i + 1, size):
189+
xj, yj = points[j]
190+
dist = abs(xi - xj) + abs(yi - yj)
191+
edges.append([i, j, dist])
192+
193+
ans = Solution().Kruskal(edges, size)
194+
return ans
195+
```
196+
197+
### 3.4 Kruskal 算法复杂度分析
198+
199+
Kruskal 算法的时间复杂度主要取决于以下几个因素:
200+
201+
1. **边的排序**:对 $E$ 条边进行排序的时间复杂度为 $O(E \log E)$。
202+
203+
2. **并查集操作**
204+
- 查找操作(find)的时间复杂度为 $O(\alpha(n))$,其中 $\alpha(n)$ 是阿克曼函数的反函数,增长极其缓慢,可以近似认为是常数时间。
205+
- 合并操作(union)的时间复杂度也是 $O(\alpha(n))$。
206+
207+
3. **遍历边的过程**:需要遍历所有边,时间复杂度为 $O(E)$。
80208

81-
```
209+
因此,Kruskal 算法的总体时间复杂度为:
210+
- 时间复杂度:$O(E \log E)$,其中 $E$ 是图中的边数。
211+
- 空间复杂度:$O(V)$,其中 $V$ 是图中的顶点数,主要用于存储并查集数据结构。

0 commit comments

Comments
 (0)