最近花了大概十天內時間去學習了一下二分圖匹配,不學不知道,一學才發現自己有很多知識漏洞,不過幸虧 在賽前補了一下,這個總結有習題集和一些重要(難!)的二分圖算法以及學習資源,記錄下來供以後自己回顧用,也給大家提供一些好的資料。
首先就是最簡單的二分圖最大匹配,這個算法有兩種,一種是匈牙利算法(O(V*E)),另一種是HK算法(O(sqrt(V)*E)),其實它們的本質區別在於匈牙利算法是直接從空匹配開始dfs,找增廣路,而HK算法時預先找到一些不相交的極大最短增廣路集,然後再進增廣,其實就是先BFS一下,再去dfs。關於這兩個個算法的詳細解釋,可以借鑑一下這個資料:匈牙利算法和HK算法。
匈牙利算法模板:
//hdu2063——模板題
#include <iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=505;
int n,m,k;
int line[maxn][maxn];
int used[maxn],girl[maxn];
bool found(int x){
for(int i = 1;i<=n;++i){
if(line[x][i]&&!used[i]){
used[i]=1;
if(girl[i]==0||found(girl[i])){
girl[i]=x;
return 1;
}
}
}
return 0;
}
int main(int argc, char** argv) {
int x,y;
while(scanf("%d",&k)&&k){
scanf("%d %d",&m,&n);
memset(line,0,sizeof(line));
memset(girl,0,sizeof(girl));
for(int i = 0;i<k;i++){
scanf("%d %d",&x,&y);
line[x][y]=1;
}
int sum=0;
for(int i = 1;i<=m;++i){
memset(used,0,sizeof(used));
if(found(i)) sum++;
}
printf("%d\n",sum);
}
return 0;
}
HK算法模板:
//hdu2389——HK算法模板
#include <iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
using namespace std;
const int maxn=3e3+7;
const int INF =0x3f3f3f3f;
int G[maxn][maxn],visited[maxn];
int cx[maxn],cy[maxn];//標記分別與x,y匹配的點。
int dx[maxn],dy[maxn];//存儲BFS時x,y的層數
int n,m,dis;
struct people{
int x,y;
int v;
}p[maxn];
struct umbrella{
int x,y;
}u[maxn];
int bfs(){
queue<int> q;
dis=INF;
memset(dx,-1,sizeof(dx));
memset(dy,-1,sizeof(dy));// dx、dy用來標記當前點是否在一個交替路中
for(int i=1;i<=m;i++){
if(cx[i]==-1){//初始化x點集中沒有匹配的點入隊
q.push(i);
dx[i]=0;
}
}
while(!q.empty()){//BFS搜出目前最短的增廣路集合
int u=q.front();
q.pop();
if(dx[u]>dis) break;//已經找到最短增廣路,跳出
for(int v=1;v<=n;v++){
if(G[u][v]&&dy[v]==-1){//如果u與v有邊而且v沒有被搜到
dy[v]=dx[u]+1;//v在一條交替路上
if(cy[v]==-1) dis=dy[v];//如果當前點是未匹配點,那麼該交替路爲增廣路
else{//否則把當前節點入隊,層數加一
dx[cy[v]]=dy[v]+1;
q.push(cy[v]);
}
}
}
}
if(dis==INF) return 0;//如果不存在增廣路,那麼滿足最大匹配。
else return 1;
}
int find(int x)//類似經典的最大匹配寫法
{
for(int i = 1;i<=n;++i){
if(!visited[i]&&G[x][i]&&dy[i]==dx[x]+1){//i在一條交替路上
visited[i]=1;
if(cy[i]!=-1&&dy[i]==dis) continue;//如果該點已經匹配,而且不滿足最短增廣路,就跳出
if(cy[i]==-1||find(cy[i])){
cy[i]=x;
cx[x]=i;
return 1;
}
}
}
return 0;
}
int match()
{
memset(cx,-1,sizeof(cx));
memset(cy,-1,sizeof(cy));
int ans=0;
while(bfs()){//每次bfs求出一些不相交的最短增廣路
memset(visited,0,sizeof(visited));
for(int i = 1;i<=m;++i)
if(cx[i]==-1&&find(i))//如果i點還沒有匹配,進行匹配
ans++;
}
return ans;
}
double dist(int i,int j){
return sqrt((p[i].x-u[j].x)*(p[i].x-u[j].x)+(p[i].y-u[j].y)*(p[i].y-u[j].y));
}
int main(int argc, char** argv) {
int ncase;
scanf("%d",&ncase);
for(int t = 1;t<=ncase;++t){
memset(G,0,sizeof(G));
int time;
scanf("%d %d",&time,&m);
for(int i = 1;i<=m;++i)
scanf("%d %d %d",&p[i].x,&p[i].y,&p[i].v);
scanf("%d",&n);
for(int i = 1;i<=n;++i)
scanf("%d%d",&u[i].x,&u[i].y);
for(int i = 1;i<=m;++i){
int r=p[i].v*time;
for(int j = 1;j<=n;j++)
if(dist(i,j)<=r)//建二分圖
G[i][j]=1;
}
printf("Scenario #%d:\n",t);
printf("%d\n\n",match());
}
return 0;
}
其實關於二分圖的難點不在於求最大匹配,其關鍵還是在於怎樣去建圖,關於怎樣建圖,可以參考這篇博客:二分圖的建圖
以及這篇論文:二分圖常用建圖技巧(強推!!!!!)
關於二分圖最大匹配還有一些定理需要知道,
定理一:最小頂點覆蓋=最大匹配
定理二:最小邊覆蓋(原圖是二分圖)=最小路徑覆蓋(原圖是DAG圖)=結點總數N-最大匹配
定理三:最大獨立集=結點總數-最大匹配。
關於這三個定理的解釋以及定義可以參考這篇文章:關於二分圖的一些定理。(強推!!!)
注意關於最小路徑覆蓋有一個小trick,當有向圖的邊有相交時,我們不能直接去求最小路徑覆蓋,而需要先求圖的傳遞閉包,再求出最小路徑覆蓋,後面kuangbin帶你飛專題有這個坑點,具體可以參考這篇文章:關於最小路徑覆蓋的一些技巧。
講完最大匹配,該講最大權匹配了,最大權匹配就是邊上面有權值(其實最大匹配也有權值,只不過爲1!!),帶上權值我們怎麼做呢?這裏有一個KM算法,專門就是爲此服務的,其算法思想的核心在於給X點集和Y點集的每個點賦一個期望數組,同時開一個slack數組來記錄每個點通過減少最少的期望能加一條邊。
具體算法的過程,可以參考這篇博文:KM算法詳解
模板如下:
//hdu2255——模板題
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn=307;
int n;
int ex_girl[maxn],ex_boy[maxn],girl_visited[maxn],boy_visited[maxn];
int slack[maxn],love[maxn][maxn];
int match[maxn];
bool find(int girl){
girl_visited[girl]=1;
for(int boy=0;boy<n;++boy){
if(boy_visited[boy]) continue;
int d=ex_girl[girl]+ex_boy[boy]-love[girl][boy];
if(!d){
boy_visited[boy]=1;
if(match[boy]==-1||find(match[boy])){
match[boy]=girl;
return true;
}
}
else slack[boy]=min(slack[boy],d);
}
return false;
}
int KM(){
memset(match,-1,sizeof(match));
memset(ex_boy,0,sizeof(ex_boy));
for(int i = 0;i<n;++i){
// ex_girl[i]=*max_element(love[i],love[i]+n);
ex_girl[i]=love[i][0];
for(int j = 1;j<n;++j)
ex_girl[i]=max(ex_girl[i],love[i][j]);
}
for(int i = 0;i<n;++i){
fill(slack,slack+n,INF);
while(1){
memset(girl_visited,0,sizeof(girl_visited));
memset(boy_visited,0,sizeof(boy_visited));
if(find(i)) break;
int d=INF;
for(int i = 0;i<n;++i){
if(!boy_visited[i])
d=min(d,slack[i]);
}
for(int i = 0;i<n;++i){
if(girl_visited[i]) ex_girl[i]-=d;
if(boy_visited[i]) ex_boy[i]+=d;
else slack[i]-=d;
}
}
}
int ans=0;
for(int i = 0;i<n;++i)
ans+=love[match[i]][i];
return ans;
}
int main(int argc, char** argv) {
while(~scanf("%d",&n)){
for(int i = 0;i<n;++i)
for(int j = 0;j<n;++j)
scanf("%d",&love[i][j]);
printf("%d\n",KM());
}
return 0;
}
下面講一下二分圖的多重匹配,多重匹配分爲兩種,一種是多重最大匹配,比較好寫,另一種是多重最優匹配,比較難理解一些,其實二分圖多重匹配就是在添加一個超級匯點和源點,然後把以前的X點集和Y點集的維度增加了一維,多了一個限制,其實就是以前的二分圖是一對一,現在變成了一對多(一個點跟多個匹配邊相連),多對多。
具體的解釋可以參考這篇文章:二分圖多重匹配
話不多說上代碼!
二分圖多重最大匹配
//hdu3605——二分圖多重最大匹配板子題
#include <iostream>
#include<cstring>
using namespace std;
const int maxn=1e5+7;
int n,m,visited[15],G[maxn][15];
int match[15][maxn];
// match[i][j]=k 表示第 i 個星球上住的第 j 個人是 k
int cnt[15],cap[15];//cap爲 y節點的容量,cnt爲當前 y節點使用的容量
int dfs(int x){
for(int i = 1;i<=m;++i){
if(G[x][i]&&!visited[i]){
visited[i]=1;
if(cnt[i]<cap[i]){//如果 y 節點還有容量可以匹配
cnt[i]++;
match[i][cnt[i]]=x;
return 1;
}
for(int j = 1;j<=m;++j){//如果 y 節點容量已經滿了,試着爲 y 節點的某個對象換對象
if(dfs(match[i][j])){
match[i][j]=x;//y 節點的 第 i 個對象讓給 x
return 1;
}
}
}
}
return 0;
}
int judge(){
memset(cnt,0,sizeof(cnt));
for(int i = 1;i<=n;++i){//爲 x 節點匹配對象
memset(visited,0,sizeof(visited));
if(!dfs(i)) return 0;
}
return 1;
}
int main(int argc, char** argv) {
while(~scanf("%d%d",&n,&m)){
for(int i = 1;i<=n;++i)
for(int j = 1;j<=m;++j)
scanf("%d",&G[i][j]);
for(int i = 1;i<=m;++i) scanf("%d",&cap[i]);
if(judge()) printf("YES\n");
else printf("NO\n");
}
return 0;
}
關於這個算法的解析可以參考這篇文章:網絡流三·二分圖多重匹配
二分圖多重最優匹配:
//hihocoder 1393——二分圖最優匹配
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<queue>
using namespace std;
const int MAXN=250;
const int MAXE=20010;
const int INF=0x3f3f3f3f;
struct Node
{
int to,next,val;
Node(int to,int next,int val):to(to),next(next),val(val){};
Node(){}
};
Node edge[MAXE];
int head[MAXN],cnt;
void addEdge(int u,int v,int val)
{
edge[cnt]=Node(v,head[u],val);head[u]=cnt++;
edge[cnt]=Node(u,head[v],0);head[v]=cnt++;
}
int step[MAXN];
bool BFS(int st,int ed)
{
memset(step,-1,sizeof(step));
step[st]=0;
queue<int> que;
que.push(st);
while(!que.empty())
{
int u=que.front();que.pop();
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(edge[i].val>0&&step[v]==-1)
{
step[v]=step[u]+1;
que.push(v);
if(v==ed) return true;
}
}
}
return false;
}
int DFS(int st,int ed,int flow)
{
if(st==ed||flow==0) return flow;
int curr=0;
for(int i=head[st];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(step[v]==step[st]+1&&edge[i].val>0)
{
int d=DFS(v,ed,min(flow,edge[i].val));
if(d>0)
{
edge[i].val-=d;
edge[i^1].val+=d;
flow-=d;
curr+=d;
if(flow==0) break;
}
}
}
if(curr==0) step[st]=INF;
return curr;
}
int Dinic(int st,int ed)
{
int flow=0;
while(BFS(st,ed))
{
flow+=DFS(st,ed,INF);
}
return flow;
}
int main()
{
int t,n,m,st,ed,need,num,good,item,sum;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
sum=cnt=0;st=n+m+1;ed=st+1;
memset(head,-1,sizeof(head));
for(int i=1;i<=m;i++)
{
scanf("%d",&need);
sum+=need;
addEdge(n+i,ed,need);
}
for(int i=1;i<=n;i++)
{
scanf("%d%d",&num,&good);
addEdge(st,i,num);
for(int j=1;j<=good;j++)
{
scanf("%d",&item);
addEdge(i,n+item,1);
}
}
int res=Dinic(st,ed);
if(res!=sum) printf("No\n");
else printf("Yes\n");
}
}
這裏借鑑了hihocoder裏的習題來解釋。。。。
如果x部節點可以匹配多個y部節點,y部節點可以同時匹配多個x部節點,那麼應該用網絡流來解決。(因爲匈牙利算法無法應對兩邊都可以選多個這種情況)
怎麼建圖呢?
很簡單,假設x部節點的容量爲capx[ i ],y部節點的容量爲capy[ i ],同時給出x部節點可以與y部節點相連的的邊,那麼對於每個x部節點,超級源點都與x部節點連邊,邊權爲capx[i];對於每個y部節點,都與超級匯點連接邊,邊權爲capy[i]。然後連接每個x與y直接相連的邊,邊權爲1。
這樣一來,求出最大流就是最大匹配方案了:流量通道上的邊的剩餘流量代表匹配結果
下面介紹一個算法;婚姻匹配算法:(GS算法)
這個算法的實際應用很多,也挺有意思的,我在這也就不對算法進行解釋了,關於具體思路可以參考這篇文章
Stable Matching Problem穩定匹配問題-----穩定婚姻算法
話不多說,上板子和題。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=200+5;
int B_g[N][N],G_b_score[N][N];
int bg[N],gb[N];
bool mark[N][N];
int n;
struct node
{
int num,v;
double x,y,z;
}node1[N],node2[N];
struct nodee
{
double dis;
int id,v;
}fz[N];
bool cmp(nodee a,nodee b)
{
if(a.dis==b.dis) return a.v>b.v;
return a.dis<b.dis;
}
double Dis(node a,node b)
{
double x=a.x-b.x;
double y=a.y-b.y;
double z=a.z-b.z;
return sqrt(x*x+y*y+z*z);
}
void stable_marry()
{
memset(mark,false,sizeof(mark));
memset(bg,-1,sizeof(bg));
memset(gb,-1,sizeof(gb));
queue<int>q;
while(!q.empty()) q.pop();
for(int i=1;i<=n;i++) q.push(i);
int head,nxt;
while(!q.empty())
{
head=q.front();
q.pop();
for(int i=1;i<=n;i++)
{
int nxt=B_g[head][i];
if(mark[head][nxt]) continue;
mark[head][nxt]=1;
if(gb[nxt]==-1)
{
gb[nxt]=head;
bg[head]=nxt;
break;
}
else if(G_b_score[nxt][gb[nxt]]<G_b_score[nxt][head])
{
q.push(gb[nxt]);
gb[nxt]=head;
bg[head]=nxt;
break;
}
}
}
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d%d%lf%lf%lf",&node1[i].num,&node1[i].v,&node1[i].x,&node1[i].y,&node1[i].z);
for(int i=1;i<=n;i++)
scanf("%d%d%lf%lf%lf",&node2[i].num,&node2[i].v,&node2[i].x,&node2[i].y,&node2[i].z);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
fz[j].dis=Dis(node1[i],node2[j]);
fz[j].id=j;
fz[j].v=node2[j].v;
}
sort(fz+1,fz+n+1,cmp);
for(int j=1;j<=n;j++)
B_g[i][j]=fz[j].id;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
fz[j].dis=Dis(node2[i],node1[j]);
fz[j].id=j;
fz[j].v=node1[j].v;
}
sort(fz+1,fz+n+1,cmp);
for(int j=1;j<=n;j++)
G_b_score[i][fz[j].id]=n-j+1;
}
stable_marry();
for(int i=1;i<=n;i++)
printf("%d %d\n",node1[i].num,node2[bg[i]].num);
printf("\n");
}
return 0;
}
接下來講一個很牛逼的算法,它可以解決一般圖和二分圖的最大和最大權匹配,對,你沒聽錯,就是可以解決你學的大部分問題。當然這麼牛逼的算法,肯定不會很好理解,我也是花了好長時間才弄懂,但是自己還是不會敲,只會用板子(QAQ)。
下面推薦幾個講的不錯的博客,當然我感覺講的好,還是不好理解!!!
附上我看的網上的板子。
//URAL1099——模板題
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<vector>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<algorithm>
using namespace std;
const int maxn=300;
int N;
bool G[maxn][maxn];
int match[maxn];
bool InQueue[maxn],InPath[maxn],InBlossom[maxn];
int head,tail;
int Queue[maxn];
int start,finish;
int NewBase;
int father[maxn],Base[maxn];
int Count;
void CreateGraph(){
int u,v;
memset(G,0,sizeof(G));
scanf("%d",&N);
while(scanf("%d%d",&u,&v)!=EOF){
G[u][v]=G[v][u]=1;
}
}
void Push(int u){
Queue[tail++]=u;
InQueue[u]=1;
}
int Pop(){
int res=Queue[head++];
return res;
}
int FindCommonAncestor(int u,int v){
memset(InPath,0,sizeof(InPath));
while(true){
u=Base[u];
InPath[u]=1;
if(u==start)break;
u=father[match[u]];
}
while(true){
v=Base[v];
if(InPath[v])break;
v=father[match[v]];
}
return v;
}
void ResetTrace(int u){
int v;
while(Base[u]!=NewBase){
v=match[u];
InBlossom[Base[u]]=InBlossom[Base[v]]=1;
u=father[v];
if(Base[u]!=NewBase)father[u]=v;
}
}
void BlossomContract(int u,int v){
NewBase=FindCommonAncestor(u,v);
memset(InBlossom,0,sizeof(InBlossom));
ResetTrace(u);
ResetTrace(v);
if(Base[u]!=NewBase)father[u]=v;
if(Base[v]!=NewBase)father[v]=u;
for(int tu=1;tu<=N;tu++){
if(InBlossom[Base[tu]]){
Base[tu]=NewBase;
if(!InQueue[tu])Push(tu);
}
}
}
void FindAugmentingPath(){
memset(InQueue,0,sizeof(InQueue));
memset(father,0,sizeof(father));
for(int i=1;i<=N;i++){
Base[i]=i;
}
head=tail=1;
Push(start);
finish=0;
while(head<tail){
int u=Pop();
for(int v=1;v<=N;v++){
if(G[u][v]&&(Base[u]!=Base[v])&&match[u]!=v){
if((v==start)||(match[v]>0)&&father[match[v]]>0){
BlossomContract(u,v);
} else if(father[v]==0){
father[v]=u;
if(match[v]>0){
Push(match[v]);
} else {
finish=v;
return;
}
}
}
}
}
}
void AugmentPath(){
int u,v,w;
u=finish;
while(u>0){
v=father[u];
w=match[v];
match[v]=u;
match[u]=v;
u=w;
}
}
void Edmonds(){
memset(match,0,sizeof(match));
for(int u=1;u<=N;u++){
if(match[u]==0){
start=u;
FindAugmentingPath();
if(finish>0)AugmentPath();
}
}
}
void PrintMatch(){
Count=0;
for(int u=1;u<=N;u++){
if(match[u]>0)Count++;
}
printf("%d\n",Count);
for(int u=1;u<=N;u++){
if(u<match[u]){
printf("%d %d\n",u,match[u]);
}
}
}
int main(){
CreateGraph();
Edmonds();//進行匹配
PrintMatch();//輸出匹配
return 0;
}
!!!終於把二分圖匹配的知識點講完了QAQ,還真是多的煩,下面開始給出kuangbin帶你飛——二分圖匹配專題的題解!!
hdu2255——KM算法 | 題解 |
hdu3488——二分圖最大權匹配(拆點)或者跑一個最小費用最大流 | 題解 |
POJ2289——(一對多)多重最大匹配+二分 | 題解 |
POJ2112——(一對多)多重最大匹配+二分 | 題解 |
POJ3189——多重最大匹配+二分 | 題解 |
URAL1099——一般圖匹配+帶花樹算法 | 題解 |
hdu4687——一般圖匹配+帶花樹算法 | 題解 |