數缺形時少直觀,形缺數時難入微。
——華羅庚
序
最小生成樹問題是我在各項圖論問題中最先理解與解決的,其目的就是在連通圖中選擇出:
使得各點構成聯通的最小邊權的邊集
其中用到的資料結構與演算法也是相對很好理解的並查集和Kruskal演算法,我在我之前的文章小話資料結構-圖 (聚焦與於實現的理解)也有提到過,現在再來系統的闡述一下這問題的解決思路。
並查集
並查集是一種樹型的資料結構,用於處理一些不相交集合的合併及查詢問題。
並查集是一個寫法簡單,經常使用到的資料結構,主要操作有以下三種
初始化操作
int p[N]; //儲存每個點的祖宗節點 for (int i = 1; i <= n; i ++ ) p[i] = i;// 初始化,節點編號是1~n
查詢函式
int find( int x ){ if(p[x] != x) p[x] = find(p[x]); return p[x];//返回的是x的祖宗節點 }
合併操作
p[find(a)] = find(b);//將a加入b的祖宗的集合
並查集還可以維護每一個子集的大小、或是自子集到祖宗節點的距離,給出以下程式碼,只是使用Kruskal演算法只需要使用樸素的並查集就可以了。
int p[N], size[N];//p[]儲存每個點的祖宗節點, size[]表示祖宗節點所在集合中的點的數量 int find(int x){ if (p[x] != x) p[x] = find(p[x]); return p[x]; } for (int i = 1; i <= n; i ++ ){// 初始化,節點編號是1~n p[i] = i; size[i] = 1; } // 合併a和b所在的兩個集合並儲存集合中元素個數: size[find(b)] += size[find(a)]; p[find(a)] = find(b);
int p[N], d[N];//p[]儲存每個點的祖宗節點, d[x]儲存x到p[x]的距離 int find(int x){ if (p[x] != x){ int u = find(p[x]); d[x] += d[p[x]];//繼承偏移量 p[x] = u; } return p[x]; } for (int i = 1; i <= n; i ++ ){// 初始化,節點編號是1~n p[i] = i; d[i] = 0; } // 合併a和b所在的兩個集合: p[find(a)] = find(b); d[find(a)] = distance; // 初始化find(a)的偏移量
Kruskal演算法【O(mlogm)】
這個頂著一個高階名字的針對解決最小生成樹的演算法,也就是一個徹頭徹尾的貪心思想的演算法,基本的步驟如下
①:將所有邊按照權值從小到大排序
②:將所有邊依次放入圖中,如果沒有連入新的點,則丟棄不要。
③:當整個圖聯通時,返回結果
這裡給一張別人部落格裡非常直觀的動圖
在②步驟中,並查集就可以發揮出其作用,快速的判定出當前選擇的邊的點是否在一個集合中,從而方便的實現演算法。
那我們直接用程式碼來實現:
int n, m; int p[N]; struct Edge{ int a, b, w; }edges[M]; int find(int x){ if (p[x] != x) p[x] = find(p[x]); return p[x]; } int kruskal(){ sort(edges, edges + m);//排序 for (int i = 1; i <= n; i ++ ) p[i] = i; int res = 0, cnt = 0;//res記錄權值,cnt記錄已選擇的邊數 for (int i = 0; i < m; i ++ ){ int a = edges[i].a, b = edges[i].b, w = edges[i].w; a = find(a), b = find(b); if (a != b) {//將選擇的邊併入圖中 p[a] = b; res += w; cnt ++ ; } } if (cnt < n - 1) return INF;//若結束後不能使整個圖聯通,則無法求出結果 return res; }
至此,kruskal演算法就成功實現了,可以根據實際情況改變部分引數,從而獲得需要求解的部分。
希望我的拋磚引玉能引起更多的思考?!