什么是生成树?
连通无向图中的所有顶点且任意两个顶点间只有一条通路的子图。生成树中边的数量 = 顶点数 - 1
。如下图均为生成树。
什么是最小生成树?
最小生成树是指所有生成树中边的权重和最小的生成树。
最小生成树算法
最小生成树算法主要有两个:kruskal(克鲁斯卡尔)算法和prim(普里姆)算法。下面就分别介绍下这两类算法。
1. kruskal 算法
其主要步骤如下:
- 首先获取所有的边,对这些边进行从小到大的排序
- 然后不断往图里添加边,并且避免造成环
- 直到所有点都能互相访问后停止算法
伪代码如下:
function kruskal(graph) {
// 对图里的边进行排序
const sortedEdges = graph.edges.sort()
// 去掉图里的边
graph.removeAllEdges()
for (let i = 0; i < sortedEdges.length; i++) {
// 如果添加的边未造成环,那么就进行添加
if (!graph.hasCycle()) {
graph.addEdge(sortedEdges[i])
}
}
return graph
}
算法要点在于怎么判断是否存在环。
在此之前,先说明三个定义:
- 无向图 G 的一个极大连通子图称为 G 的一个连通分量(或连通分支)。连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量。连通分量与连通分量之间没有任何边相连。
- 在无向图中,如果从顶点到顶点有路径,则称和连通。如果图中任意两个顶点之间都连通,则称该图为连通图,否则,将其中较大的连通子图称为连通分量。
- 在有向图中,如果对于每一对顶点和,从到和从到都有路径,则称该图为强连通图;否则,将其中的极大连通子图称为强连通分量。
kruskal 就可以这样理解:初始时,把图中的个顶点看成是独立的个连通分量,从树的角度看,也是个根节点。选边的标准是这样的:若边上的两个顶点从属于两个不同的连通分量,则此边可取,否则考察下一条权值最小的边。
那么如何判断两个顶点是否属于同一个连通分量呢?这个可以参照并查集的做法解决。它的思路是:如果两个顶点的根节点是一样的,则属于同一个连通分量。这也同样暗示着:在加入新边时,要更新父节点。
先定义边的数据结构:
class Edge(object):
def __init__(self, start, end, weight):
"""
定义边,顶点采用 0至顶点个数-1编号
:param start: 顶点start
:param end: 顶点end
:param weight: 顶点start 和顶点end 之间边的权重
"""
self.start = start
self.end = end
self.weight = weight
下面是 Kruskal 算法实现:
class Kruskal(object):
def __init__(self, edges, node_count=0):
"""
Kruskal 算法
:param edges: list:[Edge]
"""
self.edges = edges
self.root = list(range(node_count))
self.path_length = 0
def is_same_root(self, i, j):
"""
判断 i 和 j 的根节点是否相同
:param i:
:param j:
:return: bool
"""
while self.root[i] != i:
i = self.root[i]
while self.root[j] != j:
j = self.root[j]
return i == j
def update_root(self, i, j):
"""
将 j 的根节点更新为 i 的根节点
"""
if i > j:
i, j = j, i
while self.root[i] != i:
i = self.root[i]
self.root[j] = i
def kruskal(self):
# 先排序
edges = sorted(self.edges, key=lambda edge: edge.weight)
for edge in edges:
i = edge.start
j = edge.end
if not self.is_same_root(i, j):
print("%d 和 %d 是相连的" % (i, j))
self.update_root(i, j)
self.path_length += edge.weight
return self.path_length
其中判断是否是相同根节点也可以利用查并集的方式:
def find(self, i):
if self.root[i] != i:
self.root[i] = self.find(self.root[i])
return self.root[i]
最后 Kruskal
算法可写为
def kruskal(self):
edges = sorted(self.edges, key=lambda edge: edge.weight)
for edge in edges:
i = edge.start
j = edge.end
w = edge.weight
root_i, root_j = self.find(i), self.find(j)
if root_i != root_j:
print("%d 和 %d 是相连的" % (i, j))
self.root[root_j] = root_i
self.path_length += w
return self.path_length
2. prim 算法
其主要步骤如下:
- 从一个节点(随机选一个)开始,去找与这个节点相邻最短的边
- 将找到的边添加到这个节点上,形成一个组件
- 再从这个组件开始去找相邻权重最小的边,再添加到这个组件上,不断重复直到所有节点都能被访问
伪代码如下
function prim(graph) {
const visited = []
// 从一个节点开始
const sourceComponent = graph.randomVertex()
// 如果还有节点不能被访问就继续找
while (visited.length < graph.vertices.length) {
// 将相邻权重最小的边作为组件的一部分,且不构成环
const smallestEdge = sourceComponent.findSmallestEdge()
sourceComponent.addEdge(smallestEdge)
visited.push(smallestEdge.toVertex)
}
}
下面是 prim 算法实现:
class Prim(object):
def __init__(self, graph):
"""
:param graph: 邻接表形式的图,具体值表示边的权重
"""
self.graph = graph
def prim(self, start=0):
length = len(self.graph)
distances = [float('inf')] * length
visited = [False] * length
visited[start] = True
index = start
path_length = 0
# 与 start 相连的点 进行初始化
for i in range(length):
distances[i] = self.graph[start][i]
for i in range(length):
if i == start:
continue
minor = float('inf')
for j in range(length):
# 未访问的节点中 距离最近的节点
if not visited[j] and distances[j] < minor:
minor = distances[j]
index = j
visited[index] = True
# print(i, index, minor)
path_length += minor
# 更新最近的距离
for j in range(length):
if not visited[j] and distances[j] > self.graph[index][j]:
distances[j] = self.graph[index][j]
return path_length
其中寻找距离最近的节点也可以利用堆进行优化:
def prim(self, start=0):
length = len(self.graph)
distances = [float('inf')] * length
visited = [False] * length
path_length = 0
# 元素为 (距离, 下标)
heap = []
heapq.heappush(heap, (0, start))
while heap:
dist, index = heapq.heappop(heap)
if visited[index]:
continue
visited[index] = True
path_length += dist
# 将与index相连的节点加入
for j in range(length):
w = self.graph[index][j]
if not visited[j] and w < distances[j]:
distances[j] = w
heapq.heappush(heap, (w, j))
return path_length
3. leetcode 题目
3.1 1584. 连接所有点的最小费用
给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。
连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,
其中 |val| 表示 val 的绝对值。 请你返回将所有点连接的最小总费用。
只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
示例 1:
输入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
输出:20
示例 2:
输入:points = [[3,12],[-2,5],[-4,1]]
输出:18
示例 3:
输入:points = [[0,0],[1,1],[1,0],[-1,1]]
输出:4
示例 4:
输入:points = [[-1000000,-1000000],[1000000,1000000]]
输出:4000000
示例 5:
输入:points = [[0,0]]
输出:0
提示:
1 <= points.length <= 1000
-106 <= xi, yi <= 106
所有点 (xi, yi) 两两不同。
Related Topics 并查集
题目要求连接所有点,有且仅有一条路径,即最终结果是生成树。因为要求的是最小的总费用,便是最小生成树。我们可以采用前面总结的最小生成树算法来实现。
首先定义曼哈顿距离的计算函数:
def manhattanDistance(points, index1, index2):
point1 = points[index1]
point2 = points[index2]
return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1])
若采用 Kruskal 算法,我们需要将题目给定的一系列点转换成类 Kruskal
初始化需要的格式,并进行初始化,最后求的结果:
if __name__ == '__main__':
points = [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]]
n = len(points)
edges = []
for i in range(n):
for j in range(i+1, n):
edges.append(Edge(i, j, manhattanDistance(points, i, j)))
s = Kruskal(edges, node_count=n)
print(s.kruskal())
若采用 Prim 算法,则需要将题目给定的一系列点转换成邻接表的形式,最终求的结果:
if __name__ == '__main__':
points = [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]]
n = len(points)
graph = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(i+1, n):
weight = manhattanDistance(points, i, j)
graph[i][j] = weight
graph[j][i] = weight
s = Prim(graph)
print(s.prim(0))
参考
- https://www.jianshu.com/p/c046fcaa190c
- https://www.cnblogs.com/dengfaheng/p/9245794.html