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」棋盘游戏
找到了两篇关于博弈论整理的比较齐全的博客: