两个需要求 sg 函数的树上博弈问题
这是两道切树的博弈题,方向正好相反,第一道是切子树,第二道是切向上到根的链。
[AGC017D] Game on Tree
题意:给你一棵1为根的树,Alice 和 Bob 轮流操作,每次切掉一个子树(不能以1为根),切到只剩根1一个点结束,谁会获胜?
这题洛谷上有 remote judge 渠道:AGC017D
Solution:
我们计算每棵树的 sg 值,注意到不能把自己整个切掉,顶多切连向儿子的边。可以视作每个儿子是一个独立的子问题,而多个子问题并行博弈,就是把所有子问题的 sg 值异或起来。
这个子问题 sg 值是什么呢,并不是儿子那棵子树的 sg 值,因为现在我可以把儿子的这个子树整个切掉了,但我们每个子树计算的 sg 值是不能把自己全切掉的。
聪明的你会发现,这个子问题相比儿子的 sg 值,其实就是多了一个操作空间:把儿子整个切掉。那这就相当于我往儿子这棵子树根上再挂一个父亲,然后这个新树的 sg 值就是子问题的 sg 值了。
“注意到” 一个结论,就是任意一棵树,我往根上再加一个父亲结点,形成的新树 sg 值等于原来的 sg 值 +1。
那这就好做了,子问题 sg 就是子树 sg+1。那么下面这一个式子就能把所有子树的 sg 值算出来了:($\bigoplus $ 是异或和)
这个结论是怎么 “注意到” 的呢?
我们的注意力肯定不是凭空产生的,我们从最简单的情况开始推:
只有一个点,sg 值为 0;两个点,我可以切掉儿子子树,取 mex 得 sg 为 1;
用数学归纳的思想,假设在树点数较少时,加一个父亲结点会使 sg 值 +1 这个结论成立。那么对于任意一棵树,我再加一个父亲结点形成新树,新树有两种操作,一是把儿子整个切掉,剩下一个点 sg=0;二是切儿子内的子树,就是模仿儿子 sg 值的计算过程,切完的剩余部分 sg 值本应该分布于 \([0,sg[son]-1]\), 但是现在根部都多了一个父亲结点,根据我们的归纳,在点数少时这个剩余部分的新 sg 值应该是原 sg 值 +1,因此新值分布于 \([1,sg[son]]\)。两种操作取 mex ,新树的 sg 值为 sg[son]+1,所以归纳成功。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const int N=105050;
const int qwq=N*23;
const int inf=0x3f3f3f3f;
inline int read() {
int sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int n;
int sg[N];
vector <int> e[N];
void DFS(int u,int fa) {
for(int v : e[u]) {
if(v==fa) continue;
DFS(v,u);
sg[u] ^= (sg[v]+1);
}
}
int main() {
int x,y;
n = read();
for(int i=1;i<n;i++) {
x = read(); y = read();
e[x].push_back(y);
e[y].push_back(x);
}
DFS(1,1);
if(sg[1]) cout<<"Alice";
else cout<<"Bob";
return 0;
}
这题代码倒是很短很简单,但是式子的证明还是有点思考价值的。
[SPOJ11414] COT3 - Combat on a tree
题意:给你一棵黑白树,给出每个点初始颜色,Alice 和 Bob 轮流操作,每次选择一个白点,把它所有祖先和它自己都涂黑,问:Alice 先手第一步涂哪些点可以获胜?
这题洛谷上也有 remote judge 渠道:SP11414
Solution:
发现和上一题一样,每个子树的 sg 值是固定的,只要这棵子树里的所有点还没有被操作。
一次操作是选择树内的一个点,将该点到根的链涂黑,也就相当于砍掉这条链,画图会发现这样做会切出来很多子树,变成一个森林,每棵切出来的子树问题独立,sg 值取异或,算出森林的 sg 值,将所有操作形成的森林求 mex 就是原来那棵树的 sg 值了。
最终要求先手操作哪些点可获胜,也就是以1为根的树里,哪些点切出来的森林 sg=0,说明后手操作此森林必败。
可惜我们发现求每一棵树的 sg 值,都需要遍历这棵树的所有操作,然后暴力算 mex,这两步每一个都是 n^2 的复杂度。
但是求 sg 值竟然是可以优化。
在我们求完某个子树的 sg 值后,那些操作森林的 sg 值先不要清空,而是可以批量继承给父亲。我们发现计算它父亲的 sg 值时,选取子树内同样的点来操作,切出来的森林与儿子切出来的森林相比只是多出来了它所有兄弟子树。那么儿子内所有操作的森林 sg 值,异或上它所有兄弟的 sg 值,就是父亲点所有操作的 sg 值了。
然后对于父亲结点,它要合并所有来自儿子子树内的操作,以及它本身(如果是它自己就是白点的话,操作后森林为所有儿子子树)。因此我们有一个森林 sg 值合并的需求。
当我们把父亲结点所有操作合并起来,也就是得到了所有操作的森林 sg 值,求出它们的 mex 即为父亲这棵树的 sg 值。
整体异或,合并,求mex值,我们发现可以用一个01trie全部搞定。
不知道是不是经典问题,反正完全能够应用于这道树形博弈题上,给人的感觉还是很妙的。至少我在写完 n^2 的暴力之后没想到求 sg 值的过程还能数据结构优化。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const int N=105050;
const int qwq=N*23;
const int inf=0x3f3f3f3f;
inline int read() {
int sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int n;
int a[N];
int sg[N];
vector <int> e[N];
int sons[N];
int ans[N],cnt;
int tot,ch[qwq][2],rt[N];
int tag[qwq];
int siz[qwq];
inline void pushdown(int now,int k) {
if((tag[now]>>(k-1))&1) swap(ch[now][0],ch[now][1]);
if(ch[now][0]) tag[ch[now][0]] ^= tag[now];
if(ch[now][1]) tag[ch[now][1]] ^= tag[now];
tag[now] = 0;
}
int merge(int r1,int r2,int k) {
if(!r1 || !r2) return r1 + r2;
if(k==0) { siz[r1] |= siz[r2]; return r1; }
pushdown(r1,k);
pushdown(r2,k);
ch[r1][0] = merge(ch[r1][0],ch[r2][0],k-1);
ch[r1][1] = merge(ch[r1][1],ch[r2][1],k-1);
siz[r1] = siz[ch[r1][0]] + siz[ch[r1][1]];
return r1;
}
int ask(int now) {
int res = 0;
for(int k=20;k>=0;k--) {
pushdown(now,k);
if(siz[ch[now][0]]!=(1<<k)) now = ch[now][0];
else res += (1<<k), now = ch[now][1];
}
return res;
}
void DFS(int u,int fa) {
for(int v : e[u]) {
if(v==fa) continue;
DFS(v,u);
sons[u] ^= sg[v];
}
if(!a[u]) {
int now = rt[u];
siz[now]++;
for(int k=20;k>=0;k--) {
pushdown(now,k);
int c = (sons[u]>>k)&1;
if(!ch[now][c]) ch[now][c] = ++tot;
now = ch[now][c];
siz[now]++;
}
}
for(int v : e[u]) {
if(v==fa) continue;
tag[rt[v]] ^= sons[u]^sg[v];
rt[u] = merge(rt[u],rt[v],21);
}
sg[u] = ask(rt[u]);
// cout<<"sg["<<u<<"] = "<<sg[u]<<endl;
}
void calc(int u,int fa,int val) {
if(!a[u] && !val) ans[++cnt] = u;
for(int v : e[u]) {
if(v==fa) continue;
calc(v,u,val^sg[v]^sons[v]);
}
}
int main() {
int x,y;
n = read(); tot = n;
for(int i=1;i<=n;i++) a[i] = read(), rt[i] = i;
for(int i=1;i<n;i++) {
x = read(); y = read();
e[x].push_back(y);
e[y].push_back(x);
}
DFS(1,1);
calc(1,1,sons[1]);
if(!cnt) {
cout<<"-1";
return 0;
}
sort(ans+1,ans+cnt+1);
for(int i=1;i<=cnt;i++) cout<<ans[i]<<"\n";
return 0;
}