LOJ #536. 「LibreOJ Round #6」花札

题目链接

LOJ #536. 「LibreOJ Round #6」花札

题目大意

Alice 和 Shinobu 手中分别有 \(n_1,n_2\) 张牌,每张牌有颜色和点数两个属性。

游戏规则是:Alice 先任意出一张牌,然后双方交换手牌,接下来由 Alice 开始双方交替出牌,每次当前方出的牌要和上一次对方出的牌至少有一个属性相同,Alice 开头出的两张牌也要满足至少有一个属性相同。最后无牌可出的人输。

注意交换手牌后,双方知道所有牌的情况。请对于 Alice 第一次出牌的所有情况,求出谁有必胜策略。

\(n_1,n_2\leq 4\times 10^4\)\(m,c\leq 10^4\)

\(m,c\) 分别表示点数和颜色的种类数。

思路

是二分图博弈,纯粹称之为思维题似乎不太 Trivial。

考虑将 Alice 手中的牌作为左部节点,Shinobu 手中的牌作为右部节点,两张牌有至少一个属性相同时在之间连边。那么对于一个 Alice 第一次出牌的情况,相当于固定了一个左部起点,然后 Alice 和 Shinobu 交替在此二分图上游走,且不能重复访问一个节点。


二分图博弈有定理:先手必胜,当且仅当起点在所有最大匹配上

若存在最大匹配使得起点不在其中,那么先手第一步走到的右部点必然不和起点匹配,因为起点是非匹配点,所以其邻居应全为匹配点,于是后手走到该右部点的匹配点上。然后不断重复此过程,每次后手都走到上轮点的匹配点上。

若后手无法操作,即先手走到的点是非匹配点,那么游戏中的行走路径两端都是非匹配点,类似匈牙利算法将路径上的匹配全部翻转,则会使得原图的匹配数加 \(1\),与最大匹配矛盾,所以先手必输。

若起点在所有最大匹配中,则相当于先手先走了一步匹配,与前面的情况是对称的,从而后手必输。


于是现在要找到求最大匹配的网络流上的必经点.

先优化建图,将种点数和颜色建一个点,每张牌与其对应的点数的颜色连边。这样图就不是二分图了,但是边数从 \(O(n^2)\) 优化到了 \(O(n)\) 级别。然后跑网络流,对于容量全为 \(1\) 的网络流,\(Dinic\) 的时间复杂度是 \(O(\min(V^{2/3},E^{1/2})E)\) 的。


存在一个最大流满足 \(S\)\(i\) 没有流量,等价于在任意一个最大流中,残留网络上存在 \(S\)\(i\) 的路径。

充分性:若此 ”任意一个“ 最大流满足无流量,则直接成立。否则将原先 \(S\)\(i\) 的流路和残留网络上的 \(S\)\(i\) 的路径全部取反,便得到了一个 \(S\)\(i\) 无流量的最大流。

必要性:对于一个 \(S\)\(i\) 没有流量的最大流,将其和任意一个最大流作差。由于任意点流量平衡,所以差图由若干简单环组成,将包含 \(S\)\(i\) 的一系列环取出,先前的流残量网络上 \(S\)\(i\) 的路径占据了环的一部分,取环的剩下一部分便组合出了当前最大流中残量网络上一条 \(S\)\(i\) 的路径。


于是我们任意求出一个最大流,在残量网络上从 \(S\) 开始 \(bfs\),访问不到的左部点是 Alice 必胜,否则是 Shinobu 必胜。

Code

#include<iostream>
#include<queue>
#include<cstring>
#define mem(a,b) memset(a, b, sizeof(a))
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 40044
#define M 10011
#define Inf 0x3f3f3f3f
using namespace std;

const int V = 2*N + 2*M, E = 12*N;
int head[V], nxt[E], to[E];
int cap[E], now[V], dis[V];
int cnt, S, T;
queue<int> q;

void init(){ mem(head, -1), cnt = -1; }
void add_e(int a, int b, int c, bool id){
    nxt[++cnt] = head[a], head[a] = cnt, to[cnt] = b, cap[cnt] = c;
    if(id) add_e(b, a, 0, 0);
}

bool bfs(){
    mem(dis, 0), dis[S] = 1;
    q.push(S);
    while(!q.empty()){
        int cur = q.front(); q.pop();
        now[cur] = head[cur];
        for(int i = head[cur]; ~i; i = nxt[i]) if(cap[i])
            if(!dis[to[i]]) dis[to[i]] = dis[cur] + 1, q.push(to[i]);
    }
    return dis[T];
}

int dfs(int x, int flow){
    if(x == T) return flow;
    int flown = 0;
    for(int i = now[x]; ~i; i = nxt[i]){
        now[x] = i;
        if(!cap[i] || dis[to[i]] != dis[x] + 1) continue;
        int t = dfs(to[i], min(cap[i], flow - flown));
        cap[i] -= t, cap[i^1] += t;
        flown += t;
        if(flow == flown) break;
    }
    return flown;
}

int m, c, n1, n2;
int x1[N], y1[N], x2[N], y2[N];

int main(){
    ios::sync_with_stdio(false);
    cin>>m>>c;
    cin>>n1;
    rep(i,1,n1) cin>>x1[i]>>y1[i];
    cin>>n2;
    rep(i,1,n2) cin>>x2[i]>>y2[i];

    init();
    S = 0, T = n1+n2+m+c+1;
    rep(i,1,n1){
        add_e(S, i, 1, 1);
        add_e(i, n1+n2+x1[i], 1, 1), add_e(i, n1+n2+m+y1[i], 1, 1);
    }
    rep(i,1,n2){
        add_e(n1+i, T, 1, 1);
        add_e(n1+n2+x2[i], n1+i, 1, 1), add_e(n1+n2+m+y2[i], n1+i, 1, 1);
    }
    int flow = 0;
    while(bfs()) flow += dfs(S, Inf);

    rep(i,1,n1) cout<< !dis[i] <<endl;
    return 0;
}

另外

还有一道经典的二分图博弈题:LOJ #6033. 「雅礼集训 2017 Day2」棋盘游戏

找到了两篇关于博弈论整理的比较齐全的博客:

组合游戏与博弈论]【学习笔记】

博弈论总结(已更新二分图博弈)

posted @ 2022-03-21 17:34  Neal_lee  阅读(153)  评论(0编辑  收藏  举报