KM算法(運用篇)


傳送門:KM算法---理解篇

最佳匹配

什么是完美匹配

如果一個二分圖,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部的所有頂點都找到了增廣路徑后,則找到了完備匹配,此完備匹配即為最佳匹配。

相等子圖的若干性質

    1. 在任意時刻,相等子圖上的最大權匹配一定小於等於相等子圖的頂標和。
    2. 在任意時刻,相等子圖的頂標和即為所有頂點的頂標和。
    3. 擴充相等子圖后,相等子圖的頂標和將會減小。
    4. 當相等子圖的最大匹配為原圖的完備匹配時,匹配邊的權值和等於所有頂點的頂標和,此匹配即為最佳匹配

以上就是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 }

 


           

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM