最佳匹配
什么是完美匹配
如果一個二分圖,X部和Y部的頂點數相等,若存在一個匹配包含X部與Y部的所有頂點,則稱為完美匹配。
換句話說:若二分圖X部的每一個頂點都與Y中的一個頂點匹配,**並且**Y部中的每一個頂點也與X部中的一個頂點匹配,則該匹配為完美匹配。
什么是完備匹配
如果一個二分圖,X部中的每一個頂點都與Y部中的一個頂點匹配,**或者**Y部中的每一個頂點也與X部中的一個頂點匹配,則該匹配為完備匹配。
什么是最佳匹配
帶權二分圖的權值最大的完備匹配稱為最佳匹配。
二分圖的最佳匹配不一定是二分圖的最大權匹配。
轉化
可以添加一些權值為0的邊,使得最佳匹配和最大權匹配統一起來。
KM算法
求二分圖的最佳匹配有一個非常優秀的算法,可以做到O(N^3),這就是KM算法。該算法描述如下:
1.首先選擇頂點數較少的為X部,初始時對X部的每一個頂點設置頂標,頂標的值為該點關聯的最大邊的權值,Y部的頂點頂標為0。
2.對於X部中的每個頂點,在相等子圖中利用匈牙利算法找一條增廣路徑,如果沒有找到,則修改頂標,擴大相等子圖,繼續找增廣路徑。當每個點都找到增廣路徑時,此時意味着每個點都在匹配中,即找到了二分圖的完備匹配。該完備匹配即為二分圖的最佳匹配。
什么是相等子圖呢?因為每個頂點有一個頂標,如果我們選擇邊權等於兩端點的頂標之和的邊,它們組成的圖稱為相等子圖。
如果從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。則可以保證至少有一條邊擴充進入相等子圖。
3.當X部的所有頂點都找到了增廣路徑后,則找到了完備匹配,此完備匹配即為最佳匹配。
相等子圖的若干性質
- 在任意時刻,相等子圖上的最大權匹配一定小於等於相等子圖的頂標和。
- 在任意時刻,相等子圖的頂標和即為所有頂點的頂標和。
- 擴充相等子圖后,相等子圖的頂標和將會減小。
- 當相等子圖的最大匹配為原圖的完備匹配時,匹配邊的權值和等於所有頂點的頂標和,此匹配即為最佳匹配
以上就是KM算法的基本思路。但是朴素的實現方法,時間復雜度為O(n4)——需要找O(n)次增廣路,每次增廣最多需要修改O(n)次頂標,每次修改頂 標時由於要枚舉邊來求d值,復雜度為O(n2)。實際上KM算法的復雜度是可以做到O(n3)的。我們給每個Y頂點一個“松弛量”函數slack,每次開 始找增廣路時初始化為無窮大。在尋找增廣路的過程中,檢查邊(i,j)時,如果它不在相等子圖中,則讓slack[j]變成原值與 A[i]+B[j]-w[i,j]的較小值。這樣,在修改頂標時,取所有不在交錯樹中的Y頂點的slack值中的最小值作為d值即可。但還要注意一點:修 改頂標后,要把所有不在交錯樹中的Y頂點的slack值都減去d
模板一,用全局變量minz表示邊權和頂標最小的差值,省去slack數組
1 #include<iostream> 2 #include<cstring> 3 #include<cstdio> 4 #include<vector> 5 #include<map> 6 using namespace std; 7 typedef long long ll; 8 const int maxn = 300 + 10; 9 const int INF = 0x3f3f3f3f; 10 11 int wx[maxn], wy[maxn];//每個點的頂標值(需要根據二分圖處理出來) 12 int cx[maxn], cy[maxn];//每個點所匹配的點 13 int visx[maxn], visy[maxn];//每個點是否加入增廣路 14 int cntx, cnty;//分別是X和Y的點數 15 int Map[maxn][maxn];//二分圖邊的權值 16 int minz;//邊權和頂標最小的差值 17 18 bool dfs(int u)//進入DFS的都是X部的點 19 { 20 visx[u] = 1;//標記進入增廣路 21 for(int v = 1; v <= cnty; v++) 22 { 23 if(!visy[v] && Map[u][v] != INF)//如果Y部的點還沒進入增廣路,並且存在路徑 24 { 25 int t = wx[u] + wy[v] - Map[u][v]; 26 if(t == 0)//t為0說明是相等子圖 27 { 28 visy[v] = 1;//加入增廣路 29 30 //如果Y部的點還未進行匹配 31 //或者已經進行了匹配,可以從原來的匹配反向找到增廣路 32 //那就可以進行匹配 33 if(cy[v] == -1 || dfs(cy[v])) 34 { 35 cx[u] = v; 36 cy[v] = u;//進行匹配 37 return 1; 38 } 39 } 40 else if(t > 0)//此處t一定是大於0,因為頂標之和一定>=邊權 41 { 42 minz = min(minz, t);//邊權和頂標最小的差值 43 } 44 } 45 } 46 return false; 47 } 48 49 int KM() 50 { 51 memset(cx, -1, sizeof(cx)); 52 memset(cy, -1, sizeof(cy)); 53 memset(wx, 0, sizeof(wx));//wx的頂標為該點連接的邊的最大權值 54 memset(wy, 0, sizeof(wy));//wy的頂標為0 55 for(int i = 1; i <= cntx; i++)//預處理出頂標值 56 { 57 for(int j = 1; j <= cnty; j++) 58 { 59 if(Map[i][j] == INF)continue; 60 wx[i] = max(wx[i], Map[i][j]); 61 } 62 } 63 for(int i = 1; i <= cntx; i++)//枚舉X部的點 64 { 65 while(1) 66 { 67 minz = INF; 68 memset(visx, 0, sizeof(visx)); 69 memset(visy, 0, sizeof(visy)); 70 if(dfs(i))break;//已經匹配正確 71 72 //還未匹配,將X部的頂標減去minz,Y部的頂標加上minz 73 for(int j = 1; j <= cntx; j++) 74 if(visx[j])wx[j] -= minz; 75 for(int j = 1; j <= cnty; j++) 76 if(visy[j])wy[j] += minz; 77 } 78 } 79 80 int ans = 0;//二分圖最優匹配權值 81 for(int i = 1; i <= cntx; i++) 82 if(cx[i] != -1)ans += Map[i][cx[i]]; 83 return ans; 84 } 85 int n, k; 86 int main() 87 { 88 while(scanf("%d", &n) != EOF) 89 { 90 for(int i = 1; i <= n; i++) 91 { 92 for(int j = 1; j <= n; j++) 93 scanf("%d", &Map[i][j]); 94 } 95 cntx = cnty = n; 96 printf("%d\n", KM()); 97 } 98 return 0; 99 }
模板二,用slack數組(對於完全圖的優化很快)
和全局變量不同的是,全局變量在每次while循環中都需要賦值成INF,每次求出的是所有點的最小值,而slack數組在每個while外面就初始化好,每次while循環slack數組的每個值都在用到,一次增廣路中求出的slack值會更准確,循環次數比全局變量更少
1 #include<iostream> 2 #include<cstring> 3 #include<cstdio> 4 #include<vector> 5 #include<map> 6 using namespace std; 7 typedef long long ll; 8 const int maxn = 300 + 10; 9 const int INF = 0x3f3f3f3f; 10 11 int wx[maxn], wy[maxn];//每個點的頂標值(需要根據二分圖處理出來) 12 int cx[maxn], cy[maxn];//每個點所匹配的點 13 int visx[maxn], visy[maxn];//每個點是否加入增廣路 14 int cntx, cnty;//分別是X和Y的點數 15 int Map[maxn][maxn];//二分圖邊的權值 16 int slack[maxn];//邊權和頂標最小的差值 17 18 bool dfs(int u)//進入DFS的都是X部的點 19 { 20 visx[u] = 1;//標記進入增廣路 21 for(int v = 1; v <= cnty; v++) 22 { 23 if(!visy[v] && Map[u][v] != INF)//如果Y部的點還沒進入增廣路,並且存在路徑 24 { 25 int t = wx[u] + wy[v] - Map[u][v]; 26 if(t == 0)//t為0說明是相等子圖 27 { 28 visy[v] = 1;//加入增廣路 29 30 //如果Y部的點還未進行匹配 31 //或者已經進行了匹配,可以從原來的匹配反向找到增廣路 32 //那就可以進行匹配 33 if(cy[v] == -1 || dfs(cy[v])) 34 { 35 cx[u] = v; 36 cy[v] = u;//進行匹配 37 return 1; 38 } 39 } 40 else if(t > 0)//此處t一定是大於0,因為頂標之和一定>=邊權 41 { 42 slack[v] = min(slack[v], t); 43 //slack[v]存的是Y部的點需要變成相等子圖頂標值最小增加多少 44 } 45 } 46 } 47 return false; 48 } 49 50 int KM() 51 { 52 memset(cx, -1, sizeof(cx)); 53 memset(cy, -1, sizeof(cy)); 54 memset(wx, 0, sizeof(wx));//wx的頂標為該點連接的邊的最大權值 55 memset(wy, 0, sizeof(wy));//wy的頂標為0 56 for(int i = 1; i <= cntx; i++)//預處理出頂標值 57 { 58 for(int j = 1; j <= cnty; j++) 59 { 60 if(Map[i][j] == INF)continue; 61 wx[i] = max(wx[i], Map[i][j]); 62 } 63 } 64 for(int i = 1; i <= cntx; i++)//枚舉X部的點 65 { 66 memset(slack, INF, sizeof(slack)); 67 while(1) 68 { 69 70 memset(visx, 0, sizeof(visx)); 71 memset(visy, 0, sizeof(visy)); 72 if(dfs(i))break;//已經匹配正確 73 74 75 int minz = INF; 76 for(int j = 1; j <= cnty; j++) 77 if(!visy[j] && minz > slack[j]) 78 //找出還沒經過的點中,需要變成相等子圖的最小額外增加的頂標值 79 minz = slack[j]; 80 //和全局變量不同的是,全局變量在每次while循環中都需要賦值成INF,每次求出的是所有點的最小值 81 //而slack數組在每個while外面就初始化好,每次while循環slack數組的每個值都在用到 82 //在一次增廣路中求出的slack值會更准確,循環次數比全局變量更少 83 84 85 //還未匹配,將X部的頂標減去minz,Y部的頂標加上minz 86 for(int j = 1; j <= cntx; j++) 87 if(visx[j])wx[j] -= minz; 88 for(int j = 1; j <= cnty; j++) 89 //修改頂標后,要把所有不在交錯樹中的Y頂點的slack值都減去minz 90 if(visy[j])wy[j] += minz; 91 else slack[j] -= minz; 92 } 93 } 94 95 int ans = 0;//二分圖最優匹配權值 96 for(int i = 1; i <= cntx; i++) 97 if(cx[i] != -1)ans += Map[i][cx[i]]; 98 return ans; 99 } 100 int n, k; 101 int main() 102 { 103 while(scanf("%d", &n) != EOF) 104 { 105 for(int i = 1; i <= n; i++) 106 { 107 for(int j = 1; j <= n; j++) 108 scanf("%d", &Map[i][j]); 109 } 110 cntx = cnty = n; 111 printf("%d\n", KM()); 112 } 113 return 0; 114 }