title: KM算法原理+證明
date: 2020-04-26
categories: ["算法"]
summary: "以匈牙利算法為基礎,改善后用於求解帶權二分圖的求最佳匹配問題。百度百科中有KM算法的介紹,當中有證明過程:[百度KM算法]"
author: White Song
tags: ["二分圖"]
cover: https://img.yilon.top/blog/czsh9.png
blog: https://blog.yilon.top
介紹
以匈牙利算法
為基礎,改善后用於求解帶權二分圖的求最佳匹配問題
百度百科中有KM算法的介紹,當中有證明過程:百度KM算法
最大權匹配
帶權二分圖的邊權重和最大的匹配,如下圖所示,最大和為102
最佳匹配
帶權二分圖的邊權重和最大的完備匹配,如下圖所示
顯然,最佳匹配和最大權匹配並不一致,但如果把剩下的邊補上,並且設置權重為零,那么二者可以統一起來
KM算法
下面講的KM算法是用來求最佳匹配,而非最大權匹配。
KM算法是一種計算機算法,功能是求完備匹配下的最大權匹配。
如果不存在完備匹配,那么算法會求最大匹配,如果最大匹配有多種,那么結果是最大匹配中權重和最大的。
在一個二分圖內,左頂點為X,右頂點為Y,現對於每組左右連接XiYj有權,求最大匹配,並且使得該匹配中所有的和是最大。
該算法是通過給每個頂點一個標號(叫做頂標)來把求最大權匹配的問題轉化為求完備匹配的問題的。
設頂點的頂標為的頂標為,頂點的頂標為,頂點與之間的邊權為,在算法執行過程中,對於圖中的任意一條邊,始終成立。
相等子圖
G當中每一條邊有左右兩個頂標,*相等子圖*就是那些頂標和等於邊權重的邊構成的子圖,如下圖綠色加粗線構成相等子圖
KM算法的正確性基於以下的定理:
定理
:若二分圖中,,並且存在某個相等子圖有完備匹配,那么這個完備匹配就是二分圖的最大權匹配。
證明:因為這個完備匹配存在於相等子圖中,因此,這個匹配所有邊都滿足:,同時由於完備匹配包含所有的頂點,因此這個屬於相等子圖的完備匹配的總權重等於所有頂標的和。
如果這個二分圖存在另外一個完備匹配,如果它不完全屬於相等子圖,即存在某條邊,那么該匹配的權重和就小於所有頂標的和,即小於上述屬於相等子圖的完備匹配的權重和。
算法過程
首先選擇頂點數較少的為X部,初始時對X部的每一個頂點設置頂標,頂標的值為該點關聯的最大邊的權值,Y部的頂點頂標為0。
對於X部中的每個頂點,在相等子圖中利用匈牙利算法找一條增廣路徑,如果沒有找到,則修改頂標,擴大相等子圖,繼續找增廣路徑。當每個點都找到增廣路徑時,此時意味着每個點都在匹配中,即找到了二分圖的完備匹配。該完備匹配是最大權重的完備匹配,即為二分圖的最佳匹配。
匈牙利算法對左邊第一個頂點,在相等子圖中進行增廣路徑搜索,找到路徑1-C后進行匹配增廣操作,如下圖所示
回答第一個問題,需要理解如何在保持相等子圖原來的邊符合相等子圖要求的同時,讓新加的邊也滿足相等子圖的要求。
那么在增廣路徑搜索時,我們知道,如果下面這些紫色邊任意一條加入相等子圖后,都可以在相等子圖中使用匈牙利算法找到一條增廣路徑2-A(or 2-B or 2-C-1-A):
KM算法選擇上述三條紫色邊中,頂標和與邊權重差值最小的邊1-A或者2-A,以該最小差值為d
這里有一個問題就是為什么最小那個?首先比這更小了就不能擴大相等子圖了,但是如果大了,就不能保證總是成立了。比如上圖選擇了2-B邊的差值2,那么修改頂標值后,1-A有
。
而KM算法中需要修改的頂標,是那些在匈牙利算法增廣路徑搜索時,產生一棵交錯樹,為了保證總是成立,交錯樹上所有的頂標都要參與修改。
比如在如上第二個頂點搜索增廣路徑時,產生如下圖所示的橙色頂標集合{1,2,C}:
修改頂標后產生如下圖所示的結果:
在該相等子圖上以頂點2為開始點,搜索增廣路徑2-A(or 2-C-1-A),進行增廣操作:
同樣對左邊第三個點:
另外一個問題是為什么修改橙色頂標而不去修改頂標A(找到最小差對應的邊的右邊頂標)?修改頂標A的值為-1,那么邊1-A也可以加入相等子圖了。問題就在於能不能保證總是成立,如下圖所示結果,修改頂標A,邊3-A就不滿足該條件了。除非在修改頂標A的同時,增加頂標3的值,但是需要修改的頂標集合需要額外的搜索算法,而修改橙色頂標所需要的交錯樹在增廣路徑搜索時可以一並產生。
C++代碼
#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#include<map>
#include<stdlib.h>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn = 3;
const int INF = 0x3f3f3f3f;
int wx[maxn], wy[maxn];//每個點的頂標值(需要根據二分圖處理出來)
int cx[maxn], cy[maxn];//每個點所匹配的點
int visx[maxn], visy[maxn];//每個點是否加入增廣路
int cntx=maxn, cnty=maxn;//分別是X和Y的點數
//int Map[maxn][maxn] = { {3,INF,100},{2,1,3 },{INF,INF,5} };//二分圖邊的權值
int Map[maxn][maxn] = { {3,0,100},{2,1,3 },{0,0,5} };//二分圖邊的權值
int minz;//邊權和頂標最小的差值
bool dfs(int u)//進入DFS的都是X部的點
{
visx[u] = 1;//標記進入增廣路
for (int v = 0; v < cnty; v++)
{
if (!visy[v] && Map[u][v] != INF)//如果Y部的點還沒進入增廣路,並且存在路徑
{
int t = wx[u] + wy[v] - Map[u][v];
if (t == 0)//t為0說明是相等子圖
{
visy[v] = 1;//加入增廣路
//如果Y部的點還未進行匹配
//或者已經進行了匹配,但是可以從v點原來匹配的cy[v]找到一條增廣路
//因為visy[v] = 1;因此從if (!visy[v] && Map[u][v] != INF) 中可以看出下一個找到的cy[v]-v'連接肯定不在已經匹配的邊集合M中
//因為當v'=v時if不滿足!visy[v]=true
//這保證了所找到的路徑是屬於M和不屬於M的邊交替出現
//這時,下面遞歸的cx[u] = v; 和cy[v] =u 就可以對路徑上的邊取反,達到增廣的效果
//這是DFS的算法
//那就可以進行匹配
if (cy[v] == -1 || dfs(cy[v]))
{
cx[u] = v;
cy[v] = u;//進行匹配
return true;
}
}
else if (t > 0)//此處t一定是大於0,因為頂標之和一定>=邊權
{
minz = min(minz, t);//邊權和頂標最小的差值
}
}
}
return false;
}
int KM()
{
memset(cx, -1, sizeof(cx));
memset(cy, -1, sizeof(cy));
memset(wx, 0, sizeof(wx));//wx的頂標為該點連接的邊的最大權值
memset(wy, 0, sizeof(wy));//wy的頂標為0
for (int i = 0; i < cntx; i++)//預處理出頂標值
{
for (int j = 0; j < cnty; j++)
{
if (Map[i][j] == INF) {
continue;
}
wx[i] = max(wx[i], Map[i][j]);
}
}
for (int i = 0; i < cntx; i++)//枚舉X部的點
{
while (1)
{
minz = INF;
memset(visx, 0, sizeof(visx));
memset(visy, 0, sizeof(visy));
if (dfs(i))break;//已經匹配正確
//還未匹配,將X部的頂標減去minz,Y部的頂標加上minz
for (int j = 0; j < cntx; j++)
if (visx[j])wx[j] -= minz;
for (int j = 0; j < cnty; j++)
if (visy[j])wy[j] += minz;
}
}
int ans = 0;//二分圖最優匹配權值
for (int i = 0; i < cntx; i++)
if (cx[i] != -1)ans += Map[i][cx[i]];
return ans;
}
int n, k;
int main()
{
int result = KM();
return 0;
}