關於KM算法及其延伸的一些解題研究
KM算法作爲一種簡潔明瞭的算法,巧妙地解決了帶權二分圖在特定條件(帶權最大匹配一定是完備匹配的圖)下的求解問題,規避了網絡流費用流算法較爲繁瑣的代碼,在信息學競賽具有相當的地位。爲增加本文的可讀性,筆者已經將輸入輸出和初始化的代碼隱去。
題目描述
倉庫管理員M最近一直很煩惱,因爲他的上司給了他一個艱難的任務:讓他儘快想出一種合理的方案,把公司的倉庫整理好。已知公司共有n個倉庫和n種貨物,由於公司進貨時沒能很好的歸好類,使得大部分的倉庫裏面同時裝有多種貨物,這就給搬運工作人員搬運貨物時帶來了很多的麻煩。倉庫管理員M的任務就是設計一種合理的方案,把倉庫裏面的貨物重新整理,把相同的貨物放到同一個倉庫,以便於日後的管理,在整理過程中肯定需要把某些貨物從一個倉庫搬運到另一個倉庫,已知每一次搬運貨物所付出的代價等於搬運該貨物的重量。
編程任務:
請你幫助倉庫管理員M設計搬運方案,使得把所有的貨物歸好類:使每種貨物各自佔用一個倉庫,或者說每個倉庫裏只能放一種貨物。同時要求搬運貨物時所付出的所有的總的代價最小。
輸入格式
第一行爲n (1 <= n <= 150),倉庫的數量。
以下爲倉庫貨物的情況。第i+1行依次爲第i個倉庫中n種貨物的數量x(0 <= x <= 100)。
輸出格式
把所有的貨物按要求整理好所需的總的最小代價。
輸入輸出樣例
輸入
4
62 41 86 94
73 58 11 12
69 93 89 88
81 40 69 13
輸出
650
說明/提示
樣例說明:方案是:第1種貨物放到倉庫2中;第2種貨物放到倉庫3中;第3種貨物放到倉庫4中;第4種貨物放到倉庫1中
首先給出筆者解題代碼。定義等略去。
bool dfs(int x)
{
va[x]=1;
for(int i=1;i<=n;i++)
{
if(!vb[i])
{
if((la[x]+lb[i])==w[x][i])
{
vb[i]=1;
if(!match[i]||dfs(match[i]))
{
match[i]=x;
return true;
}
}
else delta=min(delta,la[x]+lb[i]-w[x][i]);
}
}
return false;
}
int KM()
{
for(int i=1;i<=n;i++)
{
la[i]=-(1<<30);
lb[i]=0;
for(int j=1;j<=n;j++)
{
la[i]=max(la[i],w[i][j]);
}
}
for(int i=1;i<=n;i++)
{
while(true)
{
memset(va,0,sizeof(va));
memset(vb,0,sizeof(vb));
delta=(1<<30);
if(dfs(i))break;
for(int j=1;j<=n;j++)
{
if(va[j])la[j]-=delta;
if(vb[j])lb[j]+=delta;
}
}
}
int ans=0;
for(int i=1;i<=n;i++)
{
ans-=w[match[i]][i];
}
return ans;
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
store[i][j]=read();
ll[j]+=store[i][j];
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
w[i][j]=-(ll[j]-store[i][j]);
}
cout<<KM();
return 0;
}
本題幾乎可稱爲KM算法的標準模板,爲了進一步介紹及討論首先引入交錯樹的概念(默認讀者已掌握匈牙利算法的操作方式以及相關術語)
其實也很好理解,就是在一般的匈牙利算法中,從一個左部點出發嘗試進行匹配,如果失敗,那他所有訪問過的節點以及爲了訪問這些節點所經過的邊共同構成交錯樹。(該樹根節點爲左部點,葉子也是左部點)
之後是頂標,這個概念爲相等子圖而服務。 所謂相等子圖,就是二分圖中所有節點滿足Ai+Bi=W(i,j)的邊所構成的子圖。 而頂標,就是這裏的Ai,Bi。實際操作時,將左部點的頂標賦值爲Ai,右部點賦值爲Bi。
之後我們就可以引入定理
“若相等子圖中存在完備匹配,則這個完備匹配就是二分圖的帶權最大匹配。”
根據以上定理我們可以推知,我們要做的就是採取一個適當的策略擴大相等子圖的規模,直到它存在完備匹配。
for(int i=1;i<=n;i++)
{
la[i]=-(1<<30);
lb[i]=0;
for(int j=1;j<=n;j++)
{
la[i]=max(la[i],w[i][j]);
}
}
在這裏我們將所有的Ai賦值爲其所連邊中權值最大者,Bi賦值爲0。此時他必然滿足滿足Ai+Bj>=W(i,j)。(事實上,也存在其他賦值方法,本質上來講,只需滿足對於任意i,j有Ai+Bj>=W(I,j)即可,這裏採用的是較爲簡便的賦值方法)
for(int i=1;i<=n;i++)
{
while(true)
{
memset(va,0,sizeof(va));
memset(vb,0,sizeof(vb));
delta=(1<<30);
if(dfs(i))break;
for(int j=1;j<=n;j++)
{
if(va[j])la[j]-=delta;
if(vb[j])lb[j]+=delta;
}
}
}
我們對每一個左部節點進行匹配(使用while(true)不達目的不罷休)如果dfs(i)==1,即這個節點可以匹配,那就不管他。否則修改頂標(失敗的交錯樹中的頂標)。 這裏有一點需要注意,即修改頂標後,再一次進行dfs時,原來並非相等子圖中的邊,現在就有可能成爲相等子圖中的邊。而且由於減去的delta最終求得結果爲“即將加入的邊還比要求多多少”故可保證每次均有>=1條邊進入相等子圖,從概率的角度來講,進一步提高了左部點完成匹配的可能。
以上便是該算法的核心思路,重複上述過程直到達成條件即可(所有左部點均匹配成功)。
結語:KM算法在處理最大帶權二分圖的特殊情況時有一定的優越性,當同樣也具有較大的侷限性,掌握KM算法後,一定不能滿足於只解決特定情況下的問題,也還要積極探索更爲高級的算法,解決更爲普適性的問題。事實上,理解了二分圖慣用的匈牙利算法和KM算法,對費用流的理解也會有所幫助。
感謝洛谷平臺提供的題面及測試數據。