- 给定一张(n)个点的有根树森林。
- (Alice)和(Bob)轮流操作,一个人每次可以删去一个点到根的所有节点,先不能操作者输。
- 判断谁有必胜策略。
- 数据组数(le10,nle10^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)值应该是(sgoplus(toplus SG(y))),也就是((sgoplus SG(y))oplus t)。
考虑对于固定的(x),(t)是可以直接求出的,因此我们关键是要维护出所有可能的(sgoplus SG(y)),而这其实是一个可以由(y)决定的量,不受其余子树的干涉,只需要把不同子树的可能值合并起来即可。
所以我们只需要知道(sg)这种决策由(y)到(x)发生的转变,根据前面的结论(sg)将会变化(SG(y)oplus t),而为了保持形式不变需要给它异或上(SG(x)),总共给它异或上了(toplus x),而这恰是一个只与(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;
}