zoukankan      html  css  js  c++  java
  • 二分图匹配

    二分图匹配

    含义:把无向图G=(V,E),分为两个集合V1,V2,所有的边都在V1,V2之间,而V1或V2内部没有边,V1中的一个点与V2中的一个点关联,称为一个匹配

    一个图是不是二分图,一般通过染色判断,用两种颜色对所有顶点染色,如果相邻顶点的颜色都不同那么就是二分图。

    ps。一个图是二分图当且仅当它不含边的数量为奇数的点

    常见问题:

    (1)无权图:求包含边数最多的匹配,求二分图的最大匹配(最大流、匈牙利算法)

    (2)带权图:求边权和最大的匹配(KM算法,转化为费用流求解)

    QUS1:求二分图的最大匹配

    定义:(1)最大匹配:包含边数最多的匹配

       (2)完美匹配:所有点都在匹配的边上

    做法1:转化为最大流求解

    把每个边转化为有向边,流量为1,在V1上加一个人为的源点s(连接所以的V1),在V2上加上一个人为的汇点t(连接所有的V2),然后求s-->t的最大流即可

    POJ 2339 Selecting Courses

    由于是求最大匹配,所以可以保证每个时间点只连接一门课程

    #include<iostream>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<stack>
    #include<cstdio>
    #include<queue>
    #include<map>
    #include<vector>
    #include<set>
    using namespace std;
    const int maxn=1010;
    const int INF=0x3fffffff;
    typedef long long LL;
    typedef unsigned long long ull;
    //最大流解决二分图的最大匹配
    //虚拟一个源点,到每一条的每个上课时间,7天,12节课,共84个节点,对每个课时选择上哪些课,建立权值为1的边,最后都汇集到虚拟的汇点
    //EK求最大流
    int mp[410][410];
    int pre[410];
    int N,M,K;
    void input(){
    	memset(mp,0,sizeof(mp));
    	int n,m,t;
    	for(int i=1;i<=N;i++){
    		scanf("%d",&t);
    		mp[0][i]=1;  //到这门课(源点) 
    		for(int j=0;j<t;j++){
    			scanf("%d %d",&n,&m); //星期几、第几节课 
    			mp[i][N+(n-1)*12+m]=1; //这门课---时间点 
    			mp[N+(n-1)*12+m][84+N+1]=1;  //时间点---汇点 
    		}
    	}
    } 
    queue<int> que;
    bool bfs(int s,int t){
    	while(!que.empty()) que.pop();
    	que.push(s);
    	memset(pre,-1,sizeof(pre));
    	pre[s]=0;
    	int index;
    	while(!que.empty()){
    		index=que.front();
    		que.pop();
    		for(int i=1;i<=t;i++){
    			if(mp[index][i]>0&&pre[i]==-1){
    				pre[i]=index;
    				if(i==t) return 1;
    				que.push(i);
    			}
    		}
    	}
    	return 0;
    }
    int ek(int s,int t){
    	int summ=0;
    	while(bfs(s,t)){
    		for(int i=t;i!=s;i=pre[i]){
    			mp[pre[i]][i]--;  //正向减 
    			mp[i][pre[i]]++; //反向加 
    		}
    		summ++;
    	}
    	return summ;  //最大匹配数 
    }
    int main(){
    	while(scanf("%d",&N)!=EOF){
    		input();
    		printf("%d
    ",ek(0,84+N+1));
    	}
    return 0;
    }

    做法2:匈牙利求解(更简单)

    可以看作是最大流的特殊实现,因为二分图是一个很简单的图,所以对s,t的操作是多余的,可以直接从V1开始找增广路径

    如果用邻接矩阵:时间O(V^3),空间O(V^2)

    如果有邻接表:时间O(VE),空间O(V+E)

    #include<iostream>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<stack>
    #include<cstdio>
    #include<queue>
    #include<map>
    #include<vector>
    #include<set>
    using namespace std;
    const int maxn=1010;
    const int INF=0x3fffffff;
    typedef long long LL;
    //匈牙利算法求最大二分图匹配
    int g[510][510];
    int match[510];   //匹配结果 
    int reverb[510]; 
    int k,m,m_girl,n_boy;
    bool dfs(int x){   //找一个增广路径,给女孩x找一个配对男孩 
     	for(int i=1;i<=n_boy;i++){
    		if(!reverb[i]&&g[x][i]){
    			reverb[i]=1; //预定男孩,给女孩x
    			//上面是
    			if(!match[i]||dfs(match[i])){  //情况1:男孩x还没有匹配,就直接分
    				                           //情况2:如果男孩i已经配对,尝试dfs更换原有配对,更换成功后,就有属于女孩x
    				match[i]=x;
    				return true; 
    			} 
    		}
    	}
    	return false;  //没有喜欢的男孩  或者  更换不成功 
    }
    
    
    int main(){
    	while(scanf("%d",&k)!=EOF&&k){
    		scanf("%d %d",&m_girl,&n_boy);
    		memset(g,0,sizeof(g));
    		memset(match,0,sizeof(match));
    		for(int i=0;i<k;i++){
    			int a,b;
    			scanf("%d %d",&a,&b);
    			g[a][b]=1;
    		}
    		int summ=0;
    		for(int i=1;i<=m_girl;i++){
    			memset(reverb,0,sizeof(reverb));
    			if(dfs(i)) summ++;
    		}
    		printf("%d
    ",summ);
    	}
    	
    	
    	
    return 0;
    }
    

    例题:UVA10080 Gopher II

    打地鼠

    【这个完全是匈牙利算法求最待匹配】

    https://www.luogu.com.cn/problem/UVA10080

    #include<iostream>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<stack>
    #include<cstdio>
    #include<queue>
    #include<map>
    #include<vector>
    #include<set>
    using namespace std;
    const int maxn=105;
    const int INF=0x3fffffff;
    typedef long long LL;
    typedef unsigned long long ull;
    /*
    题意:每只地鼠要走到距离小于等于v*t的洞里,问最少有几只地鼠会被抓走
    一只地鼠一个洞,显然二分图最大匹配
    建图:时间复杂度Θ(n^2)每个地鼠都暴力算一遍每个洞的哈密顿距离
    二分图匹配: 匈牙利算法
    当把地鼠和洞穴连边建成一个二分图后,这个二分图的最大匹配就是能躲进洞穴的地鼠的最大个数,最后我们输出 n-(最大匹配) 即可
    */ 
    int n,m,s,vi;
    int mp[maxn][maxn];
    int match[maxn];
    double xx[maxn],yy[maxn];
    bool vis[maxn];
    double dis(double x1,double y1,double x2,double y2){
    	return sqrt(pow(x1-x2,2)+pow(y1-y2,2));
    }
    void input(){
    	memset(mp,0,sizeof(mp));
    	memset(match,0,sizeof(match));
    	for(int i=1;i<=n;i++){
    		scanf("%lf %lf",&xx[i],&yy[i]); //地鼠的位置 
    	}
    	for(int i=1;i<=m;i++){
    		double x,y;
    		scanf("%lf %lf",&x,&y);
    		for(int j=1;j<=n;j++){
    			if(dis(x,y,xx[j],yy[j])<=vi*s){
    				mp[j][i]=1;
    				//地鼠-->洞 
    			}
    		}
    	}
    }
    bool dfs(int p){  //地洞 
    	int tmp;
    	for(int i=1;i<=n;i++){
    		if(mp[i][p]&&!vis[i]){
    			vis[i]=1;
    			tmp=match[i];
    			match[i]=p;
    			if(tmp==0||dfs(tmp)) return 1;
    			match[i]=tmp; //回溯 
    		}
    	}
    	return 0;
    }
    void ek(){
    	int res=0;
    	for(int i=1;i<=m;i++){
    		memset(vis,0,sizeof(vis));
    		res+=dfs(i);
    		//cout<<res<<endl;
    	}
    	printf("%d
    ",n-res);
    }
    int main(){
    	while(scanf("%d %d %d %d",&n,&m,&s,&vi)!=EOF){
    		input();
    		ek();
    	}
    return 0;
    }

     

     相关的几个问题:

    (1)最小路径覆盖:一个不含圈的有向图G中,G的一个路径覆盖是一个其节点不相交的路径集合P,图中的每一个节点仅包含于P中的某一条路径。求一个不含圈的有向图G的最小路径覆盖书

    最小路径覆盖数=G的定点数-最小路径覆盖中的边数

    how?拆点:把每个顶点拆为Xi,Yi然后连边,所求出的二分图的最大匹配数则为原图G的最小路径覆盖中的边数

    so-----> 最小路径覆盖数=G的定点数-二分图的最大匹配数

    (2)最小点集覆盖:最少的点让每条边都至少和其中一个点关联:

    so----->最小点集覆盖=最大匹配

    (3)最大独立集

    在N个点的图G中选出m个点,使这m个点两两之间没有边,求m最大值

    二分图的最大独立集=节点数n-最大匹配数

    二分图最优匹配:

    又称带权最大匹配,每条边有权值,求最大权值和,若X,Y顶点集合个数相同,那么最优匹配也是一个完备匹配,如果个数不相等,可以通过加补点加0边实现转化。

    KM算法(对匈牙利算法的贪心拓展):

    给每个顶点可行顶标,对所有的边(i,j)都有lx[i]+ly[j]>=w[i,j]成立,且对所有在完备匹配M中的边(i,j),都有lx[i]+ly[j]=w[i,j]成立,则M是图G的一个最佳匹配

    如果X于Y中节点个数相同并且所有点的度数都相等则一定存在完备匹配,否则补点加0边

    how?

    欲求完全二分图的最佳匹配,只要有匈牙利算法求其相等子图的完备匹配即可,逐次修改可行顶标的方法使对于的相等子图之最大匹配逐次增广,最后出现完备匹配。

    解释https://www.cnblogs.com/logosG/p/logos.html

    一些题目https://www.cnblogs.com/skyming/archive/2012/02/18/2356919.html

    伪代码:

    int lx[maxn],ly[maxn],sx[maxn],sy[maxn],match[maxn];
    int n,m;
    bool path(int u){  //匈牙利
    	sx[u]=1;
    	for(int v=0;v<n;v++){
    		if(!sy[v]&&lx[u]+ly[v]==wei[u][v]){
    			sy[v]=1;
    			if(match[v]==-1||path(match[v])){
    				match[v]=u;
    				return 1;
    			}
    		}
    	} 
    	return 0;
    } 
    int bestmatch(){
    	int i,j;
    	for(i=0;i<n;i++){
    		lx[i]=-INF;
    		ly[i]=0;  //初始值
    		for(j=0;j<n;j++){
    			if(lx[i]<wei[i][j]){
    				lx[i]=wei[i][j];
    			}
    		} 
    	}
    	memset(match,-1,sizeof(match));
    	for(int u=0;u<n;u++){
    		memset(sx,0,sizeof(sx));
    		memset(sy,0,sizeof(sy));
    		if(path(u)){
    			break; //匹配成功,跳出进行下一个顶点的匹配 
    		}
    		int dx=INF;
    		for(i=0;i<n;i++){
    			if(sx[i])
    				for(j=0;j<n;j++){
    					if(!sy[j]){
    						dx=min(lx[i]+ly[j]-wei[i][j],dx);
    					}
    				}
    				for(j=0;j<n;j++){
    					if(sx[j])  //对于访问过的x节点
    					lx[j]-=dx; 
    					if(sy[j]) //对于访问过的y节点
    					ly[j]+=dx; 
    				}	
    		}
    	}
    }
    
    
    int main(){
    return 0;
    }

    代码:

    #include<iostream>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<stack>
    #include<cstdio>
    #include<queue>
    #include<map>
    #include<vector>
    #include<set>
    using namespace std;
    const int maxn=1010;
    const int INF=0x3fffffff;
    typedef long long LL;
    typedef unsigned long long ull;
    int love[maxn][maxn];   // 记录每个妹子和每个男生的好感度
    int ex_girl[maxn];      // 每个妹子的期望值
    int ex_boy[maxn];       // 每个男生的期望值
    bool vis_girl[maxn];    // 记录每一轮匹配匹配过的女生
    bool vis_boy[maxn];     // 记录每一轮匹配匹配过的男生
    int match[maxn];        // 记录每个男生匹配到的妹子 如果没有则为-1
    int slack[maxn];        // 记录每个汉子如果能被妹子倾心最少还需要多少期望值
    int n;
    bool dfs(int gg){
    	vis_girl[gg]=1;
    	for(int i=0;i<n;i++){
    		if(vis_boy[i]) continue;
    		int gap=ex_girl[gg]+ex_boy[i]-love[gg][i];
    		if(gap==0){
    			vis_boy[i]=true;
    			if(match[i]==-1||dfs(match[i])){   // 找到一个没有匹配的男生 或者该男生的妹子可以找到其他人
    				match[i]=gg;
    				return 1;
    			}
    			else{
    				slack[i]=min(slack[i],gap);
    				 // slack 可以理解为该男生要得到女生的倾心 还需多少期望值 取最小值
    			}
    		}
    	}
    	return false;
    }
    int km(){
    	memset(match,-1,sizeof(match));
    	memset(ex_boy,0,sizeof(ex_boy));// 初始每个男生的期望值为0
    	   // 每个女生的初始期望值是与她相连的男生最大的好感度
    	for(int i=0;i<n;i++){
    		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(vis_girl,false,sizeof(vis_girl));
    			memset(vis_boy,false,sizeof(vis_boy));
    			if(dfs(i)) break;// 找到归宿 退出
    
                // 如果不能找到 就降低期望值
                // 最小可降低的期望值
                int d=INF;
                for(int j=0;j<n;j++){
                	if(!vis_boy[j]) d=min(d,slack[j]);
    			}
    			for(int j=0;j<n;j++){
    				// 所有访问过的女生降低期望值
    				if(vis_girl[j]) ex_girl[j]-=d;
    				// 所有访问过的男生增加期望值
    				if(vis_boy[j]) ex_boy[j]+=d;
    				// 没有访问过的boy 因为girl们的期望值降低,距离得到女生倾心又进了一步
    				else slack[j]-=d;
    			}
    		}
    	}
    	int res=0;
    	for(int i=0;i<n;i++) res+=love[match[i]][i];
    	return res;
    }
    
    
    int main(){
    	while(scanf("%d",&n)!=EOF){
    		for(int i=0;i<n;i++){
    			for(int j=0;j<n;j++) scanf("%d",&love[i][j]);
    		}
    	}
    	printf("%d
    ",km());
    return 0;
    }
    

      

    费用流解决最优匹配

    两种不同的建图方式,可以得到不同的解

    (1)按照求最大匹配的建边方式,求出来的是最佳匹配(一定是完备匹配),因为S-X集合的每一条边都满流

    建图:

    添加超级源点S,超级汇点T,从S向二分图X集合中的每个顶点连接一条权值为0,容量为1的有向边,从Y集合向T连接一条权值为0,容量为1的有向边,然后把原有的边变成容量为1,权值不变的有向边,求从S到T的最小(大)费用流,就能得到最小(大)匹配

    例题:

    【n个玩具在m台机器上面加工,给出每个玩具在每台机器上面的加工时间,每台机器同一时间只能加工一个玩具,求加工完所有的玩具所需的平均时间】

    这道题的建边思想也很不错,不知道前面有多少个玩具在加工,那么就枚举耽误了后面的玩具多少时间,枚举自己是倒数第i个加工的,那么所需总时间就是i*time

    所以玩具i连接工厂j要连n条边,边权分别是k*time[i][j](k=1,2,3...n)

    #include<iostream>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<stack>
    #include<cstdio>
    #include<queue>
    #include<map>
    #include<vector>
    #include<set>
    using namespace std;
    const int maxn=1010;
    int INF=0x3fffffff;
    typedef long long LL;
    typedef unsigned long long ull;
    //这道题的建边思想也很不错,不知道前面有多少个玩具在加工,那么就枚举耽误了后面的玩具多少时间,枚举自己是倒数第i个加工的,那么所需总时间就是i*time
    //所以玩具i连接工厂j要连n条边,边权分别是k*time[i][j](k=1,2,3...n) 
    struct node{
    	int x,y,flow,cost,nex,op; //op是相反的那条便 
    }mp[1500000];
    int tot,st,ed,n,m,head,tail,test;
    double ans;
    int que[200000000],headd[3000],f[3000],pre[3000];
    bool vis[3000];
    int data[51][51];
    void build(int x,int y,int cost){
    	tot++;
    	mp[tot].x=x;mp[tot].y=y;mp[tot].flow=1;mp[tot].cost=cost;
    	mp[tot].op=tot+1;
    	mp[tot].nex=headd[x];
    	headd[x]=tot;
    	tot++;
    	mp[tot].x=y;mp[tot].y=x;mp[tot].flow=0;mp[tot].cost=-cost;
    	mp[tot].op=tot-1;
    	mp[tot].nex=headd[y];
    	headd[y]=tot;
    }
    void inti(){
    	scanf("%d %d",&n,&m);
    	for(int i=1;i<=n;i++){
    		for(int j=1;j<=m;j++) scanf("%d",&data[i][j]);
    	}
    	tot=0;
    	memset(headd,0,sizeof(headd));
    	for(int i=1;i<=n;i++){
    		int temp=n; //要用这后面的数字 
    		for(int j=1;j<=m;j++){
    			for(int k=1;k<=n;k++){
    				temp++;
    				build(i,temp,data[i][j]*k);
    			}
    		}
    	}
    	st=n+n*m+1;
    	ed=st+1;
    	//超级源点连接 
    	for(int i=1;i<=n;i++) build(st,i,0);
    	for(int i=n+1;i<=n*m+n;i++) build(i,ed,0);
    	ans=0;
    	memset(pre,0,sizeof(pre));
    }
    bool spfa(){
    	memset(f,100,sizeof(f));
    	INF=f[0];
    	memset(vis,false,sizeof(vis));
    	head=1,tail=1;
    	f[st]=0;vis[st]=1;
    	que[1]=st;
    	pre[st]=-1;
    	while(head<=tail){ //找增广路 
    		int x=que[head];
    		vis[x]=0;
    		for(int i=headd[x];i;i=mp[i].nex){
    			int y=mp[i].y;
    			if(f[x]+mp[i].cost<f[y]&&mp[i].flow){
    				f[y]=f[x]+mp[i].cost;
    				pre[y]=i;  //pre是记录的边!!!! 
    				if(vis[y]==false){
    					vis[y]=true;
    					tail++;
    					que[tail]=y;
    				}
    			}
    		}
    		head++;
    	}
    	if(f[ed]<INF){
    		int t=pre[ed];
    		ans+=f[ed];
    		while(t!=-1){
    			mp[t].flow-=1;
    			mp[mp[t].op].flow++;
    			t=pre[mp[t].x];
    		}
    		return true;
    	}
    	else return false;
    }
    int main(){
    	scanf("%d",&test);
    	while(test--){
    		inti();
    		while(spfa()){
    			ans=ans/n;
    			printf("%.6lf
    ",ans);
    		}
    	}
    return 0;
    }

    (2)如果要求最大权匹配(不一定完备匹配),那么只需要再引入一个顶点A,从X集合的每个顶点向A连接一条容量为1,权值为0的边,然后再有A向T连接一条权值为0,容量不小于|X|的边,求最大费用最大流(分流思想)

  • 相关阅读:
    STL整理之map
    链表及数组模拟链表
    树链剖分详解
    Luogu P3879 【[TJOI2010]阅读理解】
    Luogu P2727 【01串 Stringsobits】
    CF1200D 【White Lines】
    Luogu P4945 【最后的战役】
    Luogu P4944 【PION贪吃蛇】
    Luogu P2426 【删数】
    P2163 【[SHOI2007]园丁的烦恼】
  • 原文地址:https://www.cnblogs.com/shirlybaby/p/12869906.html
Copyright © 2011-2022 走看看