【学习笔记】二分图

其实就是把博主记不住的概念明确一下

二分图

图上的点可以完美的被分成两部分,使得每一个部分的点中互相没有连边

或者说:可以黑白染色、没有奇环

判定

直接 \(dfs\) 染色

\(Code\)

	inline void dfs1(int x)
	{
		for(int i=head[x];i;i=e[i].nxt) 
		{
			int t=e[i].to;
			if(col[t]==-1) col[t]=1-col[x],dfs1(t);
		}return ;
	}

这里的 \(col[\space]\) 就是表示每个点所属于的点集标号

我第一次写的是按照 \(0,1\) 这样走的,还可以有 \(1,2\) (就是在 \(col\) 计算的时候拿 \(3\) 减呗)

最大匹配

匹配:二分图的一个边集,使得边集中的所有边没有公共端点

最大匹配:边集中 \(size\) 最大的一个

匹配边:顾名思义,在匹配的边集中的边

增广路(交错路):一个从连接左部非匹配点和右部非匹配点的路径

使得匹配边和非匹配边交替出现,长度应为奇数

正是增广路的这个定义,使得我们可以在找到一条增广路的时候把路径上的所有的边取反,然后边集大小会 \(+1\)

求法:(两种)

1.网络最大流:\(Dinic\)

这种做法就是偷懒不想学二分图的人做的(博主一开始想直接搞网络流不学二分图来着)

代码放的是 \(LGOJ2055\) 假期的宿舍

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
	inline int read()
	{
		int res=0,f=1; char k;
		while(!isdigit(k=getchar())) if(k=='-') f=-1;
		while(isdigit(k)) res=res*10+k-'0',k=getchar();
		return res*f;
	}
	const int N=1010; 
	struct node{
		int nxt,to,lim;
	}e[N<<1];
	int head[N],cnt=1,dep[N],n,S,T,inf=1e15+10; 
	inline void add2(int u,int v,int w)
	{
		e[++cnt].lim=w; e[cnt].nxt=head[u]; e[cnt].to=v;
		return head[u]=cnt,void();
	}
	inline void add1(int u,int v,int w)
	{
		add2(u,v,w); add2(v,u,0);
		return ;
	}
	inline bool bfs()
	{
		queue<int> q; q.push(S); memset(dep,-1,sizeof(dep)); dep[S]=0;
		while(!q.empty())
		{
			int fr=q.front(); q.pop();
			for(int i=head[fr];i;i=e[i].nxt)
			{
				if(e[i].lim<=0) continue;
				int t=e[i].to;
				if(dep[t]!=-1) continue;
				dep[t]=dep[fr]+1; q.push(t);
			}
		}
		return dep[T]!=-1; 
	}
	inline int dfs(int now,int in)
	{
		if(now==T) return in; int out=0;
		for(int i=head[now];i;i=e[i].nxt)
		{
			if(e[i].lim<=0) continue;
			int t=e[i].to;
			if(dep[t]!=dep[now]+1) continue;
			int res=dfs(t,min(e[i].lim,in)); 
			e[i].lim-=res; e[i^1].lim+=res; 
			in-=res; out+=res; 
		}
		if(!out) dep[now]=0; 
		return out;
	}
	inline void prework()
	{
		memset(head,0,sizeof(head)); memset(e,0,sizeof(e)); cnt=1;
		return ;
	}
	int st[N],at[N];
	inline void work()
	{
		prework(); int num=0;
		n=read();  S=n<<1|1; T=S+1;
		for(int i=1;i<=n;++i) 
		{
			st[i]=read();
			if(st[i]) add1(i+n,T,1);
		}
		for(int i=1;i<=n;++i) at[i]=read();
		for(int i=1;i<=n;++i)
		{
			if(!st[i]||(st[i]&&!at[i])) add1(S,i,1),num++;
		}
		for(int i=1;i<=n;++i)
		{
			for(int j=1;j<=n;++j)
			{
				int fl=read();
				if(i==j||fl) add1(i,j+n,1);
			}
		}
		int ans=0;
		while(bfs()) ans+=dfs(S,inf);
		puts(num<=ans?"^_^":"T_T");
		return ;
	}
	signed main()
	{
		int T=read(); while(T--) work(); 
		return 0;
	}
}
signed main(){return yspm::main();}

2.匈牙利算法

就是每次dfs找增广路,如果找到了答案加 \(1\)

代码放的还是这个题的,大概可以考虑对比一下,匈牙利是有好处的:好背 \(+\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
	inline int read()
	{
		int res=0,f=1; char k;
		while(!isdigit(k=getchar())) if(k=='-') f=-1;
		while(isdigit(k)) res=res*10+k-'0',k=getchar();
		return res*f;
	}
	const int N=1010; 
	struct node{
		int to,nxt;
	}e[N<<1];
	int head[N],cnt,n,to[N];
	inline void add(int x,int y)
	{
		e[++cnt].nxt=head[x]; e[cnt].to=y;
		return head[x]=cnt,void();
	}
	inline void prework()
	{
		cnt=0; memset(head,0,sizeof head); memset(e,0,sizeof(e));
		memset(to,-1,sizeof(to));
		return ;
	}
	bool r[N][N],fl[N],at[N],vis[N];
	inline bool dfs(int x)
	{
		for(int i=head[x];i;i=e[i].nxt)
		{
			int t=e[i].to; if(vis[t]) continue;
			vis[t]=1; if(to[t]==-1||dfs(to[t])) return to[t]=x,1;
 		}
		return 0;
	}
	inline void work()
	{
		prework(); n=read();
		for(int i=1;i<=n;++i) fl[i]=read();
		for(int i=1;i<=n;++i) at[i]=read();
		int cnt=0; for(int i=1;i<=n;++i) if(!fl[i]) cnt++,at[i]=0; else if(!at[i]) cnt++;
		for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) r[i][j]=read(); 
		for(int i=1;i<=n;++i)
		{
			for(int j=1;j<=n;++j)
			{
				
				if(i==j&&fl[i]) add(i,i);
				else if(r[i][j]&&fl[i]) add(j,i);
			}
		}
		int ans=0;
		for(int i=1;i<=n;++i)
		{
			memset(vis,0,sizeof(vis));
			if(!at[i]&&dfs(i)) ans++;
		}
		puts(ans>=cnt?"^_^":"T_T");
		return ;
	}
	signed main()
	{
		int T=read(); while(T--) work();
		return 0;
	}
}
signed main(){return yspm::main();}

到了这里,考察上面两个板块的有一个题:SHOI2002舞会

博主比较菜,所以题目都是板子题(真的真的板子诶)

最小点覆盖

在二分图中的一个点集,使得图中所有边有端点在这个点集中(定义真的可懂)

\(König\) 定理:

二分图最小点覆盖集的大小 \(=\) 二分图最大匹配集的大小

先说构造最小点覆盖的方法:

  1. 求出最大匹配。

  2. 从右部每个未匹配点出发寻找增广路(一定失败,要不然就不是最大匹配了),标记访问过的节点。

  3. 取左部标记点、右部未标记点,构成一组最小覆盖。

画画图就可以理解了

然后是证明:

经过上述构造方法后

右部未匹配点一定是标记点——因为它们是出发点

左部未匹配点一定是未标记点——如果被标记则找到了增广路(两边都是未匹配点诶)

一对匹配点都被标记或者都未标记——因为左部匹配点只能通过右部到达

取左部标记点、右部未标记点,恰好使得每对匹配点被取走一个

所以数量值相等


匹配边一定被覆盖——每对匹配点取走一个

不存在连接两个未匹配点的边——否则出现仅包含1条边的增广路

连接左部匹配点和右部未匹配点的边——后者是出发点,前者一定被标记

连接左部未匹配点和右部匹配点的边——后者未被标记,否则存在增广路

所以所有的边都被覆盖了,大功告成

例题:UVA1194 - Machine Schedule

还是个板子题,读懂题意就可以了

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
	inline int read()
	{
		int res=0,f=1; char k;
		while(!isdigit(k=getchar())) if(k=='-') f=-1;
		while(isdigit(k)) res=res*10+k-'0',k=getchar();
		return res*f;
	}
	const int N=1e5+10;
	struct node{
		int to,nxt;
	}e[N<<1];
	int head[N],cnt,n,m,k;
	inline void add(int u,int v)
	{
		e[++cnt].nxt=head[u]; e[cnt].to=v;
		return head[u]=cnt,void();
	}
	int to[N]; bool vis[N];
	inline bool dfs(int x)
	{
		for(int i=head[x];i;i=e[i].nxt)
		{
			int t=e[i].to; if(vis[t]) continue;
			vis[t]=1; if(!to[t]||dfs(to[t])) return to[t]=x,1;
		}return 0;
	}
	int ans=0;
	inline void prework()
	{
		memset(head,0,sizeof(head));
		memset(e,0,sizeof(e));
		memset(to,0,sizeof(to)); 
		ans=0; cnt=0; 
		return ;
	}
	inline void work()
	{
		m=read(); k=read();
		for(int i=1;i<=k;++i)
		{
			read();
			int u=read(),v=read();
			add(u,v); 	
		}
		for(int i=1;i<=n;++i)
		{
			memset(vis,0,sizeof(vis)); 
			if(dfs(i)) ans++; 
		} cout<<ans<<endl;
		return prework();
	}
	signed main()
	{
		while(n=read()) work();
		return 0;
	}
}
signed main(){return yspm::main();}

最大独立集

定义:任意两点在图中都没有边相连的点集称为图的独立集。

定理:二分图最大独立集 \(=\) 图的点数 \(-\) 二分图最大匹配

证明:选出最多的点构成独立集

\(\Leftrightarrow\) 在图中去掉最少的点,使剩下的点之间没有边。

\(\Leftrightarrow\) 用最少的点覆盖所有的边,去掉的是最小覆盖。

挺好理解的吧

例题:骑士放置

给定一个\(n \times m\) 的矩形和几个禁止放置点,求最多可以放几个骑士

首先看出来是求最大独立集不难吧

建图就是对当前点和可以被攻击的点建图就行

一个比较好的 \(trick\)

我们发现骑士所在的位置和可以攻击到的位置的 \(id\) 是不一样的

(这里的 \(id\) 是横纵坐标之和)

这样就可以省略 \(dfs\) 染色的过程了,直接建图的时候看一下奇偶性就可以了

注意: 这里的障碍点不要写\(while(t--)\)

原因是 \(t\) 在后面统计答案的时候还要被减掉,如果减没了就完蛋了(笔者卡了半小时)

Code:

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
	inline int read()
	{
		int res=0,f=1; char k;
		while(!isdigit(k=getchar())) if(k=='-') f=-1;
		while(isdigit(k)) res=res*10+k-'0',k=getchar();
		return res*f;
	}
	const int N=10010;
	struct node{
		int to,nxt;
	}e[N<<6];
	int n,m,head[N],cnt,to[N],k;
	bool g[210][201],vis[N];
	inline void add(int u,int v)
	{
		e[++cnt].nxt=head[u],e[cnt].to=v;
		return head[u]=cnt,void();
	}
	inline int get(int x,int y){return m*(x-1)+y;}
	inline bool dfs(int x)
	{
		for(int i=head[x];i;i=e[i].nxt)
		{
			int t=e[i].to; if(vis[t]) continue;
			vis[t]=1; if(!to[t]||dfs(to[t])) return to[t]=x,1;
		}
		return 0;
	}
	inline void max_match()
	{
		int ans=n*m-k; 
		for(int i=1;i<=n;++i) 
		{
			for(int j=1;j<=m;++j)
			{
				if(g[i][j]||(i+j)%2==1) continue; 
				else 
				{
					memset(vis,0,sizeof(vis));
					if(dfs(get(i,j))) 
					{
						ans--;
					}
				}
			}
		} 
		return printf("%lld\n",ans),void();
	}
	int fx[8]={-1,-1,-2,-2,1,1,2,2};
	int fy[8]={-2,2,-1,1,-2,2,-1,1};
	inline bool in(int x,int y){return x>=1&&x<=n&&y>=1&&y<=m;}
	signed main()
	{
		n=read(); m=read(); k=read();
		for(int i=1;i<=k;++i) 
		{
			int x=read(),y=read(); g[x][y]=1;
		} 
		for(int i=1;i<=n;++i)
		{
			for(int j=1;j<=m;++j)
			{
				if((i+j)%2==1||g[i][j]) continue;
				for(int k=0;k<8;++k)
				{
					int tx=i+fx[k];
					int ty=j+fy[k];
					if(!in(tx,ty)||g[tx][ty]) continue;
					add(get(i,j),get(tx,ty));
				}
			}
		}
		max_match();
		return 0;
	}
}
signed main(){return yspm::main();}

猜你喜欢

转载自www.cnblogs.com/yspm/p/12637929.html