【洛谷6665】[清华集训2016] Alice 和 Bob 又在玩游戏(博弈论+Trie树)
- 给定一张\(n\)个点的有根树森林。
- \(Alice\)和\(Bob\)轮流操作,一个人每次可以删去一个点到根的所有节点,先不能操作者输。
- 判断谁有必胜策略。
- 数据组数\(\le10,n\le10^5\)
博弈论:\(SG\)函数
考虑设\(SG(x)\)表示\(x\)的子树对应的\(SG\)函数。
首先,显然对于若干棵不同的树之间的决策是互不关联的,因此它们的总\(SG\)值就是各个\(SG\)值的异或和。
因此我们的核心问题就在于如何在已求出\(x\)子树内所有\(SG(y)\)的前提下,推出\(SG(x)\)。
对于这种问题我们只能借助定义来求解,即想办法求出所有后继情况的\(SG\)值的\(mex\)。
一个最简单的后继情况就是删去\(x\),那么就相当于把子节点拆成了若干棵森林,那么这种情况的\(SG\)值就是所有子节点\(SG\)值的异或和(不妨设它为\(t\),这个值之后还会用到)。
否则,我们可能会删去某个子节点\(y\)子树的某一个点,假设删去这个点后\(y\)子树的\(SG\)值变成了\(sg\)。
由于不管删去\(x\)子树内的哪个点,都将导致\(x\)被删去,因此除\(y\)以外的其余子节点也相当于变成了若干棵独立的树。
也就是说,这种情况下的\(SG\)值应该是\(sg\oplus(t\oplus SG(y))\),也就是\((sg\oplus SG(y))\oplus t\)。
考虑对于固定的\(x\),\(t\)是可以直接求出的,因此我们关键是要维护出所有可能的\(sg\oplus SG(y)\),而这其实是一个可以由\(y\)决定的量,不受其余子树的干涉,只需要把不同子树的可能值合并起来即可。
所以我们只需要知道\(sg\)这种决策由\(y\)到\(x\)发生的转变,根据前面的结论\(sg\)将会变化\(SG(y)\oplus t\),而为了保持形式不变需要给它异或上\(SG(x)\),总共给它异或上了\(t\oplus x\),而这恰是一个只与\(SG(x)\)有关的量。
全局异或可以用\(Trie\)树维护,合并子树信息可以用\(Trie\)树合并,查询\(mex\)可以\(Trie\)树上二分,于是这道题就解决了。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
using namespace std;
int n,m,ee,lnk[N+5];struct edge {int to,nxt;}e[2*N+5];
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
char oc,FI[FS],*FA=FI,*FB=FI;
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
}using namespace FastIO;
class Trie
{
private:
#define PU(x) (O[x].Sz=O[O[x].S[0]].Sz+O[O[x].S[1]].Sz)
#define PD(x,d) (O[x].G&&(T(O[x].S[0],O[x].G,d-1),T(O[x].S[1],O[x].G,d-1),O[x].G=0))
#define T(x,v,d) ((v)>>(d)&1&&(swap(O[x].S[0],O[x].S[1]),0),O[x].G^=v)
int Nt;struct node {int Sz,G,S[2];}O[N*100];
public:
I void Clear() {W(Nt) O[Nt].Sz=O[Nt].G=O[Nt].S[0]=O[Nt].S[1]=0,--Nt;}//清空
I void U(CI rt,CI v,CI d=15) {T(rt,v,d);}//全局标记
I void A(int& rt,CI v,CI d=20)//插入
{
!rt&&(rt=++Nt),~d?(PD(rt,d),A(O[rt].S[v>>d&1],v,d-1),PU(rt)):O[rt].Sz=1;//Sz维护不同元素个数
}
I int Mex(CI rt,CI d=20)//Trie树上二分出mex
{
if(!rt||!~d) return 0;PD(rt,d);
return O[O[rt].S[0]].Sz==(1<<d)?(1<<d|Mex(O[rt].S[1],d-1)):Mex(O[rt].S[0],d-1);//只有左子树满时才能向右走
}
I void Merge(int& x,CI y,CI d=20)//Trie树合并
{
if(!x||!y) return (void)(x|=y);~d&&(PD(x,d),PD(y,d),
Merge(O[x].S[0],O[y].S[0],d-1),Merge(O[x].S[1],O[y].S[1],d-1),0);//递归合并
}
}T;
int SG[N+5],Rt[N+5];I void dfs(CI x,CI lst=0)//dfs遍历
{
RI i,t=0;for(i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&(dfs(e[i].to,x),T.Merge(Rt[x],Rt[e[i].to]),t^=SG[e[i].to]);//合并Trie树,统计异或和
T.U(Rt[x],t),T.A(Rt[x],t),T.U(Rt[x],SG[x]=T.Mex(Rt[x]));//先给全局异或t修改完,然后插入删去x的后继状态t,再求出当前点SG并异或给全局
}
int main()
{
RI Tt,i,x,y,t=0;read(Tt);W(Tt--)
{
for(read(n,m),T.Clear(),ee=0,i=1;i<=n;++i) lnk[i]=SG[i]=Rt[i]=0;//清空
for(i=1;i<=m;++i) read(x,y),add(x,y),add(y,x);
for(t=0,i=1;i<=n;++i) !Rt[i]&&(dfs(i),t^=SG[i]);puts(t?"Alice":"Bob");//不同树的SG直接异或
}return 0;
}