第一節 鏢局運鏢-圖的最小生成樹
所謂最小生成樹,就是在一個具有N個頂點的帶權連通圖G中,如果存在某個子圖G',其包含了圖G中的所有頂點和一部分邊,且不形成迴路,並且子圖G'的各邊權值之和最小,則稱G'為圖G的最小生成樹。
最小生成樹的三個性質
- 最小生成樹不能有迴路
- 最小生成樹可能是一個,也可能有多個
- 最小生產樹的邊的個數等於頂點的個數減一
Kruskal 演算法(選邊法)
其核心思想是:首先按照邊的權值進行從小到大排序,每次從剩餘的邊中選擇權值較小且邊的兩個頂點不在同一個集合的邊(就是不會產生迴路的邊),加入到生產樹中,直到加入了n-1條邊為止。其實現是 貪心+並查集
#include <iostream> #include <vector> #include <algorithm> using namespace std; struct edge{ int u; int v; int w; edge(int uu =0, int vv= 0, int ww = 0):u(uu),v(vv),w(ww){} bool operator<(const edge& a) const{ return w < a.w; } }; vector<int> f; //建立並查集 void make_set(int n){ for(int i = 0 ; i <= n; ++ i) f.push_back(i); } //查詢並查集 int find_set(int x){ if(f[x] == x) return x; else{ f[x] = find_set(f[x]); return f[x]; } } //合併子集 bool union_set(int x, int y){ int t1 = find_set(x), t2 = find_set(y); if(t1!=t2){ f[t2] = t1; return true; } return false; } int main(){ int n,m; cin >> n >> m; vector<edge> e(m+1); for(int i = 1 ; i <= m; ++ i){ cin >> e[i].u >> e[i].v >> e[i].w; } sort(e.begin(),e.end()); int sum = 0, cnt = 0; make_set(n); for(int i = 1; i <= m ; ++ i){ if(union_set(e[i].u,e[i].v)){ cnt++; sum+=e[i].w; } if(cnt == n-1) break; } cout<<sum<<endl; }
n代表頂點數,m代表邊數,對邊的快速排序時間複雜度是O(MlogM),在m條邊中找出n-1條邊是O(MlogN),所以Krusal演算法時間複雜度為O(MlogM+MlogN),通常M大於N,因此最終時間複雜度為O(MlogM)。
第二節 再談最小生成樹
Prim演算法(選點法)
其核心思想是:首先選擇任意一個頂點加入生成樹中,接下來找出一條邊新增到生成樹中,這需要列舉每一樹頂點(已被選入生產樹的頂點)到每一個非樹頂點所有的邊,然後找出最短的邊加入到生成樹中。
#include <iostream> #include <vector> #include <algorithm> #define INF 100000 using namespace std; int main(){ int n,m; cin >> n >> m; vector<vector<int> > e(n,vector<int>(n,INF)); for(int i = 0 ; i < n; ++ i) e[i][i] = 0; for(int i = 0 ; i < m; ++ i){ int u,v,w; cin>> u >> v >> w; --u;--v; e[u][v] = w; e[v][u] = w; } vector<int> dist(e[0].begin(),e[0].end()); vector<bool> visit(n,false); visit[0 ] = true; int cnt = 1,sum = 0; while(cnt < n){ int minw = INF,index = 0; for(int i = 0; i < n ; ++ i){ if(!visit[i] && dist[i] < minw){ minw = dist[i]; index = i; } } visit[index] = true; cnt++; sum +=minw; for(int k = 0; k < n; ++ k){ if(!visit[k] && dist[k]> e[index][k]){ dist[k] = e[index][k]; } } } cout<<sum<<endl; }
上述Prim演算法如果使用鄰接矩陣來儲存圖的話,時間複雜度是O(N^2),觀察程式碼很容易發現,時間主要浪費在每次都要遍歷所有點找一個最小距離的頂點,對於這個操作,我們很容易想到用堆來優化,使得每次可以在log級別的時間找到距離最小的點,然後使用鄰接表儲存圖,整個演算法的時間複雜度會降到O(mlogn)
Kruskal演算法更適用於稀疏圖,沒有使用堆優化的Prim演算法適用於稠密圖,使用了堆優化的Prim演算法更適用於稀疏圖
第五節 二分圖最大匹配
二分圖的定義是:如果一個圖的所有頂點可以被分為X和Y兩個集合,並且所有邊的兩個頂點恰好一個屬於集合X,另一個屬於集合Y,即每個集合內的頂點沒有邊相連,則此圖就是 二分圖。
如何判斷一個圖是否為二分圖?首先將任意一個頂點著紅色,然後將其相鄰的頂點著藍色,如果按照這樣的著色方式可以將全部頂點著色的話,並且相鄰的頂點著色不同,那麼該圖就是二分圖。
增廣路:是一條路徑的起點和終點都是未配對的點
如果我們已經找到一種匹配方案,如何確定當前這個匹配方案已經是最大的匹配?如果在當前匹配方案下再也找不到增廣路,那麼當前匹配就是最大匹配
具體演算法步驟:
- 首先從任意一個未被配對的點u開始(一般從第1個點開始),從點u的邊中任意選擇一條邊(u->v)開始配對。
如果此時點v沒有配對成功,則配對成功,此時便找到增廣路。
如果此時點v已經配對,則遞迴深度搜尋尋找配對。如果尋找成功,則找到一條增廣路,此時需要更新原來的配對關係
- 如果剛才所選的邊配對失效,要從u的邊中重新選一條邊進行嘗試,直到u配對成功或者所有邊都被嘗試為止
- 接下來對剩餘的點配對,直到所有的點都嘗試完
- 輸出配對數
/* *匈牙利演算法 */ #include <iostream> #include <vector> #include <algorithm> using namespace std; bool dfs(int u, vector<bool>& visit, vector<int>& match, vector<vector<bool> >& e){ int n = visit.size()-1; for(int i = 1; i <= n; ++ i){ if(!visit[i] && e[u][i]){ visit[i] = true; if(!match[i] || dfs(match[i],visit,match,e)){ match[i] = u; match[u] = i; return true; } } } return false; } int main(){ int n,m; cin >> n >> m; vector<vector<bool> > e(n+1,vector<bool>(n+1,0)); for(int i = 0 ; i < m ; ++ i){ int u,v; cin >> u >> v; e[u][v] = true; e[v][u] = true; } int sum = 0; vector<int> match(n+1,0); for(int i =1 ; i <=n ; ++ i){ vector<bool> visit(n+1,false); if(dfs(i,visit,match,e)) sum++; //尋找增廣路徑,如果找到,配對數加1 } cout<<sum<<endl; }
如果二分圖有n個點,那麼最多找到n/2條增廣路徑。如果圖中共有m條邊,那麼每找到一條增廣路徑最多把所有邊遍歷一遍,所花得時間是m,
總時間複雜度是O(NM);