【学习笔记】比较分别用prim和kruskal实现最小生成树和算法优化方案
kruskal:
1.思路
:設G=(V,E)是無向連通帶權圖,V={,…,n};設最小生成樹T=(V,TE),該樹的初始狀態為只有n個頂點而無邊的非連通圖T=(V,{}),Kruskal算法將這n個頂點看成是n個孤立的連通分支。它首先將所有的邊按權值從小到大排序,然后只要T中選中的邊數不到n-1,就做如下的貪心選擇:在邊集E中選取權值最小的邊(i,j),如果將邊(i,j)加入集合TE中不產生回路(圈),則將邊(i,j)加入邊集TE中,即用邊(i,j)將這兩個連通分支合并連接成一個連通分支;否則繼續選擇下一條最短邊。把邊(i,j)從集合E中刪去。繼續上面的貪心選擇,直到T中所有頂點都在同一個連通分支上為止。此時,選取到的n-1條邊恰好構成G的一棵最小生成樹T。 那么,怎樣判斷加入某條邊后圖T會不會出現回路呢? 該算法對于手工計算十分方便,因為用肉眼可以很容易看到挑選哪些邊能夠避免構成回路(避圈法),但使用計算機程序來實現時,還需要一種機制來進行判斷。Kruskal算法用了一個非常聰明的方法,就是運用集合避圈:如果所選擇加入的邊的起點和終點都在T的集合中,那么就可以斷定一定會形成回路(圈)。其實就是我們”:邊的兩個結點不能屬于同一集合。 步驟1:初始化。將圖G的邊集E中的所有邊按權值從小到大排序,邊集TE={ },把每個頂點都初始化為一個孤立的分支,即一個頂點對應一個集合。
步驟2:在E中尋找權值最小的邊(i,j)。
步驟3:如果頂點i和j位于兩個不同連通分支,則將邊(i,j)加入邊集TE,并執行合并操作,將兩個連通分支進行合并。 步驟4:將邊(i,j)從集合E中刪去,即E=E-{(i,j)}。
步驟5:如果選取邊數小于n-1,轉步驟2;否則,算法結束,生成最小生成樹T。
偽碼詳解
(1)數據結構 int nodeset[N];//集合號數組
struct Edge {//邊的存儲結構
int u;
int v;
int w;
}e[N*N]; (2)初始化 void Init(int n){
for(int i = 1; i <= n; i++) nodeset[i] = i;//每個結點賦值一個集合號}
(3)對邊進行排序 bool comp(Edge x, Edge y) { return x.w < y.w;//定義優先級,按邊值進行升序排序}sort(e, e+m, comp);//調用系統排序函數
(4)合并集合 int Merge(int a, int b){ int p = nodeset[a];//p為a結點的集合號 int q = nodeset[b]; //q為b結點的集合號 if(p==q) return 0; //集合號相同,什么也不做,返回 for(int i=1;i<=n;i++)//檢查所有結點,把集合號是q的全部改為p { if(nodeset[i]==q) nodeset[i] = p;//a的集合號賦值給b集合號
} return 1;}
kruskal完整代碼
#include <iostream> #include <cstdio> #include <algorithm> using namespace std; const int N = 100; int nodeset[N]; int n, m; struct Edge {int u;int v;int w; }e[N*N]; bool comp(Edge x, Edge y) {return x.w < y.w; } void Init(int n) {for(int i = 1; i <= n; i++) nodeset[i] = i; } int Merge(int a, int b) { int p = nodeset[a]; int q = nodeset[b]; if(p==q) return 0; for(int i=1;i<=n;i++)//檢查所有結點,把集合號是q的改為p { if(nodeset[i]==q) nodeset[i] = p;//a的集合號賦值給b集合號 } return 1;}int Kruskal(int n){ int ans = 0; for(int i=0;i<m;i++) if(Merge(e[i].u, e[i].v)) { ans += e[i].w; n--; if(n==1) return ans; } return 0;}int main() {cout <<"輸入結點數n和邊數m:"<<endl;cin >> n >> m;Init(n);cout <<"輸入結點數u,v和邊值w:"<<endl;for(int i=1;i<=m;i++)cin >> e[i].u>> e[i].v >>e[i].w;sort(e, e+m, comp);int ans = Kruskal(n);cout << "最小的花費是:" << ans << endl;return 0; }1)時間復雜度:算法中,需要對邊進行排序,若使用快速排序,執行次數為eloge,算法的時間復雜度為O(eloge)。而合并集合需要n-1次合并,每次為O(n),合并集合的時間復雜度為O(n2)。
2)空間復雜度:算法所需要的輔助空間包含集合號數組 nodeset[n],則算法的空間復雜度是O(n)。
6.算法優化拓展
該算法合并集合的時間復雜度為O(n2),我們可以用并查集的思想優化,使合并集合的時間復雜度降為O(e*logn),優化后的程序如下。
#include <iostream> #include <cstdio> #include <algorithm> using namespace std; const int N = 100; int father[N]; int n, m; struct Edge {int u;int v;int w; }e[N*N]; bool comp(Edge x, Edge y) {return x.w < y.w;//排序優先級,按邊的權值從小到大 } void Init(int n) {for(int i = 1; i <= n; i++)father[i] = i;//頂點所屬集合號,初始化每個頂點一個集合號 } int Find(int x) //找祖宗 {if(x != father[x])father[x] = Find(father[x]);//把當前結點到其祖宗路徑上的所有結點的集合號改為祖宗集合號return father[x]; //返回其祖宗的集合號 } int Merge(int a, int b) //兩結點合并集合號 {int p = Find(a); //找a的集合號int q = Find(b); //找b的集合號if(p==q) return 0;if(p > q)father[p] = q;//小的集合號賦值給大的集合號elsefather[q] = p;return 1; } int Kruskal(int n) {int ans = 0;for(int i=0;i<m;i++) if(Merge(e[i].u, e[i].v)) { ans += e[i].w; n--; if(n==1) return ans; } return 0;}int main(){ cout <<"輸入結點數n和邊數m:"<<endl; cin >> n >> m; Init(n); cout <<"輸入結點數u,v和邊值w:"<<endl; for(int i=1;i<=m;i++) cin>>e[i].u>>e[i].v>>e[i].w; sort(e, e+m, comp); int ans = Kruskal(n); cout << "最小的花費是:" << ans << endl; return 0;}輸入結點數n和邊數m:
7 12輸入結點數u,v和邊值w:
1 2 23
1 6 28
1 7 36
2 3 20
2 7 1
3 4 15
3 7 4
4 5 3
4 7 9
5 6 17
5 7 16
6 7 25
輸出57
prim
步驟1:確定合適的數據結構。設置帶權鄰接矩陣C存儲圖G,如果圖G中存在邊(u,x),令C[u][x]等于邊(u,x)上的權值,否則,C[u][x]=∞;bool數組s[],如果s[i]=true,說明頂點i已加入集合U。
可以通過設置兩個數組巧妙地解決這個問題,closest[j]表示V-U中的頂點j到集合U中的最鄰近點,lowcost[j]表示V-U中的頂點j到集合U中的最鄰近點的邊值,即邊(j,closest[j])的權值。
(https://img-blog.csdnimg.cn/2021052415592813.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxMzU4NTc0,size_16,color_FFFFFF,t_70)
例如,在圖中7號結點到U集合中的最鄰近點是2,closest[7]=2,如圖2-63所示。7號結點到最鄰近點2的邊值為1,即邊(2,7)的權值,記為lowcost[7]=1,如圖2-64所示。
只需要在V-U集合中找lowcost[ ]值最小的頂點即可。
步驟2:初始化。令集合U={u0},u0∈V,并初始化數組closest[]、
lowcost[]和s[]。
步驟3:在V-U集合中找lowcost值最小的頂點t,
即lowcost[t]=min{lowcost[j]|j∈V-U},
滿足該公式的頂點t就是集合V-U中連接集合U的最鄰近點。 步驟4:將頂點t加入集合U。 步驟5:如果集合V-U,算法結束,否則,轉步驟6。 步驟6:對集合V-U中的所有頂點j,更新其lowcost[]和closest[]。cost [j]= C [t] [j]; closest [j] = t; },轉步驟3。 按照上述步驟,最終可以得到一棵權值之和最小的生成樹。
更新公式:
if(C[t] [j]<lowcost [j] ) {
lowcost [j]= C [t] [j]; closest [j] = t; },轉步驟3。 按照上述步驟,最終可以得到一棵權值之和最小的生成樹。
偽代碼詳解
(1)初始化。s[1]=true,初始化數組closest,除了u0外其余頂點最鄰近點均為u0,表示V-U中的頂點到集合U的最臨近點均為u0;初始代數組lowcost,u0到V-U中的頂點的邊值,無邊相連則為∞(無窮大)
s[u0] = true; //初始時,集合中U只有一個元素,即頂點u0
for(i = 1; i <= n; i++) {
if(i != u0) //除u0之外的頂點 { lowcost[i] = c[u0][i]; //u0到其它頂點的邊值 closest[i] = u0; //最鄰近點初始化為u0 s[i] = false; //初始化u0之外的頂點不屬于U集合,即屬于V-U集合 } else lowcost[i] =0;}
2)在集合V-U中尋找距離集合U最近的頂點t。
int temp = INF;
int t = u0;
for(j = 1; j <= n; j++) //在集合中V-U中尋找距離集合U最近的頂點t
{
if((!s[j]) && (lowcost[j] < temp)) //!s[j] 表示j結點在V-U集合中
{
t = j;
temp = lowcost[j];
}
}
if(t == u0) //找不到t,跳出循環
break;
(3)更新lowcost和closest數組。 s[t] = true; //否則,t加入集合U
for(j = 1; j <= n; j++) //更新lowcost和closest
{
if((!s[j]) && (c[t][j] < lowcost[j])) // !s[j] 表示j結點在V-U集合中
//t到j的邊值小于當前的最鄰近值
{
lowcost[j] = c[t][j]; //更新j的最鄰近值為t到j的邊值
closest[j] = t; //更新j的最鄰近點為t
}
}
prim完整代碼
#include <iostream> using namespace std; const int INF = 0x3fffffff; const int N = 100; bool s[N]; int closest[N]; int lowcost[N]; void Prim(int n, int u0, int c[N][N]) { //頂點個數n、開始頂點u0、帶權鄰接矩陣C[n][n]//如果s[i]=true,說明頂點i已加入最小生成樹//的頂點集合U;否則頂點i屬于集合V-U//將最后的相關的最小權值傳遞到數組lowcosts[u0] = true; //初始時,集合中U只有一個元素,即頂點u0int i;int j;for(i = 1; i <= n; i++)//①{if(i != u0) {lowcost[i] = c[u0][i];closest[i] = u0;s[i] = false;}elselowcost[i] =0;}for(i = 1; i <= n; i++) //②{int temp = INF;int t = u0;for(j = 1; j <= n; j++) //③在集合中V-u中尋找距離集合U最近的頂點t{ if((!s[j]) && (lowcost[j] < temp)) {t = j;temp = lowcost[j];}}if(t == u0)break; //找不到t,跳出循環s[t] = true; //否則,講t加入集合Ufor(j = 1; j <= n; j++) //④更新lowcost和closest{ if((!s[j]) && (c[t][j] < lowcost[j])){lowcost[j] = c[t][j];closest[j] = t; } } }} int main() {int n, c[N][N], m, u, v, w;int u0;cout <<"輸入結點數n和邊數m:"<<endl;cin >> n >> m;int sumcost = 0;for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) c[i][j] = INF;cout <<"輸入結點數u,v和邊值w:"<<endl;for(int i=1; i<=m; i++) {cin >> u >> v >> w;c[u][v] = c[v][u] = w;}cout <<"輸入任一結點u0:"<<endl;cin >> u0 ;//計算最后的lowcos的總和,即為最后要求的最小的費用之和Prim(n, u0, c);cout <<"數組lowcost的內容為:"<<endl; for(int i = 1; i <= n; i++) cout << lowcost[i] << " "; cout << endl; for(int i = 1; i <= n; i++) sumcost += lowcost[i]; cout << "最小的花費是:" << sumcost << endl << endl; return 0;}總結
以上是生活随笔為你收集整理的【学习笔记】比较分别用prim和kruskal实现最小生成树和算法优化方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【项目】uniapp前端接收后端spri
- 下一篇: 【笔记】springboot+sprin