算法训练——二分图匹配习题+算法+模板

最近花了大概十天内时间去学习了一下二分图匹配,不学不知道,一学才发现自己有很多知识漏洞,不过幸亏 在赛前补了一下,这个总结有习题集和一些重要(难!)的二分图算法以及学习资源,记录下来供以后自己回顾用,也给大家提供一些好的资料。

首先就是最简单的二分图最大匹配,这个算法有两种,一种是匈牙利算法(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——一般图匹配+带花树算法 题解

 

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