Loading

学习笔记——二分图及建图技巧

简介

二分图是一种特殊的图。其定义为:

节点由两个集合组成,且两个集合内部没有边的图。

那啥时候可以用二分图呢?当且仅当图可以进行合法的黑白染色的时候,可以考虑二分图。

二分图匹配

匈牙利算法

最基础的问题就是二分图的最大匹配。所谓最大匹配,就是对于左右两个集合内的点,每个点都只能选一次,并且左右匹配的点之间有连边,求最多能左右匹配的点对数。举个最简单的例子,就是有 \(n\) 个男生和 \(m\) 个女生,之间有相互喜欢的关系,求最终最多能牵几根红线。

由于这个问题比较基础,所以具体可以看网上其它博客,这里仅作简述。

我们常用的方法是匈牙利算法,这个算法,你可以感性地将之理解为一个有礼貌地戴绿帽的过程。

算法流程

  • \(1\sim n\) 便利每个男生。
  • 对于每个男生,先随便找一个两情相悦的女生。然后分两种情况:
    \(1\)、 这个女生没有匹配,那么就匹配上。
    \(2\)、 这个女生已经匹配了,那么就礼貌地问问那个匹配她的男生:“你好,我可以把你戴绿帽吗?”然后问题转化为那个可悲的被戴绿帽的男生去匹配其他女生的问题,可以递归解决。
  • 然后如果可以通过一连串的戴绿帽,就可以使答案加一。

这就是匈牙利的全过程,更具体的推荐扶咕咕的题解

Code

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
using namespace std;
const int MAXN=510;
int n,m,e,mtc[MAXN];
bool used[MAXN],edge[MAXN][MAXN];
bool find(int x){
	for(int i=1;i<=m;i++){
		if(used[i]||!edge[x][i]) continue;
		used[i]=1;//标记在当前处理时,该女生是否访问。
		if(mtc[i]==0||find(mtc[i])){
			mtc[i]=x;
			return 1;
		}
	}return 0;
}
int main()
{
	scanf("%d%d%d",&n,&m,&e);
	for(int i=1,u,v;i<=e;i++){
		scanf("%d%d",&u,&v);
		edge[u][v]=1;
	}int ans=0;
	memset(mtc,0,sizeof(mtc));
	for(int i=1;i<=n;i++){
		memset(used,0,sizeof(used));
		if(find(i)) ans++;
	}printf("%d\n",ans);
}

Ps:一个优化
显然,匈牙利的复杂度是 \(O(n^2)\) 的,所以有人看到百万级别的题就不会往二分图匹配上去想了。这显然不对,因为有的题目每个点连出去的边只有几条,但是点却是上万的,此时,如果在遍历的时候用 \(vector\) 或者前向星存边,那么就可以只跑必定有边的匹配,可以大大降低复杂度。例如这道题,就可以这么做:

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
#define INF 1ll<<60
using namespace std;
const int MAXN=1e6+10;
vector<int> e[MAXN];
int vis[MAXN],mtc[MAXN];
bool find(int x,int t){
	for(int i=0;i<e[x].size();i++){
		int s=e[x][i];
		if(vis[s]==t) continue;
		vis[s]=t;//记录时间戳
		if(mtc[s]==-1||find(mtc[s],t))
		{mtc[s]=x;return 1;}
	}return 0;
}
int main()
{
	int n,a,b,mx=0;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d%d",&a,&b);
		e[a].push_back(i);
		e[b].push_back(i);
		mx=max(mx,max(a,b));
	}
	memset(mtc,-1,sizeof(mtc));
	for(int i=1;i<=mx;i++){//如果 memset 就太慢了。
		if(!find(i,i)){//注意这里把 vis 改成 int 类型的,这样就不用 memset 而可以用记录时间戳取代之。
			printf("%d\n",i-1);
			return 0;
		}
	}printf("%d\n",mx);
}

建图技巧

trick1:二分图矩阵模型
题目一般是给出一个矩阵,然后给出行列的限制,求最大能放几个合法的点。先看例题吧。
ZJOI矩阵游戏

题目中可以推断出一个简单的结论:每行每列必须至少有一个黑色点。

那么我们可以把行列拆开,作为左右部图,然后如果 \(i\)\(j\) 列是 \(1\),那么说明第 \(i\)\(j\) 列已经有一个黑色的点,那么就把 \(i\) 行向 \(j\) 列建边。最后跑一次匈牙利,就可以了。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=210;
bool used[MAXN],mp[MAXN][MAXN];
int n,mtc[MAXN];
bool find(int x){
	for(int i=1;i<=n;i++){
		if(mp[x][i]&&!used[i]){
			used[i]=1;
			if(mtc[i]==0||find(mtc[i]))
			{mtc[i]=x;return 1;}
		}
	}return 0;
}
int main()
{
	int T;
	for(scanf("%d",&T);T--;){
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				scanf("%d",&mp[i][j]);
		int ans=0;
		memset(mtc,0,sizeof(mtc));
		for(int i=1;i<=n;i++){
			memset(used,0,sizeof(used));
			ans+=find(i);
		}
		puts(ans==n?"Yes":"No");
	}
}

trick2:匹配时记录路径
有的聚聚可能说了,我二分图直接用网络流跑不就好了,又快又直接。当然可以,不过如果要记录匹配路径的话,就会麻烦了,因为网络流的常用算法 \(dinic\) 是利用反向边回流实现的。

例题

这题很显然是要输出路径的,只要在跑匈牙利的时候,成功匹配就记录一下就可以了。这题的建图应该是裸的吧?

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
#define INF 1ll<<60
using namespace std;
const int MAXN=1e4+10;
vector<int> e[MAXN];
int mtc[MAXN],vis[MAXN],pp[MAXN];
bool find(int x){
	for(int i=0;i<e[x].size();i++){
		int s=e[x][i];
		if(vis[s]) continue;
		vis[s]=1;
		if(mtc[s]==-1||find(mtc[s]))
		{mtc[s]=x;pp[x]=s;return 1;}
	}return 0;
}
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=0,d;i<n;i++){
		scanf("%d",&d);
		int t1=(i+d+n)%n,t2=(i-d+n)%n;
		if(t1>t2) swap(t1,t2);
		e[i].push_back(t1);
		e[i].push_back(t2);
	}
	memset(mtc,-1,sizeof(mtc));
	int ans=0;
	for(int i=n-1;i>=0;i--){//具体题目要求,该题需要最小的字典序
		memset(vis,0,sizeof(vis));
		ans+=find(i);
	}
	if(ans<n){
		puts("No Answer");
		return 0;
	}
	printf("%d",pp[0]);
	for(int i=1;i<n;i++)
		printf(" %d",pp[i]);
}

最大独立集

二分图最大独立集,就是在整个二分图中选出最多的点,使得选出的点中没有连边。

解决方式

二分图最大独立集 = 二分图总点数 - 二分图最大匹配数

感性理解就是,对于每个匹配,两边只能选一个点,因为已经是最大匹配,所以我对于有匹配的只选一边,这样可以保证其他点是独立的,否则与最大匹配相违背。

建图技巧

trick1:黑白染色建图

比如这题,显然不是 2-sat 问题,所以考虑最大独立集。

但是题目并没有说明先输入的是男生还是女生,所以要先建图,然后跑大法师先染色。然后再跑最大独立集。

但是如果是矩阵中非行列限制,而是一些奇怪的走位,比如这题,同样我们也要用最大独立集来解决,此时就不用跑染色,直接复制一份,并在最后求答案时,把匹配数除以二就可以了。

下面贴上面两题的代码:

//P6268
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
#define INF 1ll<<60
using namespace std;
const int MAXN=1010;
int mtc[MAXN];//1 boy|2 girl
bool mp[MAXN][MAXN],vis[MAXN];
vector<int> boy,girl,e[MAXN];
bool find(int x){
	for(int i=0;i<girl.size();i++){
		int s=girl[i];
		if(mp[x][s]&&!vis[s]){
			vis[s]=1;
			if(mtc[s]==0||find(mtc[s])){
				mtc[s]=x;return 1;
			}
		}
	}return 0;
}
void dfs(int x,bool fl){
	vis[x]=1;
	if(fl) girl.push_back(x);
	else boy.push_back(x);
	for(int i=0;i<e[x].size();i++){
		int s=e[x][i];
		if(vis[s]) continue;
		dfs(s,!fl);
	}
}
int main()
{
	int n,m,a,b;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&a,&b);
		a++;b++;
		e[a].push_back(b);
		e[b].push_back(a);
		mp[a][b]=mp[b][a]=1;
	}
	for(int i=1;i<=n;i++)
		if(!vis[i]) dfs(i,0);
	memset(mtc,0,sizeof(mtc));
	int ans=0;
	for(int i=0;i<boy.size();i++){
		memset(vis,0,sizeof(vis));
		if(find(boy[i])){
			ans++;
		}
	}
	printf("%d\n",n-ans);
}

//P4304
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
#define INF 1ll<<60
using namespace std;
const int MAXN=4e4+10;
vector<int> e[MAXN];
int vis[MAXN],mtc[MAXN];
bool find(int x,int t){
	for(int i=0;i<e[x].size();i++){
		int s=e[x][i];
		if(vis[s]==t) continue;
		vis[s]=t;
		if(mtc[s]==-1||find(mtc[s],t))
		{mtc[s]=x;return 1;}
	}return 0;
}
int dx[]={-1,-2,1,2,-1,-2,1,2},
	dy[]={-2,-1,-2,-1,2,1,2,1};
int mp[210][210],n;
int X(int x,int y){return (x-1)*n+y;}

int main()
{
	char ch;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			scanf(" %c",&ch);
			mp[i][j]=ch-'0';
		}
	}
	int sum=0;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(!mp[i][j]){
				sum++;
				for(int k=0;k<8;k++){
					int ii=i+dx[k],jj=j+dy[k];
					
					if(ii>=1&&ii<=n&&jj>=1&&jj<=n&&!mp[ii][jj])
//						cerr<<X(i,j)<<' '<<X(ii,jj)<<'\n',
						e[X(i,j)].push_back(X(ii,jj));
				}
			}
	int ans=0;
	memset(mtc,-1,sizeof(mtc));
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(!mp[i][j])
				ans+=find(X(i,j),X(i,j));
	printf("%d\n",sum-ans/2);
}
posted @ 2021-04-13 20:30  ZCETHAN  阅读(235)  评论(0编辑  收藏  举报