二分图匹配相关算法及例题分析 最大匹配匈牙利算法 最大权匹配KM算法(二分图类型问题汇总)

时间:2022-03-16 06:47:34

二分图最大匹配:

问题描述:给出一个二分图,找一个边数最大的匹配。就是选择尽量多的边,使得选中的边中任意两条边均没有公共点。如果所有的点都是匹配点那就是一个完美匹配。

解决方案:增广路定理

增广路:从一个未匹配的点开始,依次走过未匹配边,匹配边,未匹配边,匹配边,。。。。。。 如果最后的终点是一个未匹配点(即最后一条边是一条未匹配边),那么这条路就是一条增广路。而将增广路上的未匹配边和匹配边进行互换,就会使得匹配边多一条,如图:

二分图匹配相关算法及例题分析 最大匹配匈牙利算法 最大权匹配KM算法(二分图类型问题汇总)

由此可知,存在一条增广路就可以将匹配的边数加一。那么一个匹配是最大匹配的充要条件便是不存在增广路。此定理不仅仅适用于二分图。

经典例子:n*m 的棋盘上放着一些棋子,要求保留最多的棋子使得任意两枚棋子都不在同一行或者同一列。这时候可以将二分图的左图表示每一行,右图表示每一列。棋盘(x,y)位置有棋子则表示左图中代表x行的点与右图中代表y列的点有一条边相连。若两条边同时依赖于同一点则表示两个棋子在同一行或者同一列,最多的棋子数就是最大匹配数。

关于最大匹配数的一些结论:

1,最大匹配数 = 最小顶点覆盖(选取最少的点,使得点集中的点能覆盖所有的边) 

证明:首先要明确的一点是最小顶点覆盖一定不会小于最大匹配数。显而易见,最大匹配中的每一个匹配都是不相邻的一条边,如果最大匹配数为n,那么至少需要n个顶点来覆盖这n条边。

剩下的证明相等,学习时看了许多博客,怎么形容呢,一头雾水,no picture you say a jb? 

两种情况:

二分图匹配相关算法及例题分析 最大匹配匈牙利算法 最大权匹配KM算法(二分图类型问题汇总)

 

2,顶点数 - 最小顶点覆盖 = 最小边覆盖 (边最少的情况下,边集中的边覆盖所有的点)

证明:设顶点数为 n ,最大匹配为 a。因为要使得边最少,所以先选择最大匹配的边(一次可以覆盖两个点) ,剩下的每个点都需要一条边去覆盖,设剩下的为 b = n -2 * a  。而最小边覆盖此时等于 a + b =n - a,证完。

3,顶点数 = 最大匹配 + 最大独立集 (点集,其中的点互不相连)

证明:顶点中去除最大匹配的点剩下的就是孤立的点,就是最大独立集

匈牙利算法(二分图最大匹配算法)

给出模板及例题 poj3041

题意:在网格上给出一些点,每次可以清除一行或一列的点,清除次数最少的情况下清除所有的点。用第一个结论最小顶点覆盖 = 最大匹配数。

/*zhizhaozhuo 匈牙利算法模板 && poj3041*/
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1e3;
int Map[maxn][maxn],vis[maxn],pre[maxn];
void init(){
	memset(Map,0,sizeof(Map));
	memset(pre,0,sizeof(pre));
}
bool find(int x,int m){
	int i,j;
	for(int i=1;i<=m;i++){  //遍历右图 
		if(Map[x][i] && !vis[i]){ //x->i 有边且 i 未被访问 
			vis[i]=1;
			if(pre[i]==0 || find(pre[i],m)){ //i 未匹配且 通过 i 结点可以找到增广路 
				pre[i]=x;
				return true;
			}
		}
	}
	return false;
}
int solve(int n,int m){
	int sum=0;
	for(int i=1;i<=n;i++){  //遍历左图 
		memset(vis,0,sizeof(vis));//清空标记 
		if(find(i,m))sum++;  
	}
	return sum;
}
int main(){
	int n,k;
	scanf("%d%d",&n,&k);
	init();
	while(k--){
		int a,b;
		scanf("%d%d",&a,&b);
		Map[a][b]=1;
	}
	int ans=solve(n,n);
	printf("%d\n",ans);
	return 0;
}

 

二分图最大权匹配:

前面所说的最大匹配可以看做是权值均为1的最大权匹配,最大权匹配就是带权的最大匹配。

原理及证明:

完备(美)匹配:二分图中左右子图中的各点都有对应的匹配。

KM算法是通过给每一个点一个顶标来把求最大权匹配的问题转换为求完备匹配的问题。

设左图的顶标为A[i] ,右图的顶标为B[j] ,顶点之间的边权为 w[i] [j]。可行性顶标 A[i] +B[j] >= w(i,j),即顶标始终满足对于任意的一条边,它连接的两个顶点的顶标和大于等于该边的边权,初始时左图中点的顶标等于与该点相连的边中边权最大的值,右图中点的顶标等于0.而将最大权匹配转换为完备匹配的关键定理就是:若由二分图中所有满足A[i] +b[j]=w[i][j] 的边<i,j> 构成的子图(也称相等子图) 有完备匹配,那么这个完备匹配就是二分图的最大权匹配。 因为完备匹配中的边都是等于两端点的顶标和的,而根据之前对顶标的定义可知非完备匹配中的边小于等于两端点的顶标和。

如果当前的子图中不存在完备匹配,就修改顶标的值再求完备匹配。

顶标的修改方法:在增广路径(交错树)上的左图集合中的点 i 减去一个值alter ,右图集合中的点 j 加上alter。

分为四种情况:1,i,j都属于增广路,A[i] - alter + B[j] + alter =w(i,j) 边的可行性不变,即原来是相等子图的边现在仍是,不是仍不是。

                         2,i属于增广路,j不属于增广路,A[i] - alter + B[j] ,值就变小了,那么此时这条不属于相等子图的边(属于早就被 j 遍历到了)就有可能加入到相等子图中。

                         3,如果i不属于增广路,j属于增广路,A[i] + B[j] +alter ,值变大了,这条不属于相等子图的边就更不可能加入了。

                          4,i,j都不属于增广路则不对其进行任何操作,可行性不变。

修改量alter的取值:因为修改顶标是为了将边加入构成相等子图,所以可以看出是选择上面的第二种情况。而且要满足

A[i] +B[j] >= w(i,j)    即:     A[i] - alter + B[j] >= w(i,j)   即:alter >= A[i] +B[j] - w(i,j) 即 alter =min( A[i] +B[j] - w(i,j) )

(注意此时要求i属于增广路,j不属于增广路  )

模板及例题: Hdu-2255,直接套就行

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1e3,INF=1e9;

int W[maxn][maxn],n;
int Lx[maxn],Ly[maxn];//顶标 
int Left[maxn];//右边第i个点对应的左边的点的编号 
bool S[maxn],T[maxn];//是否在增广路 

bool match(int i){
	S[i]=true;
	for(int j=1;j<=n;j++)if(Lx[i]+Ly[j]==W[i][j] && !T[j]){//i到j可行 且 j未被访问 
		T[j]=true;
		if(!Left[j] || match(Left[j])){ //j未标记 或者 通过j可以找打增广路 
			Left[j]=i;return true;
		}
	}
	return false;
}
void update(){//更新顶标 
	int a=INF;
	for(int i=1;i<=n;i++)if(S[i]){
		for(int j=1;j<=n;j++)if(!T[j])a=min(a,Lx[i]+Ly[j]-W[i][j]);//i在增广路 且 j不在增广路中 
	}
	for(int i=1;i<=n;i++){
		if(S[i])Lx[i]-=a;
		if(T[i])Ly[i]+=a;
	}
}
void KM(){
	for(int i=1;i<=n;i++){
		Left[i]=Lx[i]=Ly[i]=0;
		for(int j=1;j<=n;j++)Lx[i]=max(Lx[i],W[i][j]);
	}
	for(int i=1;i<=n;i++){
		for(;;){
			for(int j=1;j<=n;j++)S[j]=T[j]=0;
			if(match(i))break;else update();
		}
	}
}

int main(){
	while(~scanf("%d",&n)){
		for(int i=1;i<=n;i++){
		    for(int j=1;j<=n;j++)scanf("%d",&W[i][j]);
	    }
	    KM();
	    int sum=0;
	    for(int i=1;i<=n;i++)sum+=W[Left[i]][i];
		printf("%d\n",sum);
	}
	return 0;
}