算法訓練——二分圖匹配習題+算法+模板

最近花了大概十天內時間去學習了一下二分圖匹配,不學不知道,一學才發現自己有很多知識漏洞,不過幸虧 在賽前補了一下,這個總結有習題集和一些重要(難!)的二分圖算法以及學習資源,記錄下來供以後自己回顧用,也給大家提供一些好的資料。

首先就是最簡單的二分圖最大匹配,這個算法有兩種,一種是匈牙利算法(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帶你飛——二分圖匹配專題的題解!!


二分圖最大匹配
hdu1045——依行依列建圖 題解
hdu2444——二分圖判定+最大匹配 題解
hdu1083——最大匹配模板題 題解
hdu1281——二分圖最大匹配(求關鍵點) 題解
hdu2819——二分圖最大匹配的靈活應用 題解
hdu2389——Hopcroft-Karp(優化)算法 題解
hdu4185——巧妙建圖 題解
POJ3020——最小邊覆蓋 題解
hdu1054——最小頂點覆蓋 題解
hdu1151——最小路徑覆蓋(DAG) 題解
POJ2594——(DAG)最小路徑覆蓋+Floyd求傳遞閉包 題解
hdu3829——最大獨立集 題解
二分圖最大權匹配
hdu2255——KM算法 題解
hdu3488——二分圖最大權匹配(拆點)或者跑一個最小費用最大流 題解
二分圖多重最大匹配
POJ2289——(一對多)多重最大匹配+二分 題解
POJ2112——(一對多)多重最大匹配+二分 題解
POJ3189——多重最大匹配+二分 題解
一般圖匹配
URAL1099——一般圖匹配+帶花樹算法 題解
hdu4687——一般圖匹配+帶花樹算法 題解

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章