图论学习:二分图博弈
二分图博弈:
问题:一个二分图,先手选择其中一个点出发。之后双方轮流选择移动到一个不曾经过的点且与当前点有边相连的点,若轮到某个人时候,无法移动者判为负。
结论:
先求出二分图的最大匹配,之后如果先手的出发点不在某个最大匹配中,则先手必胜。
等价于:
1.假如先手选择的这个点在所有的最大匹配中都被选中了,则先手选这个点必败。
2.先手必胜当且仅当不是完美匹配,而且可以放的点是那些可以不匹配的点
证明:
先手选择一个点后,从起点出发,后手只能选择一个在最大匹配中的点,否则后手走的这条边可以加入到最大匹配中,与最大匹配定义不符合。
之后每次先手都选择与前一个点匹配的点,后手一定只能选择一个在最大匹配中的点,否则双方走的这条边是增广路。
最后肯定先手走完,停留在一个与起点在同一组的点,后手无法再操作,先手获胜。
例题:
1.2020 China Collegiate Programming Contest Changchun Onsite,H题
题意:
有一个密码锁,两个人一起玩游戏,给出初始的密码,规定:
1.每一次都可以转动一个位置的数字一个单位
2.不可以转动到已经出现过的数字
3.不可以转动到初始不能选择的数字
无法转动的人视为失败,问谁能获胜
题解:
判断起始状态是否是最大匹配的必须点。先不加起始状态跑一遍最大匹配,加入起始状态再跑一遍最大匹配,
若第二次出现流量(有新的匹配边),则说明起始状态是最大匹配的必须点,则Alice获胜,否则Bob获胜。
二分图博弈。每次转动一个位置的数字一个单位,也就是改变了位数和的奇偶性。
点击查看折叠代码块
#include <bits/stdc++.h>
#include <string>
using namespace std;
typedef long long ll;
const int maxn = 2e5+10;
const int maxm = 1e6+10;
const int inf = 0x3f3f3f3f;
int head[maxn],cnt=0;
struct edge{
int v,next;
int c;
}e[maxm*3];
int m,n,passwd;
void add(int u,int v){
e[cnt].v=v;e[cnt].c=1;e[cnt].next=head[u];head[u]=cnt++;
e[cnt].v=u;e[cnt].c=0;e[cnt].next=head[v];head[v]=cnt++;
}
int T;
int st,ed;//源点和汇点,源点连奇数,汇点连偶数
int fac[] = {1,10,100,1000,10000,100000};
int kind[maxn];//0表示数位和为偶数,1表示数位和为奇数
bool vis[maxn];//是否之前被选择过
void init(){
for (int i=0;i<=fac[5];i++){
int x = i,sum = 0;
while(x){
sum+=x%10;
x/=10;
}
kind[i] = (sum&1)?1:0;
}
}
int cur[maxn],dis[maxn];
bool bfs(int s,int t){
memset(dis,-1,sizeof(dis));
dis[s] = 0;
cur[s] = head[s];
queue<int> q;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
for (int i=head[u];~i;i=e[i].next){
int v=e[i].v,c=e[i].c;
if(c && dis[v]==-1){
dis[v] = dis[u]+1;
cur[v] = head[v];
q.push(v);
if(v == t) return 1;
}
}
}
return dis[t] != -1;
}
int dfs(int u,int t,int flow){
if(u == t) return flow;
int delta = flow;
for (int i=cur[u];~i;i=e[i].next){
int v=e[i].v,c=e[i].c;
if(c && dis[v] == dis[u]+1){
int d = dfs(v,t,min(delta,c));
if(!d) dis[v] = 0;
e[i].c-=d;
e[i^1].c+=d;
delta -= d;
if(!delta) break;
}
}
return flow - delta;
}
int dinic(int s,int t)
{
int ans = 0;
while(bfs(s,t)){
ans += dfs(s,t,inf);
}
return ans;
}
int main(){
init();
cin>>T;
while(T--){
scanf("%d%d%d",&m,&n,&passwd);
st=fac[5]+1,ed=st+1;
cnt = 0;
for (int i=0;i<=fac[m];i++){
head[i] = -1;
vis[i] = 0;
}
head[st] = -1,head[ed] = -1;
for (int i=1;i<=n;i++){
int x;scanf("%d",&x);
vis[x] = 1;
}
for (int i=0;i<fac[m];i++){
if(vis[i]) continue;
if(kind[i] == 1){
if(i!=passwd) add(st,i);
string s = to_string(i);
// cout<<"s = "<<s<<endl;
while(s.size()<m) s="0"+s;
// cout<<"s = "<<s<<endl;
for (int j=0;j<m;j++){
string ss = s;
ss[j] = (ss[j]-'0'+1)%10+'0';
add(i,stoi(ss));
ss[j] = (ss[j]-'0'-2+10)%10+'0';
add(i,stoi(ss));
}
}
else {
if(i!=passwd) add(i,ed);
}
}
// cout<<"cnt = "<<cnt<<endl;
int ans1 = dinic(st,ed);
// cout<<"ans1 = "<<ans1<<endl;
if(kind[passwd]==1){
add(st,passwd);
}
else add(passwd,ed);
int ans = dinic(st,ed);
if(ans) puts("Alice");
else puts("Bob");
}
return 0;
}
2.洛谷P4055 [JSOI2009]游戏
题解:
二分图博弈。考虑黑白染色,发现每次走动只能从白色格子到黑色格子或者是从黑色格子到白色格子。
如果这个二分图是一个完美匹配,那么不管我从迷宫中哪个点开始走,对手都可以走匹配边到达另外一个点。先手必败。
于是选择那些删除之后仍能保持原图最大匹配数不变的点是先手必胜点。
我们先跑网络流最大流跑出最大匹配,然后考虑在残量网络中:
从超级源点S出发,每次找流量存在的边,如果这条边连接的另一个点是和S相连的点,则这个点是先手必胜点。
从超级汇点T出发往回走,每次找流量不存在的边,如果这条边连接的另一个店是和T相连的点,则这个点是先手必胜点。
具体看代码:(注意染色的正确性,我就是这里WA了好久)
点击查看代码块
/*
二分图博弈
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6+10;
const int inf = 0x3f3f3f3f;
int head[maxn],cnt=0;
int dir[4][2]={-1,0,0,1,1,0,0,-1};
struct edge{
int u,v,next;
int c;
}e[maxn<<1];
void add(int u,int v){
e[cnt].u=u;e[cnt].v=v;e[cnt].c=1;e[cnt].next=head[u];head[u]=cnt++;
e[cnt].u=v;e[cnt].v=u;e[cnt].c=0;e[cnt].next=head[v];head[v]=cnt++;
}
int n,m;
char mp[110][110];
int st,ed;
int dis[maxn],cur[maxn];
int getpos(int x,int y){return (x-1)*m+y;}
int id[110][110],color[maxn];
bool bfs(int s,int t){
memset(dis,-1,sizeof(dis));
dis[s] = 0;
cur[s] = head[s];
queue<int> q;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
for (int i=head[u];~i;i=e[i].next){
int v=e[i].v,c=e[i].c;
if(c && dis[v]==-1){
dis[v]=dis[u]+1;
cur[v]=head[v];
q.push(v);
if( v == t) return 1;
}
}
}
return dis[t]!=-1;
}
int dfs(int u,int t,int flow){
if(u == t) return flow;
int delta = flow;
for (int i=cur[u];~i;i=e[i].next){
int v=e[i].v,c=e[i].c;
if(c && dis[v]==dis[u]+1){
int d = dfs(v,t,min(c,delta));
if(!d) dis[v] = 0;
e[i].c-=d;
e[i^1].c+=d;
delta-=d;
if(delta == 0) break;
}
}
return flow-delta;
}
int dinic(int s,int t){
int ans = 0;
while(bfs(s,t)){
ans+=dfs(s,t,inf);
}
return ans;
}
bool flag[maxn];
int ans[maxn],tot = 0;
void dfs1(int u,int op){
flag[u] = 1;
if(color[u] == op && u!=st && u!=ed) {
// cout<<"u = "<<u<<"x = "<<x<<" y = "<<y<<endl;
ans[++tot] = u;
}
for (int i=head[u];~i;i=e[i].next){
int v=e[i].v,c=e[i].c;
if(c == op && !flag[v]) dfs1(v,op);
}
}
void solve(){
dfs1(st,1);
memset(flag,0,sizeof(flag));
dfs1(ed,0);
}
int main(){
cin>>n>>m;
memset(head,-1,sizeof(head));
memset(color,-1,sizeof(color));
cnt=0;
for (int i=1;i<=n;i++){
scanf("%s",mp[i]+1);
}
st = 0,ed = m*n+1;
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
if(mp[i][j]=='#') continue;
int x=getpos(i,j);
if(((i^j)&1)){
color[x] = 1;
add(st,x);
for (int k=0;k<4;k++){
int ti=dir[k][0]+i,tj=dir[k][1]+j;
if(ti>=1 && ti<=n && tj>=1 && tj<=m && mp[ti][tj]!='#'){
int tx=getpos(ti,tj);
add(x,tx);
}
}
}
else{
add(x,ed);color[x]=0;
}
}
}
// for (int i=0;i<cnt;i++){
// printf("i = %d u = %d v = %d c = %d\n",i,e[i].u,e[i].v,e[i].c);
// }
// for (int i=1;i<=n*m;i++){
// printf("color = %d\n",color[i]);
// }
dinic(st,ed);
solve();
if(tot == 0) {
puts("LOSE");
}
else{
puts("WIN");
// sort(Ans.begin(),Ans.end());
sort(ans+1,ans+1+tot);
for (int i=1;i<=tot;i++){
int u=ans[i];
int x=(u-1)/m+1,y=(u-1)%m+1;
printf("%d %d\n",x,y);
}
}
return 0;
}
/*
4 4
####
####
#..#
##..
3 3
###
###
###
3 3
...
...
...
*/