1. 应用场景
涉及到网络中所有节点的最优路径问题,可以使用最小生成树求解。例如:交通网、电力网、通信网等等
应用场景:例如要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树
2. 基本概念
先介绍生成树以及最小生成树的概念
-
生成树:是针对连通图来说的。
- 连通图的生成树必须包含两个条件:
- 包含连通图中所有的顶点
- 任意两顶点之间有且仅有一条通路(即不能成环)
- 连通图的生成树必须包含两个条件:
a)为一张连通图,b)为对应的生成树
- 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
3. 常用求解算法
有两种常用的求解最小生成树算法,分别是Kruskal算法和Prim算法。
本次只介绍Kruskal算法,下次介绍Prim算法。
3.1 Kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
- 把图中的所有的边按权值从小到大排序,将图中的n个顶点看成n颗树。
- 按权值从小到大选择边,所选的边的两个顶点应当属于不同的树。
- 重复2,直到所有顶点都在一颗树上或者已选 n-1条边。
以下图为例,说明算法步骤:
-
step1:
先将图中所有边取出,按权重从小到大放到一个列表中
Edge Weight 6 - 7 1 2 - 8 2 5 - 6 2 0 - 1 4 2 - 5 4 6 - 8 6 2 - 3 7 7 - 8 7 0 - 7 8 1 - 2 8 3 - 4 9 4 - 5 10 1 - 7 11 3 - 5 14 -
step2:
按照权值从小到大依次取边。如果未成环,继续;如果成环,将此边丢弃。
-
cycle1:
选择代价最小的边:6 - 7
是否成环:否
合并顶点6和7,此时已选边的数量为:1
-
cycle2:
选择代价最小的边:2 - 8
是否成环:否
合并顶点5和6,此时已选边的数量为:2
-
cycle3:
选择代价最小的边:5 - 6
是否成环:否
合并顶点5和6,此时已选边的数量为:3
-
cycle4:
选择代价最小的边:0 - 1
是否成环:否
合并顶点0和1,此时已选边的数量为:4
-
cycle5:
选择代价最小的边:2 - 5
是否成环:否
合并顶点2和5,此时已选边的数量为:5
-
cycle6:
选择代价最小的边:6 - 8
是否成环:是
舍弃边6-8,此时已选边的数量为:5
-
cycle7:
选择代价最小的边:2 - 3
是否成环:否
合并顶点2和3,此时已选边的数量为:6
-
cycle8:
选择代价最小的边:7 - 8
是否成环:是
舍弃边7-8,此时已选边的数量为:6
-
cycle9:
选择代价最小的边:0 - 7
是否成环:否
合并顶点0和7,此时已选边的数量为:7
-
cycle10:
选择代价最小的边:1 - 2
是否成环:是
舍弃边1-2,此时已选边的数量为:7
-
cycle11:
选择代价最小的边:3 - 4
是否成环:否
合并顶点3和4,此时已选边的数量:8
-
-
step3:
此时,所有顶点已在树上(已选择n-1条边),算法结束。
4. 算法实现
4.1 并查集找环
使用并查集判断是否存在环。关于并查集,可以看b站视频:link,讲的很清楚
import copy
class Node(object):
def __init__(self, data, weight=1, nnext=None):
self.data = data
self.weight = weight
self.next = nnext
class Edge(object):
def __init__(self, ver1, ver2, w=1):
self.vertex1 = ver1
self.vertex2 = ver2
self.weight = w
class AdjTable(object):
def __init__(self):
self.node_list = []
self.edge_list = []
self.parent = []
def add_vertex(self, node_list):
self.node_list.extend(node_list)
self.parent = [-1] * len(self.node_list)
# 获取顶点,但不获取连接,用于复制一份只有顶点的图
def get_vertex(self):
temp = copy.deepcopy(self.node_list)
for node in temp:
node.next = None
return temp
# 添加边
def add_edge(self, orgin, end, weight=1):
self.edge_list.append(Edge(orgin, end, weight))
node1 = Node(end.data, weight, orgin.next)
node2 = Node(orgin.data, weight, end.next)
orgin.next = node1
end.next = node2
# 检测到成环,删除刚添加的边
def del_edge(self, orgin, end):
self.node_list[self.get_index(orgin.data)].next = self.node_list[self.get_index(orgin.data)].next.next
self.node_list[self.get_index(end.data)].next = self.node_list[self.get_index(end.data)].next.next
self.edge_list.pop()
# 根据数据获取索引 {"v0": 0, ...}
def get_index(self, data):
my_dict = {node.data: idx for idx, node in enumerate(self.node_list)}
return my_dict[data]
# 返回根节点的索引
def find_root(self, vertex_data):
index = self.get_index(vertex_data)
while self.parent[index] != -1:
index = self.parent[index]
return index
# 合并边上两点,并判断是否有环
def merge(self, edge):
node1 = edge.vertex1
node2 = edge.vertex2
node1_root = self.find_root(node1.data)
node2_root = self.find_root(node2.data)
if node1_root == node2_root:
return True
else:
# 将node2的父节点设为node1
self.parent[node2_root] = node1_root
return False
def take_weight(elem):
return elem.weight
def kruskal(table):
# 按照权重对边进行排序
edges = table.edge_list
edges.sort(key=take_weight, reverse=False)
# 创建一张新的图
graph = AdjTable()
graph.add_vertex(table.get_vertex())
# 依次取出权重最小的边
for edge in edges:
# 将权重最小的边添加到图中
graph.add_edge(graph.node_list[graph.get_index(edge.vertex1.data)],
graph.node_list[graph.get_index(edge.vertex2.data)],
edge.weight)
if graph.merge(edge):
graph.del_edge(edge.vertex1, edge.vertex2)
# 生成树中边数 = 节点数 - 1,停止
if len(graph.edge_list) == len(graph.node_list) - 1:
break
return graph
if __name__ == '__main__':
v0, v1, v2, v3, v4, v5, v6, v7, v8 = Node("v0"), Node("v1", ), Node("v2"), Node("v3"), \
Node("v4"), Node("v5"), Node("v6"), Node("v7"), Node("v8")
table = AdjTable()
table.add_vertex([v0, v1, v2, v3, v4, v5, v6, v7, v8])
table.add_edge(v0, v1, 4)
table.add_edge(v0, v7, 8)
table.add_edge(v1, v2, 8)
table.add_edge(v1, v7, 11)
table.add_edge(v2, v3, 7)
table.add_edge(v2, v5, 4)
table.add_edge(v2, v8, 2)
table.add_edge(v3, v4, 9)
table.add_edge(v3, v5, 14)
table.add_edge(v4, v5, 10)
table.add_edge(v5, v6, 2)
table.add_edge(v6, v7, 1)
table.add_edge(v6, v8, 6)
table.add_edge(v7, v8, 7)
graph = kruskal(table)
# 查看最小生成树连接情况
print("最小生成树")
for i in range(len(graph.node_list)):
node = graph.node_list[i]
print("v{} links:".format(i), end="")
while node.next:
print(node.next.data, end=" ")
node = node.next
print()
4.2 DFS找环
使用DFS判断是否成环,思路是从一点出发,如果到另外一点有两条路径,即可判断有环存在
import copy
class Stack(object):
def __init__(self):
self.list = []
def is_empty(self):
return len(self.list) == 0
def push(self, data):
self.list.insert(0, data)
def pop(self):
del self.list[0]
def get_top(self):
return self.list[0]
def size(self):
return len(self.list)
def show_stack(self):
return [node.data for node in self.list]
class Node(object):
def __init__(self, data, weight=1, nnext=None):
self.data = data
self.weight = weight
self.next = nnext
def get_neighbor(self):
node = self.next
neighborhood = []
while node:
# 不能用 node,用node.data,否则会出现单链之间互相链接
neighborhood.append(node.data)
node = node.next
return neighborhood
class Edge(object):
def __init__(self, ver1, ver2, w=1):
self.vertex1 = ver1
self.vertex2 = ver2
self.weight = w
class AdjTable(object):
def __init__(self):
self.node_list = []
self.edge_list = []
def add_vertex(self, node_list):
self.node_list.extend(node_list)
# 获取顶点,但不获取连接,用于复制一份只有顶点的图
def get_vertex(self):
temp = copy.deepcopy(self.node_list)
for node in temp:
node.next = None
return temp
# 添加边
def add_edge(self, orgin, end, weight=1):
self.edge_list.append(Edge(orgin, end, weight))
node1 = Node(end.data, weight, orgin.next)
node2 = Node(orgin.data, weight, end.next)
orgin.next = node1
end.next = node2
# 成环之后删除刚添加的边
def del_edge(self, orgin, end):
self.node_list[self.get_index(orgin.data)].next = self.node_list[self.get_index(orgin.data)].next.next
self.node_list[self.get_index(end.data)].next = self.node_list[self.get_index(end.data)].next.next
self.edge_list.pop()
# 根据数据获取索引 {"v0": 0, ...}
def get_index(self, data):
my_dict = {node.data: idx for idx, node in enumerate(self.node_list)}
return my_dict[data]
# 使用dfs检测是否成环
def is_ring(self, start_node):
record = []
s = Stack()
visited = [0] * len(self.node_list)
ring_flag = [0] * len(self.node_list)
s.push(start_node)
visited[self.get_index(start_node.data)] = 1
while not s.is_empty():
current_node = s.get_top()
neighbor = current_node.get_neighbor()
unvisited_neighbor = []
for data in neighbor:
if visited[self.get_index(data)] == 0:
# 记录到某点的路径
record.append(current_node.data + "-" + data)
unvisited_neighbor.append(data)
if len(unvisited_neighbor):
s.push(self.node_list[self.get_index(unvisited_neighbor[0])])
visited[self.get_index(unvisited_neighbor[0])] = 1
else:
s.pop()
# 如果从一点出发到另外一点,有两条路径,则认为成环
record = list(set(record))
for ele in record:
ring_flag[int(ele[-1])] += 1
# 判断是否成环
if max(ring_flag) == 2:
return True
else:
return False
def take_weight(elem):
return elem.weight
def kruskal(table):
# 按照权重对边进行排序
edges = table.edge_list
edges.sort(key=take_weight, reverse=False)
# 创建一张新的图
graph = AdjTable()
graph.add_vertex(table.get_vertex())
# 依次取出权重最小的边
for edge in edges:
# 将权重最小的边添加到图中
graph.add_edge(graph.node_list[graph.get_index(edge.vertex1.data)],
graph.node_list[graph.get_index(edge.vertex2.data)],
edge.weight)
# 判断是否成环,成环舍弃此边
for node in graph.node_list:
if graph.is_ring(node):
# 成环删除此边
graph.del_edge(edge.vertex1, edge.vertex2)
break
# 生成树中边数 = 节点数 - 1,停止
if len(graph.edge_list) == len(graph.node_list) - 1:
break
return graph
if __name__ == '__main__':
v0, v1, v2, v3, v4, v5, v6, v7, v8 = Node("v0"), Node("v1", ), Node("v2"), Node("v3"), \
Node("v4"), Node("v5"), Node("v6"), Node("v7"), Node("v8")
table = AdjTable()
table.add_vertex([v0, v1, v2, v3, v4, v5, v6, v7, v8])
table.add_edge(v0, v1, 4)
table.add_edge(v0, v7, 8)
table.add_edge(v1, v2, 8)
table.add_edge(v1, v7, 11)
table.add_edge(v2, v3, 7)
table.add_edge(v2, v5, 4)
table.add_edge(v2, v8, 2)
table.add_edge(v3, v4, 9)
table.add_edge(v3, v5, 14)
table.add_edge(v4, v5, 10)
table.add_edge(v5, v6, 2)
table.add_edge(v6, v7, 1)
table.add_edge(v6, v8, 6)
table.add_edge(v7, v8, 7)
graph = kruskal(table)
# 查看最小生成树连接情况
print("最小生成树")
for i in range(len(graph.node_list)):
node = graph.node_list[i]
print("v{} links:".format(i), end="")
while node.next:
print(node.next.data, end=" ")
node = node.next
print()