@
http://libo-sober.top/mydata/PythonPrimAndKruskal.html
最小生成樹(Prim算法、Kruskal算法)
生成樹的定義
生成樹是一個連通圖G的一個極小連通子圖。包含G的所有n個頂點,但只有n-1條邊,並且是連通的。 生成樹可由遍歷過程中所經過的邊組成(有多個)。
擴展:無向圖。極小連通子圖與極大連通子圖是在無向圖中進行討論的。
連通圖:在無向圖中,若從定點V1到V2有路徑,則稱頂點V1和V2是連通的。如果圖中任意一對頂點都是連通的,則稱此圖是連通圖。(連通的無向圖)
極小連通子圖:
1.一個連通圖的生成樹是該連通圖頂點集確定的極小連通子圖。(同一個連通圖可以有不同的生成樹,所以生成樹不是唯一的)
(極小連通子圖只存在於連通圖中)
2.用邊把極小連通子圖中所有節點給連起來,若有n個節點,則有n-1條邊。如下圖生成樹有6個節點,有5條邊。
3.之所以稱為極小是因為此時如果刪除一條邊,就無法構成生成樹,也就是說給極小連通子圖的每個邊都是不可少的。
4.如果在生成樹上添加一條邊,一定會構成一個環。
也就是說只要能連通圖的所有頂點而又不產生回路的任何子圖都是它的生成樹。
最小生成樹的定義
一個帶權連通無向圖的生成樹中,邊的權值之和最小的那棵樹叫做此圖的最小生成樹。
圖一的最小生成樹就是圖二(最小生成樹在某些情況下並不唯一
)。
最小生成樹的生成算法:
求解最小生成樹的算法主要有兩個:
1.Prim(普里姆)算法;
2.Kruskal(克魯斯卡爾)算法。
Prim算法
- 輸入:一個加權連通圖,其中頂點集合為V,邊集合為E;
- 初始化:定義存放當前已走點的集合Vnew = {x},其中x為集合V中的任意節點(作為起始點),定義存放當前已走邊的集合Enew = { },為空;
- 重復下列操作,直到 Vnew = V:
① 在集合E中選取權值最小的邊<u, v>,其中u為集合Vnew中的元素,而v不在Vnew集合當中,並且v∈V(如果存在有多條滿足前述條件即具有相同權值的邊,則可任意選取其中之一);
② 將v加入集合Vnew中,將<u, v>邊加入集合Enew中; - 輸出:使用集合Vnew和Enew來描述所得到的最小生成樹。
比如首先是一張圖,然后是一些定義的空的變量集合:其中,當前可選點集U是指與當前已選點集中的點存在可達關系且未被標記為已選的所有點所構成的集合。
接下來我們隨機選擇一個點作為起點,比如選擇V1
作為初始點,則需要將V1加入Vnew
集合,情況如下:
容易看出,在已選點集和可選點集連接形成的可選邊中(<V1 ,V2>,<V1 ,V3>,<V1 ,V4>),邊<V1 ,V3>的權值最小,因此需要將邊<V1 ,V3>加入Enew
集合,同時也需要將點V3加入Vnew
集合,此時兩個集合的情況如下:
接下來在已選點集和可選點集連接形成的可選邊中(<V1 ,V2>,<V1 ,V4>,<V3 ,V2>,<V3 ,V4>,<V3 ,V5>,<V3 ,V6>),選擇權值最小的邊<V3 ,V6>,將其加入Enew集合,同時將點V6加入Vnew集合,此時兩個集合的情況如下:
接着在已選點集和可選點集連接形成的可選邊中(<V1 ,V2>,<V1 ,V4>,<V3 ,V2>,<V3 ,V4>,<V3 ,V5>,<V6 ,V4>,<V6 ,V5>),邊<V6 ,V4>的權值最小,因此需要將邊<V6 ,V4>加入Enew集合,同時也需要將點V4加入Vnew集合,此時兩個集合的情況如下:
接着繼續在已選點集和可選點集連接形成的可選邊中(<V1 ,V2>,<V3 ,V2>,<V3 ,V5>,<V6 ,V5>),選擇權值最小的邊<V3 ,V2>,並將其加入Enew集合,同時將點V2加入Vnew集合,此時兩個集合的情況如下:
接着繼續在已選點集和可選點集連接形成的可選邊中(<V2 ,V5>,<V3 ,V5>,<V6 ,V5>),選擇權值最小的邊<V2 ,V5>,並將其加入Enew集合,同時將點V2加入Vnew集合,此時兩個集合的情況如下:
至此,集合Vnew = V,即所有的點都已經被選中,故結束循環。
此時,集合Vnew和Enew即可用於描述所得到的最小生成樹(如上圖中的紅色部分)。
python棧和隊列、二叉樹
圖的兩種表示形式
鄰接矩陣
矩陣的每行和每列都代表圖中的頂點,如果兩個頂點之間有邊相連,設定行列值。
無權邊則將矩陣分量標注位1或0
帶權的邊則將權重保存為矩陣分量值。
鄰接表
鄰接表(adjacent list)可以成為稀疏圖的更高效實現方案。
它維護一個包含所有頂點的主列表,主列表中的每個頂點,再關聯一個與自身右邊連接的所有頂點的列表。
下面給出用編程語言實現的具體描述(Python):
"""
Prim類接受兩個參數n,m,分別表示錄入的點和邊的個數(點的編號從1開始遞增)
接下來是m行,每行3個數x,y,length,表示點x和點y之間存在一條長度為length的邊
程序最終會打印出該無向圖中的最小生成樹的各個邊和最小權值
"""
class Node:
"""
Node類存放節點信息,
node是與某個節點(實際上是用列表索引值代表的)相連接的點
length表示這兩個點之間的權值
"""
def __init__(self, node, length):
self.node = node
self.length = length
class Edge:
"""
Edge類表示邊的信息
x,y表示這條邊的兩個頂點
length為<x,y>之間的距離
"""
def __init__(self, x, y, length):
self.x = x
self.y = y
self.length = length
class Prim:
"""Prim算法:求解無向連通圖中的最小生成樹 """
def __init__(self, n, m):
self.n = n # 輸入的點個數
self.m = m # 輸入的邊個數
self.v = [[] for i in range(n+1)] # 存放所有節點之間的可達關系與距離
"""圖的鄰接表的表示法,v是一個二維列表,其索引值代表當前節點,索引值對應的一維列表中存放的
是與以當前索引值為頂點的相連的節點信息"""
self.e = [] # 存放與當前已選節點相連的邊
"""e是一個一維列表,存放的是與當前頂點相連的所有的邊的信息"""
self.s = [] # 存放最小生成樹里的所有邊
self.vis = [False for i in range(n+1)] # 標記每個點是否被訪問過,False未訪問
def graphy(self):
"""構建圖,這里用的是鄰接表"""
for i in range(self.m):
x, y, length = list(map(int, input().split()))
self.v[x].append(Node(y, length)) # 與x相連的y節點記錄在二維列表v的x索引值對應的一維列表中
self.v[y].append(Node(x, length)) # 與y相連的x節點記錄在二維列表v的y索引值對應的一維列表中
# print(self.v)
def insert(self, point):
"""往Vnew中插入一個新的點"""
for i in range(len(self.v[point])):
"""把與point節點相連的且未被訪問的節點的邊加入列表e中"""
if not self.vis[self.v[point][i].node]:
self.e.append(Edge(point, self.v[point][i].node, self.v[point][i].length))
self.vis[point] = True
self.e = sorted(self.e, key=lambda e: e.length) # 把e中的所有邊按邊的長度從小到大排序
# for i in self.e:
# print(i.length)
def run(self, start):
"""執行函數:求解錄入的無向連通圖的最小生成樹"""
self.insert(start) # start為選擇的開始頂點
while self.n - len(self.s) > 1: # 最小生成樹的邊數=圖中節點數-1,因此可以利用這個性質來作為循環條件
for i in range(len(self.e)): # 按序遍歷所有邊
if not self.vis[self.e[i].y]: # 如果待檢測的第二個位置上的點未訪問過,則說明這條邊滿足一端在Vnew中,另一端不在
self.s.append(self.e[i]) # 則需要將該邊放進結果集合中,因為第一個遇到的肯定是權值最小的,在插入函數中已經排好序了
self.insert(self.e[i].y) # 同時將該邊的另一個節點插入Vnew中
break # 找到一條符號條件的便后就退出for循環
def print(self):
"""輸出信息"""
print(f'當前錄入總邊數為:{len(self.e)}\n其中構成最小生成樹的邊為:')
edge_sum = 0
for i in range(len(self.s)):
print(f'邊<{self.s[i].x},{self.s[i].y}> = {self.s[i].length}')
edge_sum += self.s[i].length
print(f'最小生成樹的權值為:{edge_sum}')
def main():
n, m = list(map(int, input().split()))
prim = Prim(n, m)
prim.graphy()
prim.run(1)
prim.print()
if __name__ == '__main__':
main()
在程序運行時,輸入以下內容:
6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6
運行結果:
6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6
當前錄入總邊數為:10
其中構成最小生成樹的邊為:
邊<1,3> = 1
邊<3,6> = 4
邊<6,4> = 2
邊<3,2> = 5
邊<2,5> = 3
最小生成樹的權值為:15
Process finished with exit code 0
Kruskal算法
- 設無向連通圖Graph有v個頂點,e條邊;
- 新建圖Graphnew,Graphnew擁有與原圖中相同的v個頂點,但沒有邊;
- 將原圖Graph中所有邊按權值從小到大排序;
- 進入一層循環,該循環從權值最小的邊開始遍歷每條邊,直至圖Graphnew中所有的節點都在同一個連通分量中。循環體內的內容如下:
if(這條邊連接的兩個節點於圖Graphnew中不在同一個連通分量中) 則添加這條邊到圖Graphnew
中;
下面我們還是用前面的圖一來進行舉例說明,首先是將原圖中的所有邊按權值從小到大進行排序,如下:
然后是建立一個沒有任何邊的圖Graphnew,如下(注:這里的沒有邊是指沒有被描紅的邊):
接下來我們開始遍歷上面給出的那個表,首先遍歷到的第一條邊是<V1,V3>,長度為1。由於在當前圖中,節點V1和V3並不連通,因此可以將這條邊添加到上圖中(即標紅,下同),如下:
接下來是第二條邊<V4,V6>,長度為2。在當前圖中,節點V4和V6並不連通,因此可以將這條邊添加到上圖中,如下:
然后是第三條邊<V2,V5>,長度為3。在當前圖中,節點V2和V5並不連通,因此可以將這條邊添加到上圖中,如下:
繼續往下是第四條邊<V3,V6>,長度為4。在當前圖中,節點V3和V6並不連通,因此可以將這條邊添加到上圖中,如下:
然后是第五條邊<V1,V4>,長度為5。注意到在當前圖中,節點V1和V4已經在同一個連通分支中,因此不能將這條邊添加進來,於是跳過這條邊,繼續往下。
接着是第六條邊<V2,V3>,長度也為5。在當前圖中,節點V2和V3並不連通,因此可以將這條邊添加到上圖中,如下:
至此,我們發現圖Graphnew中的所有節點都在同一個連通分支中,因此循環結束。
此時,邊集E即可用於描述所得到的最小生成樹(如上圖中的紅色部分)。
下面給出用編程語言實現的具體描述(python):
"""
Kruskal類接受兩個參數n,m,分別表示錄入的點和邊的個數(點的編號從1開始遞增)
接下來是m行,每行3個數x,y,length,表示點x和點y之間存在一條長度為length的邊
程序最終會打印出該無向圖中的最小生成樹的各個邊和最小權值
"""
class Edge:
"""
Edge類表示邊的信息
x,y表示這條邊的兩個頂點
length為<x,y>之間的距離
"""
def __init__(self, x, y, length):
self.x = x
self.y = y
self.length = length
class UnionFindSet:
"""並查集類,用於連通兩個節點以及判斷圖中的所有節點是否連通 """
def __init__(self, start, n):
self.start = start # start和n分別用於指示並查集里節點的起點和終點
self.n = n
self.pre = [0 for i in range(self.n - self.start + 2)] # pre數組用於存放某個節點的上級
self.rank = [0 for i in range(self.n - self.start + 2)] # rank數組用於降低關系樹的高度
def init(self):
"""初始化並查集"""
for i in range(self.start, self.n+1):
self.pre[i] = i
self.rank[i] = 1
def find_pre(self, x):
"""尋找節點x的上一級節點"""
if self.pre[x] == x:
return x
else:
self.pre[x] = self.find_pre(self.pre[x])
return self.pre[x]
def is_same(self, x, y):
"""判斷x節點和y節點是否連通"""
return self.find_pre(x) == self.find_pre(y)
def unite(self, x, y):
"""判斷兩個節點是否連通,如果未連通則將其連通並返回真,否則返回假"""
x = self.find_pre(x)
y = self.find_pre(y)
if x == y:
return False
if self.rank[x] > self.rank[y]:
"""類似於平衡二叉樹的概念"""
self.pre[y] = x
else:
if self.rank[x] == self.rank[y]:
self.rank[y] += 1
self.pre[x] = y
return True
def is_one(self):
"""判斷整個無向圖中的所有節點是否連通 """
temp = self.find_pre(self.start)
for i in range(self.start+1, self.n+1):
if self.find_pre(i) != temp:
return False
return True
class Kruskal:
"""Kruskal算法:求解無向連通圖中的最小生成樹 """
def __init__(self, n, m):
self.n = n # n,m分別表示輸入的點和邊的個數
self.m = m
self.e = [] # 存放錄入的無向連通圖的所有邊
self.s = [] # 存放最小生成樹里的所有邊
self.u = UnionFindSet(1, self.n) # 並查集:抽象實現Graphnew,並完成節點間的連接工作以及判斷整個圖是否連通
def graphy(self):
"""這里只是存儲所有邊的信息並按邊的長度排序"""
for i in range(self.m):
x, y, length = list(map(int, input().split()))
self.e.append(Edge(x, y, length))
self.e.sort(key=lambda e: e.length)
# for i in self.e:
# print(i.length)
self.u.init()
def run(self):
"""執行函數:求解錄入的無向連通圖的最小生成樹 """
for i in range(self.m):
if self.u.unite(self.e[i].x, self.e[i].y):
self.s.append(self.e[i])
if self.u.is_one():
"""一旦Graphnew的連通分支數為1,則說明求出了最小生成樹 """
break
def print(self):
print(f'構成最小生成樹的邊為:')
edge_sum = 0
for i in range(len(self.s)):
print(f'邊 <{self.s[i].x},{self.s[i].y}> = {self.s[i].length}')
edge_sum += self.s[i].length
print(f'最小生成樹的權值為:{edge_sum}')
def main():
n, m = list(map(int, input().split()))
kruskal = Kruskal(n, m)
kruskal.graphy()
kruskal.run()
kruskal.print()
if __name__ == '__main__':
main()
運行結果:
6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6
構成最小生成樹的邊為:
邊 <1,3> = 1
邊 <4,6> = 2
邊 <2,5> = 3
邊 <3,6> = 4
邊 <2,3> = 5
最小生成樹的權值為:15