本文塞得很滿(!),如有錯誤歡迎指出~
Upd 2020-07-29:(KM)還以為是板子錯了,后來才發現是HDU2853題目里兩個集合的數量不同,而之前寫的題目兩個集合都是相同的就沒改動板子。現已把該題目加入本文中!
二分圖及其經典匹配問題
簡介
二分圖又稱作二部圖,是圖論中的一種特殊模型。 設\(G=(V,E)\)是一個無向圖,如果頂點\(V\)可分割為兩個互不相交的子集\((A,B)\),並且圖中的每條邊\((i,j)\)所關聯的兩個頂點\(i\)和\(j\)分別屬於這兩個不同的頂點集\((i\ in\ A,j\ in\ B)\),則稱圖\(G\)為一個二分圖。 ——百度百科 - 《二分圖》
簡單來說就是這張圖中共有兩個集合\(U、V\),圖上的點屬於兩集合其一。相同集合中的點之間沒有互相關系,不同集合的點可能會有關系(連線)。故這種圖經常被視為夫妻匹配圖(emm某些特殊關系在這就當作沒有吧)。如下圖所示
二分圖最大匹配
二分圖最大匹配,顧名思義就是一個點最多只能有與其有關系的一條邊被選中,問最多能選擇多少條邊
換作夫妻匹配問題,就是問在一夫一妻制下最多能找到多少對海王夫妻
著名的解決二分圖最大匹配問題的算法為匈牙利算法,也可以借助最大流/最小割模型解決這類問題
二分圖最大權完美匹配
二分圖最大權完美匹配,表示此時的二分圖的邊是帶有邊權的
與二分圖最大匹配不同的是,最大權完美匹配側重於最大權值,要求在保證一個點最多只能有與其有關系的一條邊被選中的前提下,選出的邊的邊權總和最大
換做夫妻匹配問題,同樣以一夫一妻制為背景,但此時男女生之間存在一種叫好感度的數值,要求好感度總和最大
著名的解決二分圖最大權完美匹配問題的算法為KM算法
匈牙利(Hungary)算法
引入
匈牙利算法是基於深度優先搜索一遍一遍搜索增廣路的存在性來增加匹配對數的
我們將問題看作相親現場,每個\(U\)集合的人排隊尋找\(V\)集合里的對象
假設是男生排隊找女生,基於這個"時間"順序,假設前面的男生已經匹配了一些
下一個男生\(a\)進來匹配,算法便會遍歷一遍他認識的女生(與他有關系的\(V\)集內的點)
如果發現當前遍歷到的女生還沒有被其他男生匹配,那非常好,就直接把這個女生匹配給男生\(a\)
如果發現已經被其他男生\(b\)給綠匹配了,那算法會嘗試去和那個男生\(b\)溝通,詢問能否讓他換一個(?)
算法便會來到那個男生\(b\)那里,重新遍歷一遍他認識的女生,看看能否找到其他能夠匹配的女生(尋找增廣路/套娃)
如果可以,那么男生\(b\)便會與新找到的女生匹配,順利成章的,原來與男生\(b\)匹配的女生就可以和男生\(a\)匹配啦
如果不行,那么男生\(a\)就沒這個機會了QAQ,嘗試下一個吧(繼續遍歷),實在不行(遍歷完了)單着挺好的
就以這樣的方法一直搜索,直到所有男生(\(U\)集)該匹配的都匹配完了,就能得到最大匹配數了
(但是這樣找女朋友真的好嗎qwq,好的話給我也整一個)
代碼實現
下面使用\(vector\ G\)來儲存每個男生認識的女生
使用\(int\ match\)來儲存每個女生當前匹配的男生的編號(沒匹配則為\(0\))
如上面所說,我們需要遍歷每個\(U\)集的男生去嘗試匹配
對於每個男生\(p\),要遍歷一遍他認識的女生
for(int i:G[p])
此時女生的編號便以\(i\)來表示
如果當前的女生沒被匹配到,自然可以直接讓她與當前的男生\(p\)進行匹配
但如果已經被匹配過,則嘗試讓匹配她的那個男生去再找找看其他女生(開始套娃),如果返回\(true\)也可以正常匹配
for(int i:G[p])
{
if(!match[i]||hungary(match[i]))
{
match[i]=p;
return true;
}
}
那么對於這個套娃應該如何處理呢
每次搜索時都假設自己沒匹配到,那么上面的代碼可以滿足搜索的功能
但是可以發現一個問題,從第二層尋找增廣的搜索開始,\(match\)數組的值是沒有變的(也就是說在尋找到新的女生之前,第一層遍歷到的那個女生的\(match\)依舊是第二層的這個男生)
第二層這個男生一定還會再找到這個女生,於是便會開始無限套娃死循環\(Runtime\ Error\)
所以我們需要新開一個數組\(vis\)來儲存當前這一遍搜索是否已經讓某個男生找過增廣路了(找過了就不用再找了反正也找不到,正在找的話就直接退出防止套娃)
至此這個搜索函數算是完整了
bool hungary(int p)
{
if(vis[p]) //防止套娃
return false;
vis[p]=true; //訪問標記
for(int i:G[p])
{
if(!match[i]||hungary(match[i]))
{
match[i]=p;
return true; //海王在世
}
}
return false; //單身貴族
}
但是先這樣使用\(bool\ vis\)的話有個弊端,也就是每次搜索某個男生時都要清空一次,時間耗費總和挺大的
所以我們可以在傳值的時候再加個趟數\(op\),表示這是第幾趟匹配了(或者表示這一趟主要是為了讓哪個男生找女生),這樣使用\(int\ vis\)便可以做到不清空解決問題
bool hungary(int p,int op) //新增傳值
{
if(vis[p]==op) //依據趟數防止套娃
return false;
vis[p]=op; //標記這一趟是否訪問過
for(int i:G[p])
{
if(!match[i]||hungary(match[i],op))
{
match[i]=p;
return true;
}
}
return false;
}
完整程序
時間復雜度為\(O(nm)\)
#include<bits/stdc++.h>
using namespace std;
const int N=555;
vector<int> G[N];
int match[N],vis[N];
bool used[N][N];
bool hungary(int p,int op)
{
if(vis[p]==op)
return false;
vis[p]=op;
for(int i:G[p])
{
if(!match[i]||hungary(match[i],op))
{
match[i]=p;
return true;
}
}
return false;
}
int main()
{
int n,m,e,a,b;
scanf("%d%d%d",&n,&m,&e);
while(e--)
{
scanf("%d%d",&a,&b);
if(used[a][b]) //判重邊
continue;
used[a][b]=true;
G[a].push_back(b);
}
int ans=0;
for(int i=1;i<=n;i++)
if(hungary(i,i))
ans++;
printf("%d\n",ans);
return 0;
}
Dinic最小割 / 最大流
建圖法
由於一個點最多只能匹配到一條邊
每條邊又表示着兩點間存在關系
所以可以建立一個超級源點\(S\)以及超級匯點\(T\)
將\(U\)集與\(V\)集分散在兩列(也就是上面的二分圖圖示)
\(S\)與\(U\)集合每個點建立一條邊,流量為\(1\),表示每個人最多只能被\(1\)條邊匹配到
同樣的,\(V\)集合每個點與\(T\)建立一條邊,流量為\(1\)
最后,如果\(U\)集中點\(a\)與\(V\)集中點\(b\)之間存在關系(可匹配性)
就建立一條\(a\)至\(b\)的邊,流量也為\(1\)
這樣就相當於增廣路的條數就是二分圖的最大匹配
完整程序
時間復雜度應該為\(O(nm^2)\)
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=510;
const int maxm=50050;
struct edge{
int u,v,cap,flow;
edge(){}
edge(int u,int v,int cap,int flow):u(u),v(v),cap(cap),flow(flow){}
}eg[maxm<<1];
int tot,s,t,dis[maxn<<1],cur[maxn<<1];
vector<int> tab[maxn<<1];
void addedge(int u,int v,int cap)
{
tab[u].push_back(tot);
eg[tot++]=edge(u,v,cap,0);
tab[v].push_back(tot);
eg[tot++]=edge(v,u,0,0);
}
int bfs()
{
queue<int> q;
q.push(s);
memset(dis,INF,sizeof dis);
dis[s]=0;
while(!q.empty())
{
int h=q.front(),i;
q.pop();
for(i=0;i<tab[h].size();i++)
{
edge &e=eg[tab[h][i]];
if(e.cap>e.flow&&dis[e.v]==INF)
{
dis[e.v]=dis[h]+1;
q.push(e.v);
}
}
}
return dis[t]<INF;
}
int dfs(int x,int maxflow)
{
if(x==t||maxflow==0)
return maxflow;
int flow=0,i,f;
for(i=cur[x];i<tab[x].size();i++)
{
cur[x]=i;
edge &e=eg[tab[x][i]];
if(dis[e.v]==dis[x]+1&&(f=dfs(e.v,min(maxflow,e.cap-e.flow)))>0)
{
e.flow+=f;
eg[tab[x][i]^1].flow-=f;
flow+=f;
maxflow-=f;
if(maxflow==0)
break;
}
}
return flow;
}
int dinic()
{
int flow=0;
while(bfs())
{
memset(cur,0,sizeof(cur));
flow+=dfs(s,INF);
}
return flow;
}
bool vis[555][555];
int main()
{
int n,m,e,a,b;
scanf("%d%d%d",&n,&m,&e);
tot=0,s=1001,t=1002;
for(int i=1;i<=n;i++)
addedge(s,i,1);
for(int i=501;i<=500+m;i++)
addedge(i,t,1);
while(e--)
{
scanf("%d%d",&a,&b);
if(vis[a][b])
continue;
vis[a][b]=true;
addedge(a,b+500,1);
}
printf("%d\n",dinic());
return 0;
}
KM算法
引入
KM算法解決的“二分圖最大權完美匹配”問題是“二分圖最大匹配”進階而來,但所求存在共通點
所以KM算法是套在匈牙利算法之上的一個算法,它也需要匈牙利算法那樣一遍一遍尋找增廣路從而求出最優解
在“二分圖最大權完美匹配”問題中,每條邊存在一個邊權\(favor[i][j]\)
同時我們還需要一些數組——
\(val1[N]/val2[N]\)分別記錄\(U\)集與\(V\)集點的點權(匹配期望值)
\(vis1[N]/vis2[N]\)分別記錄每次尋找增廣路過程中\(U\)集與\(V\)集點的訪問情況
\(match[N]\)記錄最終\(U\)集內點匹配到的在\(V\)集內點的編號
\(slack[N]\)記錄匹配過程中,\(U\)集內任意點能夠選擇\(V\)集內任意點作為匹配對象所需要降低\(val\)(期望值)的最小值
一些參考書即其它博客的較為規范的名稱定義為:
\(val1[N]/val2[N]\)為頂標,且滿足\(val1[i]+val2[j]≥w[i][j]\)
滿足\(val1[i]+val2[j]=favor[i][j]\)的邊\((i,j)\)稱為相等邊
由上述相等邊構成的子圖為相等子圖
由增廣路徑構成的樹為交錯樹
故KM算法的結論為:若由二分圖中所有滿足\(A[i]+B[j]=w[i,j]\)的邊\((i,j)\)構成的子圖(稱做相等子圖)有完備匹配,那么這個完備匹配就是二分圖的最大權匹配。
理論很繞且難懂,所以下面就繼續相親(?
將此時的\(val1/val2\)看作是與該點進行匹配的期望值
初始化\(val1[i]\)為\(i\)相鄰邊權的最大值(即每次匹配女生都是從權值最大的開始)
如果匹配可以直接成功,就將\(match[i]\)的指向確定下來
如果不能,說明需要降低\(val1\)的期望繼續尋找匹配
KM算法的關鍵點在於,如果在匹配男生\(p\)時,遍歷\(V\)集尋找女生,發現女生\(i\)與其無法匹配(\(val1[p]+val2[i]≠favor[p][i]\)),可能是不存在連邊(能夠排除),也可能是某一方期望值曾發生過改變,則會將“如果讓這個男生與這個女生匹配還需要的期望值”取小存在\(slack[i]\)中
當本次匹配失敗時,將會在所有\(tmp\)中再取小,讓所有訪問過的男生期望值降低,讓所有訪問過的女生期望值增加
正是最后這個“全部再取小”再去改變期望值,才能保證在某次匹配成功時,期望值理論變化最低,使得二分圖此時的權和是最大的
總而言之,算法是遍歷\(U\)集內所有點,循環進行如下操作
- 設置最大期望值
- 利用匈牙利算法找增廣路
- 找到增廣路,匹配成功,退出
- 找不到,最小程度降低男生期望,提升女生期望
- 繼續回到(2)開始重復
另外,需要保證每次進行匈牙利算法時,每個女生只訪問一次
完整程序(DFS)
該算法最壞情況時間復雜度為\(O(n^4)\)
所以下面的代碼在該題\(6-10\)測試點將會TLE
但在平時的題目中一般不會卡復雜度
兩集合數量相同情況
以Luogu P6577 - 【模板】二分圖最大權完美匹配為例
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF=0x3f3f3f3f;
const int N=510;
int n,m,match[N];
bool vis1[N],vis2[N];
ll favor[N][N],val1[N],val2[N],slack[N];
bool dfs(int p)
{
vis1[p]=true; //標記本次匹配涉及多少男生嘗試進行換女生
for(int i=1;i<=n;i++)
if(!vis2[i]) //保證每次尋找增廣路在V集中的點只訪問一次
{
if(val1[p]+val2[i]==favor[p][i]) //男生與女生存在着連線(即這兩人可以進行匹配)
{
vis2[i]=true;
if(!match[i]||dfs(match[i])) //匈牙利算法,尋找增廣路
{
match[i]=p;
return true;
}
}
else
slack[i]=min(slack[i],val1[p]+val2[i]-favor[p][i]); //否則將男生與女生匹配還需要的期望值取小存入slack中
}
return false;
}
ll KM()
{
for(int i=1;i<=n;i++)
{
val1[i]=-INF;
for(int j=1;j<=n;j++)
val1[i]=max(val1[i],favor[i][j]); //初始val1的值為與其相鄰邊權的最大值
}
for(int i=1;i<=n;i++)
{
while(true) //只要沒找到匹配,就降低期望值一直找下去
{
memset(vis1,false,sizeof vis1);
memset(vis2,false,sizeof vis2);
memset(slack,INF,sizeof slack);
if(dfs(i))
break; //如果能找到匹配就退出
ll d=INF; //找不到就減少期望值繼續找
for(int j=1;j<=n;j++)
if(!vis2[j])
d=min(d,slack[j]); //在所有未訪問過的女生匹配所需期望值再取小(最大程度減少換人匹配的期望損耗)
for(int j=1;j<=n;j++)
{
if(vis1[j])
val1[j]-=d; //如果是上一趟匹配訪問過的男生,就降低他的期望值
if(vis2[j])
val2[j]+=d; //如果是上一趟匹配訪問過的女生,就增加她的期望值
}
}
}
ll res=0;
for(int i=1;i<=n;i++)
res+=favor[match[i]][i]; //最后將對應匹配的邊權求和輸出
return res;
}
int main()
{
int u,v;
ll w;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
favor[i][j]=-INF;
for(int i=1;i<=m;i++)
{
scanf("%d%d%lld",&u,&v,&w);
favor[u][v]=max(favor[u][v],w);
}
printf("%lld\n",KM());
for(int i=1;i<=n;i++)
printf("%d ",match[i]);
return 0;
}
兩集合數量不同情況
以HDU 2853為例,要注意每個循環是以\(U\)集為底還是以\(V\)集為底
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF=0x3f3f3f3f;
const int N=55;
int n,m,match[N]; //n為男生數量,m為女生數量
bool vis1[N],vis2[N];
ll favor[N][N],val1[N],val2[N],slack[N];
bool dfs(int p)
{
vis1[p]=true; //標記本次匹配涉及多少男生嘗試進行換女生
for(int i=1;i<=m;i++)
if(!vis2[i]) //保證每次尋找增廣路在V集中的點只訪問一次
{
if(val1[p]+val2[i]==favor[p][i]) //男生與女生存在着連線(即這兩人可以進行匹配)
{
vis2[i]=true;
if(!match[i]||dfs(match[i])) //匈牙利算法,尋找增廣路
{
match[i]=p;
return true;
}
}
else
slack[i]=min(slack[i],val1[p]+val2[i]-favor[p][i]); //否則將男生與女生匹配還需要的期望值取小存入slack中
}
return false;
}
ll KM()
{
for(int i=1;i<=n;i++)
{
val1[i]=-INF;
for(int j=1;j<=m;j++)
val1[i]=max(val1[i],favor[i][j]); //初始val1的值為與其相鄰邊權的最大值
}
for(int i=1;i<=m;i++)
{
match[i]=0; //注意match數組記錄的是第i個女生匹配到的男生編號
val2[i]=0;
}
for(int i=1;i<=n;i++)
{
memset(slack,INF,sizeof slack);
while(true) //只要沒找到匹配,就降低期望值一直找下去
{
memset(vis1,false,sizeof vis1);
memset(vis2,false,sizeof vis2);
if(dfs(i))
break; //如果能找到匹配就退出
ll d=INF; //找不到就減少期望值繼續找
for(int j=1;j<=m;j++)
if(!vis2[j])
d=min(d,slack[j]); //在所有未訪問過的女生匹配所需期望值再取小(最大程度減少換人匹配的期望損耗)
for(int j=1;j<=n;j++)
if(vis1[j])
val1[j]-=d; //如果是上一趟匹配訪問過的男生,就降低他的期望值
for(int j=1;j<=m;j++)
if(vis2[j])
val2[j]+=d; //如果是上一趟匹配訪問過的女生,就增加她的期望值
}
}
ll res=0;
for(int i=1;i<=m;i++)
if(match[i])
res+=favor[match[i]][i];
return res;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
while(cin>>n>>m)
{
int k=n+1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>favor[i][j];
favor[i][j]*=k;
}
}
int sum=0,dd;
for(int i=1;i<=n;i++)
{
cin>>dd;
sum+=favor[i][dd]/k;
favor[i][dd]++;
}
int res=KM();
cout<<n-res%k<<' '<<res/k-sum<<'\n';
}
return 0;
}
完整程序(BFS)
DFS理論上可以被卡到\(O(n^4)\),這題的數據范圍完全經不起這樣折騰
可以發現的是,每次尋找增廣路實際上只是更改一條邊
而在這條邊之前嘗試尋找的增廣路(尋找失敗的)是無效的
所以每次DFS會重新尋找那些已經已知是無效的邊,耗費時間
所以換成BFS,記錄下每次尋找的狀態,讓下一次匹配跟在上一次的狀態之后
這樣即可將最壞情況的時間復雜度降到\(O(n^3)\)
兩集合數量相同情況
以Luogu P6577 - 【模板】二分圖最大權完美匹配為例
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF=0x3f3f3f3f;
const int N=55;
int n,m,match[N],pre[N];
bool vis2[N];
ll favor[N][N],val1[N],val2[N],slack[N];
void bfs(int p)
{
int x,y=0,yy=0;
memset(pre,0,sizeof pre);
memset(slack,INF,sizeof slack);
match[0]=p;
do{
ll d=INF;
x=match[y];
vis2[y]=true;
for(int i=1;i<=n;i++)
{
if(vis2[i])
continue;
if(slack[i]>val1[x]+val2[i]-favor[x][i])
{
slack[i]=val1[x]+val2[i]-favor[x][i];
pre[i]=y;
}
if(slack[i]<d)
{
d=slack[i]; //d取最小可能
yy=i; //記錄最小可能存在的點
}
}
for(int i=0;i<=n;i++)
{
if(vis2[i])
val1[match[i]]-=d,val2[i]+=d;
else
slack[i]-=d;
}
y=yy;
}while(match[y]);
while(y)
{
match[y]=match[pre[y]]; //bfs對訪問路徑進行記錄,並在最后一並改變match
y=pre[y];
}
}
ll KM()
{
memset(match,0,sizeof match);
memset(val1,0,sizeof val1);
memset(val2,0,sizeof val2);
for(int i=1;i<=n;i++)
{
memset(vis2,false,sizeof vis2);
bfs(i);
}
ll res=0;
for(int i=1;i<=n;i++)
res+=favor[match[i]][i]; //最后將對應匹配的邊權求和輸出
return res;
}
int main()
{
int u,v;
ll w;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
favor[i][j]=-INF;
for(int i=1;i<=m;i++)
{
scanf("%d%d%lld",&u,&v,&w);
favor[u][v]=max(favor[u][v],w);
}
printf("%lld\n",KM());
for(int i=1;i<=n;i++)
printf("%d ",match[i]);
return 0;
}
兩集合數量不同情況
以HDU 2853為例,要注意每個循環是以\(U\)集為底還是以\(V\)集為底
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF=0x3f3f3f3f;
const int N=55;
int n,m,match[N],pre[N];
bool vis2[N];
ll favor[N][N],val1[N],val2[N],slack[N];
void bfs(int p)
{
int x,y=0,yy=0;
memset(pre,0,sizeof pre);
memset(slack,INF,sizeof slack);
match[0]=p;
do{
ll d=INF;
x=match[y];
vis2[y]=true;
for(int i=1;i<=m;i++)
{
if(vis2[i])
continue;
if(slack[i]>val1[x]+val2[i]-favor[x][i])
{
slack[i]=val1[x]+val2[i]-favor[x][i];
pre[i]=y;
}
if(slack[i]<d)
{
d=slack[i]; //d取最小可能
yy=i; //記錄最小可能存在的點
}
}
for(int i=0;i<=m;i++)
{
if(vis2[i])
val1[match[i]]-=d,val2[i]+=d;
else
slack[i]-=d;
}
y=yy;
}while(match[y]);
while(y)
{
match[y]=match[pre[y]]; //bfs對訪問路徑進行記錄,並在最后一並改變match
y=pre[y];
}
}
ll KM()
{
memset(match,0,sizeof match);
memset(val1,0,sizeof val1);
memset(val2,0,sizeof val2);
for(int i=1;i<=n;i++)
{
memset(vis2,false,sizeof vis2);
bfs(i);
}
ll res=0;
for(int i=1;i<=m;i++)
res+=favor[match[i]][i]; //最后將對應匹配的邊權求和輸出
return res;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
while(cin>>n>>m)
{
int k=n+1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>favor[i][j];
favor[i][j]*=k;
}
}
int sum=0,dd;
for(int i=1;i<=n;i++)
{
cin>>dd;
sum+=favor[i][dd]/k;
favor[i][dd]++;
}
int res=KM();
cout<<n-res%k<<' '<<res/k-sum<<'\n';
}
return 0;
}
本文參考的博客/文章如下(我就是個縫合怪qwq):
Luogu P6577 題解 (Writer: George1123)
Luogu P6577 題解 (Writer: Rainy7)
cnblogs 《KM算法詳解+模板》 (Writer: wenr)
CSDN 《KM算法》 (Writer: Enjoy_process)
縫合完畢,感謝閱讀!