|
2 | 2 |
|
3 | 3 | 在了解「最小生成树」之前,我们需要要先理解 「生成树」的概念。
|
4 | 4 |
|
5 |
| -> **图的生成树(Spanning Tree)**:如果无向连通图 G 的一个子图是一棵包含图 G 所有顶点的树,则称该子图为 G 的生成树。生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。 |
| 5 | +> **生成树(Spanning Tree)**:如果无向连通图 G 的一个子图是一棵包含图 G 所有顶点的树,则称该子图为 G 的生成树。生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。 |
6 | 6 |
|
7 | 7 | 换句话说,生成树是原图 G 的一个子图,它包含了原图 G 的所有顶点,并且通过选择图中一部分边连接这些顶点,使得子图中没有环。
|
8 | 8 |
|
|
51 | 51 | ### 2.3 Prim 算法的实现代码
|
52 | 52 |
|
53 | 53 | ```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)) |
55 | 101 | ```
|
56 | 102 |
|
57 |
| -### 2.3 Prim 算法 |
| 103 | +### 2.4 Prim 算法复杂度分析 |
| 104 | + |
| 105 | +Prim 算法的时间复杂度主要取决于以下几个因素: |
| 106 | + |
| 107 | +1. **初始化阶段**: |
| 108 | + - 初始化距离数组和访问数组的时间复杂度为 $O(V)$,其中 $V$ 是图中的顶点数。 |
58 | 109 |
|
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 算法 |
60 | 121 |
|
61 | 122 | ### 3.1 Kruskal 算法的算法思想
|
62 | 123 |
|
|
70 | 131 | 2. 将每个顶点看做是一个单独集合,即初始时每个顶点自成一个集合。
|
71 | 132 | 3. 按照排好序的边顺序,按照权重从小到大,依次遍历每一条边。
|
72 | 133 | 4. 对于每条边,检查其连接的两个顶点所属的集合:
|
73 |
| - 1. 如果两个顶点属于同一个集合,则跳过这条边,以免形成环路。 |
74 |
| - 2. 如果两个顶点不属于同一个集合,则将这条边加入到最小生成树中,同时合并这两个顶点所属的集合。 |
| 134 | + 1. 如果两个顶点属于同一个集合,则跳过这条边,以免形成环路。 |
| 135 | + 2. 如果两个顶点不属于同一个集合,则将这条边加入到最小生成树中,同时合并这两个顶点所属的集合。 |
75 | 136 | 5. 重复第 $3 \sim 4$ 步,直到最小生成树中的变数等于所有节点数减 $1$ 为止。
|
76 | 137 |
|
77 | 138 | ### 3.3 Kruskal 算法的实现代码
|
78 | 139 |
|
79 | 140 | ```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)$。 |
80 | 208 |
|
81 |
| -``` |
| 209 | +因此,Kruskal 算法的总体时间复杂度为: |
| 210 | +- 时间复杂度:$O(E \log E)$,其中 $E$ 是图中的边数。 |
| 211 | +- 空间复杂度:$O(V)$,其中 $V$ 是图中的顶点数,主要用于存储并查集数据结构。 |
0 commit comments