图论 —— 二分图 —— KM 算法
【原理】
KM 算法是用于求帶權二分圖的最優匹配的算法,其時間復雜度為 O(N^3)。
1.首先選擇頂點數較少的為 X 部(左點集),初始時對 X 部的每一個頂點設置頂標,頂標的值為該點關聯的最大邊的權值,Y 部(右點集)的頂點頂標為 0。
2.對于 X 部中的每個頂點,在相等子圖中利用匈牙利算法找一條增廣路徑,如果沒有找到,則修改頂標,擴大相等子圖,繼續找增廣路徑。
3.當 X 部的每個點都找到增廣路徑時,此時意味著每個點都在匹配中,即找到了該二分圖的完全匹配。該完全匹配即為二分圖的最優匹配。
【有關概念】
1)相等子圖:由于每個頂點有一個頂標,如果選擇邊權等于兩端點的頂標之和的邊,它們組成的圖稱為相等子圖。
2)頂標:每個點的頂標為該點關聯的最大邊的權值。
【頂標的修改】
如果從 X 部中的某個點 Xi 出發在相等子圖中沒有找到增廣路徑,則需要修改頂標。
如果沒有找到增廣路徑,則一定找到了許多條從 Xi 出發并結束于 X 部的匹配邊與未匹配邊交替出現的路徑,即交錯路。
將交錯路中 X 部的頂點頂標減去一個值 d,交錯路中屬于 Y 部的頂點頂標加上一個值 d,那么會發現:
- 兩端都在交錯路中的邊(i,j),其頂標和沒有變化,即:其原屬于相等子圖,現仍屬于相等子圖。
- 兩端都不在交錯路中的邊(i,j),其頂標也沒有變化,即:其原來屬于(或不屬于)相等子圖,現仍屬于(或不屬于)相等子圖。
- X 端不在交錯路中,Y 端在交錯路中的邊(i,j),其頂標和會增大,即:其原來不屬于相等子圖,現仍不屬于相等子圖。
- X 端在交錯路中,Y 端不在交錯路中的邊(i,j),其頂標和會減小,即:其原來不屬于相等子圖,現可能進入相等子圖,從而使相等子圖得到擴大。
修改頂標的目的就是要擴大相等子圖,為保證至少有一條邊進入相等子圖,可以在交錯路的邊中尋找頂標和與邊權之差最小的邊,也即前述的 d 值。
將交錯路中屬于 X 部的頂點減去 d,交錯路中屬于 Y 部的頂點加上 d,則可以保證至少有一條邊擴充進入相等子圖。
【相等子圖的性質】
1)任意時刻,相等子圖的 最大權匹配 ≤ 相等子圖的頂標和
2)任意時刻,相等子圖的 頂標和=所有頂點的頂標和
3)擴充相等子圖后,相等子圖的頂標和會減小
4)相等子圖的 最大匹配=原圖的完全匹配 時,匹配邊的權值和=所有頂點的頂標和,此匹配即為最優匹配
【實現】
1.最優匹配
#include<cstdio> #include<cstring> #include<cmath> #define INF 0x3f3f3f3f #define N 1001 int n,m;//x、y中結點個數,下標從1開始 int G[N][N];//邊權值矩陣 int Lx[N],Ly[N];//x、y中每個點的期望值 bool visX[N],visY[N];//標記左右點集是否已被訪問過 int linkX[N],linkY[N];//linkX[i]表示與X部中點i匹配的點,linkY[i]表示與Y部中點i匹配的點,-1時表示無匹配 bool dfs(int x){visX[x]=true;for(int y=1;y<=m;y++){if(!visY[y]){int temp=Lx[x]+Ly[y]-G[x][y];if(temp==0){//不在交替路中visY[y]=true;//放入交替路if(linkY[y]==-1 || dfs(linkY[y])){//如果是未匹配點,說明交替路是增廣路linkX[x]=y;//交換路徑linkY[y]=x;return true;//返回成功}}}}return false;//不存在增廣路 } void update(){int minn=INF;for(int i=1;i<=n;i++){//找出邊權與頂標和的最小的差值if(visX[i]){for(int j=1;j<=m;j++){if(!visY[j]){minn=min(minn,Lx[i]+Ly[j]-G[i][j]);}}}}for(int j=1;j<=n;j++)//將交錯路中X部的點的頂標減去minnif(visX[j])Lx[j]-=minn;for(int j=1;j<=m;j++)//將交錯路中Y部的點的頂標加上minnif(visY[j])Ly[j]+=minn; } int KM(){//更新理想值,納入更多的邊memset(linkX,-1,sizeof(linkX));memset(linkY,-1,sizeof(linkY));memset(Lx,0,sizeof(Lx));memset(Ly,0,sizeof(Ly));for(int i=1;i<=n;i++)//更新理想值for(int j=1;j<=m;j++)Lx[i]=max(Lx[i],G[i][j]);for(int i=1;i<=n;i++){while(true){memset(visX,false,sizeof(visX));memset(visY,false,sizeof(visY));if(dfs(i))break;elseupdate();}}int ans=0;for(int i=1;i<=n;i++)if(linkY[i]!=-1)//若存在邊ans+=G[linkY[i]][i];//統計邊權和return ans; }int main(){while(scanf("%d%d",&n,&m)!=EOF&&(n+m)){for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&G[i][j]);printf("%d\n",KM());}return 0; }2.邊權和最小的完全匹配
使用 KM 算法只能求二分圖的最優匹配,即邊權和最大的完全匹配,?這里有一個技巧,就是將所有的邊權取負,再進行 KM 算法,得到的解取負就是邊權和最小的完全匹配
假設存在一個最優解 res,是所有解中花費最小的,那么 -res 自然是所有花費中最大的解,當將所有邊權取負后,用 KM 算法得到的最優匹配必然是那個花費最大的解,取負后就是所需的最小邊權值的解
#include<cstdio> #include<cstring> #include<cmath> #define INF 0x3f3f3f3f #define N 1001 int n,m;//x、y中結點個數,下標從1開始 int G[N][N];//邊權值矩陣 int Lx[N],Ly[N];//x、y中每個點的期望值 bool visX[N],visY[N];//標記左右點集是否已被訪問過 int linkX[N],linkY[N];//linkX[i]表示與X部中點i匹配的點,linkY[i]表示與Y部中點i匹配的點,-1時表示無匹配 bool dfs(int x){visX[x]=true;for(int y=1;y<=m;y++){if(!visY[y]){int temp=Lx[x]+Ly[y]-G[x][y];if(temp==0){//不在交替路中visY[y]=true;//放入交替路if(linkY[y]==-1 || dfs(linkY[y])){//如果是未匹配點,說明交替路是增廣路linkX[x]=y;//交換路徑linkY[y]=x;return true;//返回成功}}}}return false;//不存在增廣路 } void update(){int minn=INF;for(int i=1;i<=n;i++){//找出邊權與頂標和的最小的差值if(visX[i]){for(int j=1;j<=m;j++){if(!visY[j]){minn=min(minn,Lx[i]+Ly[j]-G[i][j]);}}}}for(int j=1;j<=n;j++)//將交錯路中X部的點的頂標減去minnif(visX[j])Lx[j]-=minn;for(int j=1;j<=m;j++)//將交錯路中Y部的點的頂標加上minnif(visY[j])Ly[j]+=minn; } int KM(){//更新理想值,納入更多的邊memset(linkX,-1,sizeof(linkX));memset(linkY,-1,sizeof(linkY));memset(Lx,0,sizeof(Lx));memset(Ly,0,sizeof(Ly));for(int i=1;i<=n;i++)//更新理想值for(int j=1;j<=m;j++)Lx[i]=max(Lx[i],G[i][j]);for(int i=1;i<=n;i++){while(true){memset(visX,false,sizeof(visX));memset(visY,false,sizeof(visY));if(dfs(i))break;elseupdate();}}int ans=0;for(int i=1;i<=n;i++)if(linkY[i]!=-1)ans+=G[linkY[i]][i];return ans; }int main(){while(scanf("%d%d",&n,&m)!=EOF&&(n+m)){for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&G[i][j]);G[i][j]=-G[i][j];printf("%d\n",-KM());}return 0; }3.最小有向環覆蓋權值和
若原圖能由多個不相交的有向環覆蓋,那么二分圖一定存在完全匹配,比如:原圖中有向環為 1-2-3-1,則二分圖的完全匹配就是 1-2',2-3',3-1'
由于有向環覆蓋對應一個二分圖的完全匹配,該完全匹配的權值對應有向環覆蓋的權值,因此原圖權值最大的有向環匹配就是二分圖最優匹配的值
要求最小權值和,因此將邊的權值全部取負,再進行計算,最后結果再取負即可
#include<cstdio> #include<cstring> #include<cmath> #define INF 0x3f3f3f3f #define N 1001 int n,m; int G[N][N]; int Lx[N],Ly[N]; bool visX[N],visY[N]; int linkX[N],linkY[N]; bool dfs(int x){visX[x]=true;for(int y=1;y<=m;y++){if(!visY[y]){int temp=Lx[x]+Ly[y]-G[x][y];if(temp==0){visY[y]=true;if(linkY[y]==-1 || dfs(linkY[y])){linkX[x]=y;linkY[y]=x;return true;}}}}return false; } void update(){int minn=INF;for(int i=1;i<=n;i++)if(visX[i])for(int j=1;j<=m;j++)if(!visY[j])minn=min(minn,Lx[i]+Ly[j]-G[i][j]);for(int i=1;i<=n;i++)if(visX[i])Lx[i]-=minn;for(int i=1;i<=m;i++)if(visY[i])Ly[i]+=minn; } int KM(){memset(linkX,-1,sizeof(linkX));memset(linkY,-1,sizeof(linkY));for(int i=1;i<=n;i++){Lx[i]=Ly[i]=0;for(int j=1;j<=m;j++)Lx[i]=max(Lx[i],G[i][j]);}for(int i=1;i<=n;i++){while(true){memset(visX,false,sizeof(visX));memset(visY,false,sizeof(visY));if(dfs(i))break;elseupdate();}}int ans=0;for(int i=1;i<=m;i++){if(G[linkY[i]][i]==-INF){return 1;}ans+=G[linkY[i]][i];}return ans; } int main(){while(scanf("%d%d",&n,&m)!=EOF&&(n+m)){for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)G[i][j]=-INF;while(m--){int x,y,w;scanf("%d%d%d",&x,&y,&w);G[x][y]=max(G[x][y],-w);//處理重邊G[y][x]=G[x][y];//無向圖}int res=-KM();if(res==-1)printf("NO\n";elseprintf("%d\n",res);}return 0; }4.優先用原匹配邊構建的最優匹配
最優匹配可以直接使用 KM 模版,但是要在原匹配邊的基礎上使得改變的邊最少,可以進行如下的處理:
左邊點集有 n?個點,且 n<=m,則最優匹配必有 n 條邊,讓原圖中的每條邊的權值都乘以 (n+1),即擴大 n+1 倍,且若某邊本來就是原匹配用的其中一條邊,那么該邊權值在擴大 n+1 倍后,再加1。
因此任意一條邊的權值只能是 n+1 的倍數 或 (n+1 的倍數)+1,要在這種權值的邊中選出 n 條來,最終得到的最優權值和 ans? 除以 n+1,即為最優匹配解,因此就算是所有邊均使用原先的匹配,也即在所有權值的基礎上加了 n 個 1,此時除以 n+1,減去原匹配的值就是最優匹配比原匹配增長的值
如果在新二分圖中求出的權值和為 n+1 的倍數,則說明最優匹配中一條老邊都沒有復用。
綜上:所有邊權值*(n+1),老邊再 +1,最終?ans%(n+1) 就是復用舊邊的條數,ans/(n+1)-oldVal 就是最優匹配比原匹配增長的值。
#include<cstdio> #include<cstring> #include<cmath> #define INF 0x3f3f3f3f #define N 1001 int n,m; int G[N][N]; int Lx[N],Ly[N]; bool visX[N],visY[N]; int linkX[N],linkY[N]; bool dfs(int x){visX[x]=true;for(int y=1;y<=m;y++){if(!visY[y]){int temp=Lx[x]+Ly[y]-G[x][y];if(temp==0){visY[y]=true;if(linkY[y]==-1 || dfs(linkY[y])){linkX[x]=y;linkY[y]=x;return true;}}}}return false; } void update(){int minn=INF;for(int i=1;i<=n;i++)if(visX[i])for(int j=1;j<=m;j++)if(!visY[j])minn=min(minn,Lx[i]+Ly[j]-G[i][j]);for(int i=1;i<=n;i++)if(visX[i])Lx[i]-=minn;for(int i=1;i<=m;i++)if(visY[i])Ly[i]+=minn; } int KM(){memset(linkX,-1,sizeof(linkX));memset(linkY,-1,sizeof(linkY));for(int i=1;i<=n;i++){Lx[i]=Ly[i]=0;for(int j=1;j<=m;j++)Lx[i]=max(Lx[i],G[i][j]);}for(int i=1;i<=n;i++){while(true){memset(visX,false,sizeof(visX));memset(visY,false,sizeof(visY));if(dfs(i))break;elseupdate();}}int ans=0;for(int i=1;i<=m;i++)if(linkY[i]!=-1)ans+=G[linkY[i]][i];return ans; } int main(){while(scanf("%d%d",&n,&m)!=EOF&&(n+m)){for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){scanf("%d",&G[i][j]);G[i][j]=G[i][j]*(n+1);//每條邊乘以(n+1)}}int oldVal=0;//記錄原匹配權值和for(int i=1;i<=n;i++){int j;scanf("%d",&j);oldVal+=(G[i][j]/(n+1));//累計原匹配權值G[i][j]++;//老邊+1}int ans=KM();int v1=ans/(n+1);//最優匹配的權值和int v2=v1-oldVal;//最優匹配比原匹配相比多的權值數int v3=ans%(n+1);//最優匹配使用的老邊數int v4=n-v3;//最優匹配使用的新邊數printf("%d\n",v1);printf("%d\n",v2);printf("%d\n",v3);printf("%d\n",v4);}return 0; }?
總結
以上是生活随笔為你收集整理的图论 —— 二分图 —— KM 算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++语言基础 —— 顺序结构
- 下一篇: 数论 —— 欧拉函数