图论 —— 二分图 —— 匈牙利算法
【基本概念】
1.交替路
從一個未匹配點出發,依次經過 非匹配邊、匹配邊、非匹配邊… 形成的路徑
2.增廣路
定義:設 M 為二分圖 G 已匹配邊的集合,若 P 是圖 G 中一條連通兩個未匹配點的路徑(起點在 X/Y?部,終點在 Y/X 部),且屬 M 的邊(匹配邊)與不屬 M 的邊(非匹配邊)在 P 上交替出現,則稱 P 為相對 M 的一條增廣路徑。
由于增廣路的第一條邊是沒有參與匹配的,第二條邊參與了匹配,...,最后一條邊沒有參與匹配,并且起點和終點還沒有被選擇過,顯然 P 有奇數條邊。
簡單來說,從一個未匹配點出發,走交替路,若途徑另一未匹配點(除起點外),則這條交替路稱為增廣路。
如下圖,左圖中的一條增廣路如右圖所示,圖中的匹配點均用紅色標出
3.增廣路性質
- P 的長度必為奇數,第一條邊和最后一條邊都不屬于 M,且兩個端點分屬兩個集合,均未匹配。
- P 的非匹配邊比匹配邊多一條。
- P 經過取反操作可以得到一個更大的匹配 M’。
- M 為 G 的最大匹配當且僅當不存在相對于 M 的增廣路徑。
4.增廣路定理
由于增廣路中間的匹配節點不存在其他相連的匹配邊,因此交換增廣路中的匹配邊與非匹配邊不會破壞匹配的性質。
由增廣路性質可知,只要把增廣路中的匹配邊和非匹配邊交換,交換后,圖中的匹配邊數目比原來多了 1 條。
故而,可以通過不停地找增廣路來增加匹配中的匹配邊和匹配點,找不到增廣路時,即達到最大匹配,這就是增廣路定理。
5.匈牙利樹
匈牙利樹一般由 DFS 構造(類似于 DFS 樹),其是從一個未匹配點出發進行?DFS(必須走交替路),直到不能再擴展為止。
如下圖,通過左側的二分圖,進行 DFS 可以得到右側的樹,但這棵樹存在一葉結點為非匹配點(7號),而匈牙利樹要求所有葉結點均為匹配點,故這棵樹不是匈牙利樹。
但若原圖中不含 7 號結點,那么從 2 號結點出發就會得到一棵匈牙利樹,如下圖
【匈牙利算法流程】
匈牙利算法是用增廣路來求最大匹配的算法,在求最大匹配前,需要先用 DFS 或 BFS 找到增廣路。
1.流程
1)若經過一個未匹配點,則尋找成功,更新路徑信息,匹配邊數 +1,停止搜索。
2)若一直沒有找到增廣路,則不再從這個點開始搜索。事實上,此時搜索后會形成一棵匈牙利樹。我們可以永久性地把它從圖中刪去,而不影響結果。
2.性能比較
無論是使用 DFS 還是 BFS,兩個版本的時間復雜度均為 O(V*E)。
DFS 的優點是思路清晰,代碼量少,但性能不如 BFS;BFS 的優點是速度較快,但代碼量大
對于稀疏圖,BFS 版本明顯快于 DFS 版本,對于稠密圖,兩者不相上下,因而當圖為稀疏圖時,常選用 DFS 版本,當圖為稠密圖時,常選用 BFS 版本。
3.koning 定理及推論
1)基本概念
k-正則圖:各頂點的度均為 k?的無向簡單圖
最大匹配數:最大匹配的匹配邊的數目
最大獨立集數:選取最多的點集,使點集中任意兩點均不相連
最小點覆蓋數:選取最少的點集,使任意一條邊都至少有一個端點在點集中
2)內容
- 最大匹配數 =?最小點覆蓋數
- 最大獨立集數 = 頂點數 - 最大匹配數
【實現】
1.最大匹配數/最小點覆蓋數
DFS 版本
int n,m;//x、y中結點個數,下標從0開始 bool vis[N];//vis[i]表示是否在交替路中 int link[N];//存儲連接點 vector<int> G[N];//存邊 bool dfs(int x){for(int i=0;i<G[x].size();i++){//對x的每個鄰接點int y=G[x][i];if(!vis[y]){//不在交替路中vis[y]=true;//放入交替路if(link[y]==-1 || dfs(link[y])){//如果是未匹配點,說明交替路是增廣路link[y]=x;//交換路徑return true;//返回成功}}}return false;//不存在增廣路,返回失敗 } int hungarian(){int ans=0;//記錄最大匹配數memset(link,-1,sizeof(link));for(int i=1;i<=n;i++){//從左側開始每個結點找一次增廣路memset(vis,false,sizeof(vis));if(dfs(i))//找到一條增廣路,形成一個新匹配ans++;}return ans; } int main(){while(scanf("%d%d",&n,&m)!=EOF){memset(link,-1,sizeof(link));//全部初始化為未匹配點for(int i=0;i<N;i++)G[i].clear();while(m--){int x,y;scanf("%d%d",&x,&y);G[x].push_back(y);G[y].push_back(x);}printf("%d\n", hungarian());//輸出最大匹配數 }return 0; }BFS 版本
int n,m;//左邊點數,右邊點數 int vis[N];//vis[i]表示是否在交替路中 int link[N];//存連接點 int pre[N];//存前驅結點 vector<int> G[N];//存邊 queue<int> Q; int hungarian(){memset(vis,-1,sizeof(vis));memset(pre,-1,sizeof(pre));memset(link,-1,sizeof(link));int ans=0;//記錄最大匹配數for(int i=1;i<=n;i++){if(link[i]==-1){//若點未匹配pre[i]=-1;//沒有前驅while(!Q.empty())//清空隊列Q.pop();Q.push(i);bool flag=false;while(!Q.empty() && !flag){int x=Q.front();for(int j=0;j<G[x].size();j++){//對x的每個鄰接點if(!flag)//如果falg為真,則說明找到一未匹配點,不必繼續下去break;int y=G[x][j];if(vis[y]!=i){//不在交替路中vis[y]=i;//存入交替路Q.push(link[y]);//交換路徑if(link[y]>=0)//在已匹配點中pre[link[y]]=x;else {//找到未匹配點,交替路變增廣路flag=true;int d=x;int e=y;while(d!=-1){//找到一個未匹配點,無法構成匈牙利樹,讓所有點不斷的往回更新,重選下一個int temp=link[d];link[d]=e;link[e]=d;d=pre[d];e=temp;}}}}Q.pop();}if(link[i]!=-1)//統計最大匹配數ans++;}}return ans; } int main(){while(scanf("%d%d",&n,&m)!=EOF){while(m--){int x,y;scanf("%d%d",&x,&y);G[x].push_back(y);G[y].push_back(x);}printf("%d\n", hungarian());//輸出最大匹配數}return 0; }2.最大獨立集
int n,m; bool vis[N]; int link[N]; bool G[N][N]; bool dfs(int x){for(int y=1;y<=m;y++){if(G[x][y]&&!vis[y]){vis[y]=true;if(link[y]==-1 || dfs(link[y])){link[y]=x;return true;}}}return false; } int hungarian(){int ans=0;for(int i=1;i<=n;i++){memset(vis,false,sizeof(vis));if(dfs(i))ans++;}return ans; } int main(){while(scanf("%d%d",&n,&m)!=EOF&&(n+m)){memset(link,-1,sizeof(link));memset(G,true,sizeof(G));while(m--){int x,y;scanf("%d%d",&x,&y);G[x][y]=false;//不滿足條件則連一條邊}int mate=hungarian();//最大匹配數int res=n-mate;//最大獨立集printf("%d\n",res);}return 0; }3.經典應用——行列拆點建圖
1)問題
對于一個 n*m 的圖," . " 代表空白區域," # " 代表墻,現在要向空白區域內放棋子,要求同一行、同一列只能放一個棋子,除非該行/列上有墻阻隔,問最多能放多少枚棋子
如下圖,對于一個 4*4 的圖,黑色代表墻,最多能放 5 枚棋子
2)解答
只有在墻的阻隔情況下,才會出現一行/列出現多個點的情況,那么可以考慮進行縮點,將同一行且沒有墻體阻隔的區域縮成一個點,放到左點集中,將同一列且沒有墻體阻隔的區域縮成一個點,放到右點集中,從而建成一個二分圖
假設 i 為行編號,j 為列編號,若 i-j 之間存在一條邊,就相當于在方格 (i,j) 上放了一個點,這個假設使得在沒有墻體阻隔的情況下,i 行 j 列不能再放其他的點,那么在不考慮 不能同行同列 的情況下,將所有邊連接起來,即行列縮點后,對應方格編號連邊?
建好圖后,在圖上求最大匹配即可
3)實現
int n,m;//n行m列 bool vis[N];//vis[i]表示是否在交替路中 int link[N];//存儲連接點 int G[N][N];//存邊 char str[N][N]; int x[N][N],cntX;//行點集 int y[N][N],cntY;//列點集 bool dfs(int x){for(int y=0;y<cntY;y++){//對x的每個鄰接點if(G[x][y]==1&&!vis[y]){//不在交替路中vis[y]=true;//放入交替路if(link[y]==-1 || dfs(link[y])){//如果是未匹配點,說明交替路是增廣路link[y]=x;//交換路徑return true;//返回成功}}}return false;//不存在增廣路,返回失敗 } int hungarian(){int ans=0;//記錄最大匹配數memset(link,-1,sizeof(link));for(int i=0;i<cntX;i++){//從左側開始每個結點找一次增廣路memset(vis,false,sizeof(vis));if(dfs(i))//找到一條增廣路,形成一個新匹配ans++;}return ans; } int main(){while(scanf("%d%d",&n,&m)!=EOF){memset(x,0,sizeof(x));memset(y,0,sizeof(y));memset(G,false,sizeof(G));for(int i=0;i<n;i++)scanf("%s",str[i]);//對行縮點cntX=1;for(int i=0;i<n;i++){//第i行for(int j=0;j<m;j++){//第j列if(str[i][j]=='.')//同一區域x[i][j]=cntX;if(str[i][j]=='X')//墻體阻隔cntX++;}cntX++;//下一行}//對列縮點cntY=1;for(int j=0;j<m;j++){//第j列for(int i=0;i<n;i++){//第i行if(str[i][j]=='.')//同一區域y[i][j]=cntY;if(str[i][j]=='X')//墻體阻隔cntY++;}cntY++;//下一列}//連邊for(int i=0;i<n;i++)for(int j=0;j<m;j++)if(str[i][j]=='.')G[x[i][j]][y[i][j]]=true;printf("%d\n",hungarian());}return 0; }?
新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!總結
以上是生活随笔為你收集整理的图论 —— 二分图 —— 匈牙利算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 树形结构 —— 树与二叉树 —— 树的数
- 下一篇: 欧拉定理(洛谷-P5091)(十进制快速