Pbri

二分图博弈学习笔记

二分图博弈学习笔记

参考:https://zhuanlan.zhihu.com/p/359334008

二分图博弈是一类博弈模型,他可以抽象为:给出一张二分图和起点 \(S\)\(A\)\(B\) 轮流进行操作,每次操作只能选择与上一个被选的点相邻的点,且不能选择已经选择过的点,无法选点的人输掉。问先手是否必胜。

先给出结论:考虑二分图的所有最大匹配,如果在所有的最大匹配的方案中都包含了起点 \(S\) ,那么先手必胜,否则先手必败。

证明:

充分性:包含起点 \(S\) 时,先手每一步都只需要沿着匹配的边走,而后手的下一步一定会走到一个匹配点。因为如果走到了非匹配点 \(p_n\) ,那么假设到现在的路径是 \(S\rightarrow p_1\rightarrow p_2\rightarrow...\rightarrow p_{n-1}\rightarrow p_n\) ,那么把目前已经经过的匹配 \(\{S-p_1,p_2-p_3...p_{n-2}-p_{n-1}\}\) 整体右移一下得到新的匹配 \(\{p_1-p_2,p_3-p_4...p_{n-1}-p_n\}\) ,后面的匹配不变,那么这个匹配依然是最大匹配,却不包含 \(S\) ,这与 \(S\) 是必包含点相矛盾。

必要性:不包含起点 \(S\) 时,考虑一个不包含 \(S\) 的最大匹配,先手第一步一定会走到匹配点上,否则这条边也可以加入匹配中。接下来后手可以继续沿着匹配边走,而先手一定不会走出这个匹配,因为假设走到了非匹配点 \(q_n\) ,目前的路径是 \(S\rightarrow q_1\rightarrow q_2\rightarrow...\rightarrow q_{n-1}\rightarrow q_n\) ,匹配是 \(\{q_1-q_2,q_3-q_4...q_{n-2}-q_{n-1}\}\) ,那么整体右移一下得到新的匹配 \(\{q_2-q_3,q_4-q_5...q_{n-1}-q_n\}\) ,然后还可以加入一条边 \((S,q_1)\) ,形成的匹配会更大,这与最大匹配相矛盾。

综上,我们证明了这个结论。

接下来考虑如何求出所有最大匹配必经点。我们定义一条取代边为: \(x\) 是未匹配点, \((y,z)\) 是匹配边, \(x\)\(y\) 之间有非匹配边,那么由 \(x\)\(z\) 连一条匹配边,意思是 \(x\) 可以取代 \(z\) 。然后 \(z\) 成为了非匹配点,继续找别的连边。这样我们先随便找一个最大匹配,然后把所有不在这个最大匹配中的点当作起点,然后沿着取代边遍历,这样遍历到的点都是非必经点,剩下的点都是必经点。

特别注意的是,在题目中,我们一般按照局面建点。

接下来是几道例题:

GYM102832H

每转一次,所有数位之和的奇偶性就会发生改变,所以符合二分图。那么直接按上述方法跑就行。

某经典题

题意:棋盘上有多个不允许经过的点,你要操控一个点行走,然后不能经过已经经过的点,然后最先不能走的人失败。

横纵坐标之和每走一次就会发生改变,所以也是二分图。

某正睿题

题意:棋盘上有多个不允许经过的点,你要操控两个象棋中的马,不能经过已经经过的点,最先不能走的人失败。

好吧其实和上面那个题没有任何区别,就是码量大了点,扔个代码就跑。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <queue>
#define FUP(i,x,y) for(int i=(x);i<=(y);i++)
#define FDW(i,x,y) for(int i=(x);i>=(y);i--)
#define FED(i,x) for(int i=head[x];i;i=ed[i].nxt)
#define pr pair<int,int>
#define mkp(a,b) make_pair(a,b)
#define fi first
#define se second
#define pb(x) push_back(x)
#define MAXN 100010
#define INF 0x3f3f3f3f
#define LLINF 0x3f3f3f3f3f3f3f3f
#define eps 1e-9
#define MOD 1000000007
#define ll long long
#define db double
using namespace std;
int read()
{
	int w=0,flg=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-'){flg=-1;}ch=getchar();}
	while(ch<='9'&&ch>='0'){w=w*10+ch-'0',ch=getchar();}
	return w*flg;
}
const int dx[8]={-2,-1,1,2,2,1,-1,-2};
const int dy[8]={1,2,2,1,-1,-2,-2,-1};
const bool is_print=0;
int n,m,K;
bool mp[20][20];
int head[100000],ednum=1;
struct edge{
	int frm,nxt,to,w;
	bool is;
}ed[3000000];
void add_Edge(int u,int v,int w,bool is)
{
	if(is_print) printf("%d %d\n",u,v);
	ednum++;
	ed[ednum].frm=u,ed[ednum].nxt=head[u],ed[ednum].to=v,ed[ednum].w=w,ed[ednum].is=is;
	head[u]=ednum;
}
int S,T,lev[MAXN],cur[MAXN],pp[MAXN];
bool bfs()
{
	memset(lev,0x3f,sizeof(lev));
	queue<int>que;
	que.push(S),lev[S]=0;
	while(!que.empty())
	{
		int u=que.front();que.pop();
		cur[u]=head[u];
		FED(i,u)
		{
			if(!ed[i].w) continue;
			int v=ed[i].to;
			if(lev[v]>lev[u]+1)
			{
				lev[v]=lev[u]+1;
				que.push(v);
			}
		}
	}
	if(is_print){FUP(i,1,T) printf("%d ",lev[i]);puts("");}
	return lev[T]!=INF;
}
int dfs(int u,int flow)
{
	if(u==T) return flow;
	int re=0;
	for(int &i=cur[u];i;i=ed[i].nxt)
	{
		int v=ed[i].to;
		if(lev[v]!=lev[u]+1) continue;
		int fl=dfs(v,min(flow,ed[i].w));
		ed[i].w-=fl,ed[i^1].w+=fl;
		flow-=fl,re+=fl;
		if(!flow) break;
	}
	return re;
}
bool in[100000],vis[100000];
int getid(int x,int y){return x*n+y;}
void mydfs(int u)
{
	if(vis[u]) return;
	if(is_print) printf("mydfs :u=%d\n",u);
	vis[u]=true;
	FED(i,u)
	{
		if(ed[i].to==S||ed[i].to==T) continue;
		int v=pp[ed[i].to];
		mydfs(v);
	}
}
namespace solve1{
	void solve()
	{
		S=1,T=n*(n+1)+1;
		FUP(x,1,n)
		{
			FUP(y,1,n)
			{
				if(mp[x][y]) continue;
				int id=getid(x,y);
				if((x+y)&1) add_Edge(S,id,1,0),add_Edge(id,S,0,0);
				else add_Edge(id,T,1,0),add_Edge(T,id,0,0);
				if((x+y)&1)
				{
					FUP(i,0,7)
					{
						int nx=x+dx[i],ny=y+dy[i];
						if(nx<1||nx>n||ny<1||ny>n||mp[nx][ny]) continue;
						int nid=getid(nx,ny);
						add_Edge(id,nid,1,1),add_Edge(nid,id,0,0);
					}
				}
			}
		}
		int tmp=0;
		while(bfs()) tmp+=dfs(S,INF);
		if(is_print) printf("maxflow=%d\n",tmp);
		FUP(i,1,ednum)
		{
			int u=ed[i].frm,v=ed[i].to;
			if(u==S||u==T||v==S||v==T||ed[i].w||!ed[i].is) continue;
			pp[u]=v,pp[v]=u;
			in[u]=true,in[v]=true;
		}
		if(is_print){FUP(i,1,T) printf("i=%d in=%d pp=%d\n",i,in[i],pp[i]);}
		FUP(i,2,T-1) if(!in[i]) mydfs(i);
		int ans=0;
		FUP(i,2,T-1) if(!vis[i]) ans++;
		printf("%d\n",ans);
	}
}
namespace solve2{
	int getid(int x1,int y1,int x2,int y2)
	{
		return x1*n*n*n+y1*n*n+x2*n+y2;
	}
	void solve()
	{
		S=1,T=n*(n+1)*(n+1)*(n+1)+1;
		FUP(x1,1,n)
		{
			FUP(y1,1,n)
			{
				if(mp[x1][y1]) continue;
				FUP(x2,1,n)
				{
					FUP(y2,1,n)
					{
						if(mp[x2][y2]||(x1==x2&&y1==y2)) continue;
						int id=getid(x1,y1,x2,y2);
						if((x1+y1+x2+y2)&1) add_Edge(S,id,1,0),add_Edge(id,S,0,0);
						else add_Edge(id,T,1,0),add_Edge(T,id,0,0);
						if((x1+y1+x2+y2)&1)
						{
							FUP(i,0,7)
							{
								int nx=x1+dx[i],ny=y1+dy[i];
								if(nx<1||nx>n||ny<1||ny>n||mp[nx][ny]||(nx==x2&&ny==y2)) continue;
								int nid=getid(nx,ny,x2,y2);
								add_Edge(id,nid,1,1),add_Edge(nid,id,0,0);
							}
							FUP(i,0,7)
							{
								int nx=x2+dx[i],ny=y2+dy[i];
								if(nx<1||nx>n||ny<1||ny>n||mp[nx][ny]||(nx==x1&&ny==y1)) continue;
								int nid=getid(x1,y1,nx,ny);
								add_Edge(id,nid,1,1),add_Edge(nid,id,0,0);
							}
						}
					}
				}
			}
		}
		int tmp=0;
		while(bfs()) tmp+=dfs(S,INF);
		if(is_print) printf("maxflow=%d\n",tmp);
		FUP(i,1,ednum)
		{
			int u=ed[i].frm,v=ed[i].to;
			if(u==S||u==T||v==S||v==T||ed[i].w||!ed[i].is) continue;
			pp[u]=v,pp[v]=u;
			in[u]=true,in[v]=true;
		}
		if(is_print){FUP(i,1,T) printf("i=%d in=%d pp=%d\n",i,in[i],pp[i]);}
		FUP(i,2,T-1) if(!in[i]) mydfs(i);
		int ans=0;
		FUP(i,2,T-1) if(!vis[i]) ans++;
		printf("%d\n",ans/2);
	}
}
int main(){
	n=read(),m=read(),K=read();
	FUP(i,1,K)
	{
		int x=read(),y=read();
		mp[x][y]=1;
	}
	if(m==1) solve1::solve();
	else solve2::solve();
	return 0;
}

P1971

如果我们继续按照局面建边,那么局面种类太多了。所以考虑这么一个性质:一个点不会被重复经过。因为如果可以到达这个点,那么这个点的颜色就应该根据其横纵坐标之和定下来,然而经过一次之后其颜色就会发生改变,所以只会经过一次,那么我们就只需要建 \(n\times m\) 个点,表示空格在每个位置的情况而不关心其他格子的颜色状况。而建边的话可以按照初始图来建,而不用担心初始图可以由这条边到这个点,而经过一番操作之后不能到这个点,因为一个点只会被经过一次,匹配这个概念本身就已经囊括他了。

posted @ 2021-11-03 07:47  Pbri  阅读(655)  评论(0编辑  收藏  举报