猫史档案馆


【每日不刷通天塔 蒟蒻也能懂算法】增广路匈牙利算法

用户:爵士OIer爵士OIer查看:0 回复:1 评论:0 创建时间:2020-05-04T15:25:00


 

 

 

二分图

 

center_image

如图,一个图的点集可以划分为两个不相交的子集,每一个子集中的点和该子集中的其他点没有边相连的图就叫二分图。比如上面有两个点集u,v符合上述条件,则它是一个二分图。

 

 

二分图的判定

 

 · 定理:一个无向图是二分图,当且仅当图中不存在奇环(长度为奇数的环)

 · 根据该定理,可用染色法判定二分图:尝试用黑、白两种颜色标记图中的节点,当一个节点被标记后,它的所有邻接点应被标记为与它相反的颜色。若发生冲突,说明图中存在奇环。二分图染色一般基于DFS实现,时间复杂度为O(N+M)。

 · 判定流程伪代码如下:

void dfs(int x,int color){  
    v[x]=color; //对点x染色  
    //遍历x相连的无向边(x,y)  
        if (v[y]==0) //未染色  
            dfs(y,3-color); //黑1,白2,3相减取反色  
        else if (v[y]==color)  
            //该无向图不是二分图,算法结束  
    //是无向图,算法结束  
}  
//在主函数中  
    for (int i=1;i<=n;i++)  
        if (v[i]==0) dfs(i,1);  

 

 

二分图匹配

 

 · 【任意两条边都没有公共端点】的边的集合称为图的匹配

 · 最大匹配(maximal matching problem)就是包含边的个数最多的匹配。center_image

上面是一种匹配方式,但是不一定是最大匹配。

二分图匹配的用途很多,如自来水管、车站打车等。

 

 

增广路

 

 · 对于任意一组匹配M(M是一个边集),属于M的边被称为“匹配边”,不属于M的边被称为“非匹配边”。

 · 匹配边的端点被称为M的“饱和点”,其它节点被称为“非饱和点”

 · p是G的一条通路,如果p中的边为属于M中的匹配边与不属于M的非匹配边交替出现,则称p是一条M-交错路。

 · p是一条M-交错路,如果p的起点和终点都是非M-饱和点,则称p为M-增广路。

center_imagecenter_image

 

如上两图,左图是一种匹配。右图是一种更大的匹配。

 · 增广路显然具有以下性质

      1.长度len是奇数

      2.路径上第1,3,5,……,len条边是非匹配边,第2,4,6,……,len-1条边是匹配边。

 · 如果将增广路p上所有边的状态取反,即原来的边变为非匹配边,原来的非匹配边变成匹配边。那么新得到的边集M’仍然是一组匹配,并且匹配边数增加了1。由此可得推论:

 · 二分图的一组匹配M是最大匹配,当且仅当图中不存在M的增广路。

center_image

 

 

 

匈牙利算法(增广路算法)

 

 · 匈牙利算法,又称增广路算法,用于计算二分图最大匹配。其主要过程为:

       1.设M为空集,即所有边都是非匹配边。

       2.寻找M的增广路p,把路径上的所有边的匹配状态取反,得到一个更大的匹配M’。

       3.重复第2步,直至图中不存在M的增广路。

 

 · 该算法的关键在于如何找到一条增广路:依次尝试给每一个左部节点x寻找一个匹配的右部节点y。右部点y能与左部点x匹配:需要满足以下两个条件之一:

       1.y本身就是非匹配点

          此时无向边(x,y)本身就是非匹配边,自己构成一条长度为1的增广路。

       2.y已经与左部点x’匹配,但从x’出发能找到另一个右部点y’与之匹配。

          递归进入该左部点,为其寻找匹配的右部点,此时路径x~y~x’~y’为一条增广路。

 · 在实际的程序实现中,可采用DFS的框架,递归地从x出发寻找增广路。若找到,则在深搜回溯时,正好把路径上的匹配状态取反。另外可以用全局bool数组标记节点的访问情况,避免重复搜索。

 

下面贴出具体流程,方便理解。

center_imagecenter_image  center_imagecenter_imagecenter_imagecenter_imagecenter_imagecenter_image

图一到图五很好理解,不讲;

可以看到,在图六中出现了两条红色的边。搜索到第四个白色点时,出现了下面这条红色的边,占用第三个黑色点。因此,第二个白色点重新建边,搜索与第二个白色点相邻的黑色点,搜索到了第一个黑色点,出现上面这条红色的边,发现这个黑色点如果被占用,那么第一个白色点将会无法建边,回溯;

图七,第二个白色点搜索与没有被占用且与其联通的第四个黑色点,建边(图七中新出现的红色边),形成增广路。

至此,DFS过程结束。形成图八那样的一种从第一个白色点出发的最大匹配。

 

 · 匈牙利算法的正确性基于贪心策略,它的一个重要特点是:当一个节点称为匹配点后,至多因为找到增广路而更换匹配对象,但是绝不会再变回非匹配点。

 · 对于每个左部节点,寻找增广路最多遍历整张二分图一次。因此,该算法的时间复杂度为O(NM)。

 

Code:

bool dfs(int x){  
    int i,y;  
    for(i=head[x]; i>0; i=next[i]) //考虑与x相连的每个点y=ver[i]  
        if(!visit[y=ver[i]]){//没有被搜索过    
            visit[y]=1;  
            if(!match[y] || dfs(match[y])){
		   match[y]=x; return true;
	     }  
        }  
    return false;  
}

//下面是主函数核心部分
for(int i=1;i<=n;i++){  
    memset(visit,0,sizeof(visit)); //清空访问标记  
    if(dfs(i)) ans++; //从左部点i出发寻找增广路  
}  

 

 

二分图匹配的模型

 

 · 二分图匹配的模型有两个要素

      1.节点能分成两个独立的集合,每个集合内部有0条边。

      2.每个节点只能与1条匹配边相连。

 

 

 

例题:匈牙利算法-棋盘上的问题

 

还记得开篇的n皇后和状压DP吗?

 

棋盘覆盖

 

问题描述

给定一个N行N列的棋盘,已知某些格子禁止放置。

求最多能往棋盘上放多少块的长度为2、宽度为1的骨牌,骨牌的边界与格线重合(骨牌占用两个格子),并且任意两张骨牌都不重叠。

 

输入格式

第一行包含两个整数N和t,其中t为禁止放置的格子的数量。

接下来t行每行包含两个整数x和y,表示位于第x行第y列的格子禁止放置,行列数从1开始。

 

输出格式

输出一个整数,表示结果。

 

数据范围

1≤N≤100

输入样例

 

8 0

输出样例

32

 

相信大家一看到就以为是状压DP吧(雾

我们怎么用二分图的匈牙利算法?看下:

棋盘黑白染色

相邻格子连边

1*2骨牌对应一条匹配边

这个用特殊字体区分的就是简单思路。

#include<iostream>
#include<cstdio>
#include<cstring>
#define N 105
#define M 1000005
using namespace std;
struct E{int next,to;}e[M];
int n,m,num,ans;
int h[N*N],mat[N*N];
int dx[5]={0,-1,1,0,0};
int dy[5]={0,0,0,-1,1};
bool vis[N*N];
bool tag[N][N];
void add(int u,int v){
	e[++num].next=h[u];
	e[num].to=v;
	h[u]=num;
}
int cal(int x,int y){return(x-1)*n+y;}
bool dfs(int x){
	for(int i=h[x];i!=0;i=e[i].next)
		if(!vis[e[i].to]){
			vis[e[i].to]=1;
			if(!mat[e[i].to]||dfs(mat[e[i].to])){
				mat[e[i].to]=x;
				return 1;
			}
		}
	return 0;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int x,y;
		cin>>x>>y;
		tag[x][y]=1;
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			for(int k=1;k<=4;k++){
				int x=i,y=j;
				int xx=x+dx[k],yy=y+dy[k];
				if(xx<1||xx>n||yy<1||yy>n)continue;
				if(tag[x][y]||tag[xx][yy])continue;
				add(cal(x,y),cal(xx,yy));
				add(cal(xx,yy),cal(x,y));
			}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if((i+j)%2){
				memset(vis,0,sizeof(vis));
				if(dfs(cal(i,j)))ans++;
			}
	cout<<ans;
	return 0;
}

 

巨的放置

 

题目描述

给定一个N行M列的棋盘,已知某些格子禁止放置。

问棋盘上最多能放多少个不能互相攻击的車。

車放在格子里,攻击范围与中国象棋的“車”一致。

 

输入格式

第一行包含三个整数N,M,T,其中T表示禁止放置的格子的数量。

接下来T行每行包含两个整数x和y,表示位于第x行第y列的格子禁止放置,行列数从1开始。

 

输出格式

输出一个整数,表示结果。

 

数据范围

1≤N,M≤200

 

输入样例

8 8 0

输出样例

8

 

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
int n,m,t;
const int N=100000;
int head[N],nex[N],to[N],cnt;
bool g[220][220];
int match[N];
bool vis[N];
void init(){
	mem(head,-1);
	mem(nex,-1);
	mem(to,-1);
	cnt=0;
	mem(match,-1);
	mem(vis,false);
	mem(g,true);
}
void add(int a,int b){
	++cnt;
	nex[cnt]=head[a];
	head[a]=cnt;
	to[cnt]=b;
}
bool dfs(int p){
	for (int i=head[p];i!=-1;i=nex[i]){
		int y=to[i];
		if(vis[y])continue;
		vis[y]=true;
		if (match[y]==-1||dfs(match[y])){
			match[y]=p;
			return true;
		}
	}
	return false;
}
int main(){
	init();
	cin >>n>>m>>t;
	while(t--){
		int x,y;
		cin>>x>>y;
		g[x][y]=false;
	}
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			if (g[i][j]){
				add(i,j+n);
				add(j+n,i);
			}
		}
	}
	int res=0;
	for (int i=1;i<=n;i++){
		mem(vis,false);
		if(dfs(i))res++;
	}
	cout<<res<<"\n";
	return 0;
}

 

例题:Place the Robots

 

问题描述

有一个N*M(N,M<=50)的棋盘,棋盘的每一格是三种类型之一:空地、喵地、墙。机器人只能放在空地上。在同一行或同一列的两个机器人,若它们之间没有墙,则它们可以互相攻击。问给定的棋盘,最多可以放置多少个机器人,使它们不能互相攻击。

center_image

把每一行、每一列被墙隔开,并且包含空地的连续区域称作“块”。

显然,在一个块之中,最多只能放一个机器人。把这些块编上号。

把每个横向块看作X部的点,竖向块看作Y部的点,若两个块有公共的空地,则在它们之间连边。

由于每条边表示一个空地,有冲突的空地之间必有公共顶点,所以问题转化为二部图的最大匹配问题。

 

center_image


回复

上一页1 页 / 共 1下一页
爵士OIer爵士OIer

dd

点赞0


评论