最小生成樹與判斷無向圖是否有回路(並查集)
一、最小生成樹算法:
(1)Kruskal算法
(a)找出權重最小的邊
(b)判斷加入該邊以后是否會構成回路(並查集),如果不會,將該邊加入生成樹中
重復(a)(b),直到生成樹中有n-1條邊
(2)Prim算法
選一個結點作為起始結點,並將其加入已選結點集合;
(a)尋找與已選結點集合任一 一個(不能是兩個)結點相關聯的最小邊(也就是這條最小邊關聯的結點不能都在已選結點集合中,從而保證了加入這條邊一定不會構成回路)
重復(a)直到生成樹中有n-1條邊
注意:該算法不用使用並查集判斷是否有回路,因為加入的最小邊關聯的結點不能都在已選結點集合中
代碼實現:(Prim算法,可AC練習題目,可優化(參考下面算法2練習題目1的代碼))20191206
#include<iostream> #define inf 999999 using namespace std; int book[310],e[301][310],n,count; int maxm = -inf; int Prim(int cur){ book[cur] = 1; count++; int minn = inf; int index; int node; for(int j=1;j<=n;j++){ if(book[j]==1){ for(int i=1;i<=n;i++){ if(book[i]==0 && e[j][i]<minn) { minn = e[j][i]; node = j; index = i;//find the smallest distance of two vertices } } } } if(e[node][index]>maxm) maxm = e[node][index]; return index; } int main(){ int i,j,m,a,b,c; cin>>n>>m; for(i=1;i<=n;i++){ for(j=1;j<=n;j++){ if(i==j) e[i][j]=0; else e[i][j]=inf; } } for(i=1;i<=m;i++){ cin>>a>>b>>c; e[a][b]=c; e[b][a]=c;//indirected graph } int next = Prim(1); while(count!=n-1){ //count used to judge if all vertices are in the tree next = Prim(next); } cout<<count<<" "<<maxm<<endl; return 0; }
二、用Kruskal判斷無向圖是否有回路的方法總結:
算法1:並查集(推薦)
並查集(與路徑壓縮)算法描述:《算法筆記》P328-332
-
-
初始化:每個元素都是獨立的一個集合
-
查找:反復尋找父親結點,直到找到根節點
-
合並:將一個集合的根節點的父親指向另一個結合的根節點
-
-
路徑壓縮
-
把當前查詢結點的路徑上的所有結點的父親都指向根節點
-
目的是為了降低查找的時間復雜度
-
因此帶有路徑壓縮的合並操作步驟如下:以合並ab兩個結點為例
-
查找a結點的根結點:將查詢a結點過程中所遍歷到的結點的父親指向根節點
-
查找b結點的根結點:同上
-
-
-
代碼實現:(可優化(參考下面算法2練習題目1的代碼))20200130

#include<iostream> #define inf 9999999 using namespace std; int n,edge,maxm; int e[310][310],father[310]; int findfather(int node){ int vertex = node; while(node != father[node]) node = father[node]; while(vertex != father[vertex]){// path compression int temp = vertex; vertex = father[vertex]; father[temp] = node; } return node; } void unionsets(int node1,int node2){ int fa1 = findfather(node1); int fa2 = findfather(node2); father[fa2] = fa1; } bool cycle(int nodea,int nodeb){ if(findfather(nodea) == findfather(nodeb)) return false; else { unionsets(nodea,nodeb); edge++;// end condition return true; } } void Kruskal(){ int minn = inf; int nodea,nodeb; for(int i=1;i<=n;i++){ for(int j=i+1;j<=n;j++){ if(e[i][j] < minn) { minn = e[i][j]; nodea = i; nodeb = j; } } } if(cycle(nodea,nodeb)){ maxm = e[nodea][nodeb]; } e[nodea][nodeb] = inf; e[nodeb][nodea] = inf;//ignore the edge checked } int main(){ int m; cin>>n>>m; for(int i=1;i<=n;i++){ father[i] = i; //initialize for(int j=1;j<=n;j++){ e[i][j]=inf; } } int a,b,c; for(int i=1;i<=m;i++){ cin>>a>>b>>c; e[a][b]=c; e[b][a]=c;//indirected } while(edge<n-1) Kruskal(); cout<<edge<<" "<<maxm<<endl; return 0; }
測試數據:見練習題目第1個
算法2:基於dfs判斷(同有向圖)
算法描述:拓撲排序與判斷有向圖是否有環
區別於有向圖的是:用dfs判斷無向圖是否有回路要注意防止結點“殺回馬槍”,也就是說要防止結點訪問它的父親結點;比如說1dfs到2,要防止2dfs時去訪問1.
解決辦法就是記錄每一個結點的父親結點。
代碼實現:
練習題目2:20200129

#include<iostream> #define inf 99999999 using namespace std; int book[110],e[110][110],n,sum,father[110],arr[110][110]; int base = inf; bool dfs (int cur){ book[cur] = -1; for(int i=1;i<=n;i++){ if(e[cur][i]!=0&&e[cur][i]!=inf&&father[cur]!=i&&book[i]==-1) return false; if(e[cur][i]!=0&&e[cur][i]!=inf&&book[i]==0){ father[i] = cur; if(!dfs(i)) return false; } } book[cur] = 1; return true; } bool Kruskal(){ int minn = inf; int nodea,nodeb; for(int i=1;i<=n;i++){ for(int j=i+1;j<=n;j++){ if(arr[i][j]<minn) { minn = arr[i][j]; nodea = i; nodeb = j; } } } if(minn!=inf){ e[nodea][nodeb] = minn; e[nodeb][nodea] = minn; for(int i=0;i<110;i++) book[i] = 0; bool state=1; for(int i=1;i<=n;i++){ base = inf; if(!dfs(i)){ state = 0; break; } } if(!state){ e[nodea][nodeb] = inf; e[nodeb][nodea] = inf; } else sum += minn; arr[nodea][nodeb] = inf; return true; } else return false; } int main(){ cin>>n; int temp; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ cin>>temp; if(i==j) temp = inf; arr[i][j] = temp; e[i][j] = inf; } } while(Kruskal()) ; cout<<sum<<endl; return 0; }
練習題目1:20200131(相對於練習題目2的代碼有很大優化)

#include<iostream> #include<algorithm> #define inf 999999 using namespace std; int n,counte,maxm; int book[310],father[310]; bool e[310][310]; typedef struct{ int nodea; int nodeb; int weight; }Edge; Edge edge[100010]; bool cmp(Edge e1,Edge e2){ return e1.weight < e2.weight; } bool dfs (int cur){ book[cur] = -1; for(int i=1;i<=n;i++){ if(e[cur][i]==0) continue; if(e[cur][i]==1 && father[cur]!=i && book[i]==-1) return false; if(e[cur][i]==1 && book[i]==0){ father[i] = cur; if(!dfs(i)) return false; //注意不能單純地dfs,一定要判斷並return false;不判斷的話就會執行到最后一行並return true } } book[cur] = 1; return true; } void Kruskal(Edge ed){ e[ed.nodea][ed.nodeb] = 1; e[ed.nodeb][ed.nodea] = 1; for(int i=0;i<310;i++) book[i] = 0;//每放入一條邊,都要重新判斷是否構成環 bool state=1; for(int i=1;i<=n;i++){ if(book[i] != 0) continue; if(!dfs(i)){ state = 0;//有回路 break; } } if(state){ counte++; maxm = ed.weight; } else{ e[ed.nodea][ed.nodeb] = 0; e[ed.nodeb][ed.nodea] = 0; } } int main(){ int m,a,b,c; cin>>n>>m; for(int i=1;i<=m;i++){ cin>>a>>b>>c; edge[i].nodea = a; edge[i].nodeb = b; edge[i].weight = c; } sort(edge,edge+m,cmp); for(int i=1;i<=m;i++) { Kruskal(edge[i]); if(counte == n-1) break; } cout<<counte<<" "<<maxm<<endl; return 0; }
算法3:如果 邊數 + 連通分支數 - 1 >= 結點數,則圖中有回路
代碼實現:(沒有考慮有多個連通分支的情況,第二組測試數據通不過)20191206

#include<iostream> #define inf 999999 using namespace std; int book[101],e[101][101],n,data[101],sum,count,edge; bool is_cycle(int nodea,int nodeb){ //if edge>=the number of vertices, then there is a cycle int statea=0;//to record if nodea is booked before adding the edge int stateb=0; if(book[nodea]==0){ book[nodea] = 1; count++; statea++; } if(book[nodeb]==0){ book[nodeb] = 1; count++; stateb++; } edge++;//sequence(after count++) //after add the edge, if it leads to a cycle, then recover it if(edge>=count){ edge--; if(statea){ count--; book[nodea]=0; } if(stateb){ count--; book[nodeb]=0; } return true; } else return false; } void Kruskal(){ int minn = inf; int nodea,nodeb; for(int i=1;i<=n;i++){ for(int j=i+1;j<=n;j++){ if(e[i][j]<minn) { minn = e[i][j]; nodea = i; nodeb = j;//find the two vertices of the min edge } } } if(!is_cycle(nodea,nodeb)){ cout<<nodea<<" "<<nodeb<<endl; book[nodea] = 1; book[nodeb] = 1; sum += e[nodea][nodeb]; } e[nodea][nodeb] = inf; e[nodeb][nodea] = inf;//ignore the edge checked } int main(){ int i,j,m,a,b,c; scanf("%d %d",&n,&m); for(i=1;i<=n;i++){ for(j=1;j<=n;j++){ if(i==j) e[i][j]=0; else e[i][j]=inf; } } for(i=1;i<=m;i++){ scanf("%d %d %d",&a,&b,&c); e[a][b]=c; e[b][a]=c; } for(int i=0;i<m;i++) Kruskal(); cout<<sum<<endl; return 0; }
測試數據:

//左圖(AC) 6 9 1 2 10 2 4 5 1 6 2 2 3 3 4 5 11 6 5 3 3 5 15 4 6 10 1 4 20 //右圖(WA) 8 8 1 2 1 2 3 2 3 4 3 5 6 4 6 7 5 7 8 6 1 4 7 1 5 8