SP11414 COT3 - Combat on a tree
Alice和Bob在一棵\(n\)个节点的树上玩游戏。每个节点最初都是黑色或白色。
他们轮流执行以下操作:
从当前树中选择一个白色节点\(v\),将路径\((1,v)\)上的所有白色节点都变为黑色.最后操作的玩家获胜.爱丽丝先手。当他们都使用最佳策略时,求是否能够必胜,并求出第一步的方案.
竟然找到了省选模拟赛的题加强版(
对于这道题,我们考虑计算出每个点的这个子树的sg值,从而得到全局的答案。
那么我们可以很容易发现,对于一个点\(u\),如果这个子树的点都是黑点,那么\(sg(u)=0\),必败,否则子树里的白点可以作为\(u\)的后继状态,假设白点为\(v\),那么\(sg(u)=mex(sg'(v))\),\(mex(S)\)为集合\(S\)中未出现的最小自然数。
而之所以不是\(v\)的\(sg\)是因为当选了\(v\)这个点之后,会把当前游戏割裂成多个子游戏,也就是将\(v\)到\(u\)的路径上的点删掉,得到了若干个子树,所以\(sg'(v)=\oplus_isg(i)\),其中\(i\)为每个子树的根。
得到了sg值之后,如果\(sg(1)=0\),那么无解,否则对每个点还是做上面的过程,如果得到的值为\(0\),那么可以作为答案。
这样子的复杂度是\(O(n^2)\)的,如果写的丑可能会变成\(O(n^3)\)。
我们考虑用trie优化这个过程。
子游戏的异或可以看作以\(v\)为根的子树的集合全部异或上一个数,那么我们记录一个标记,每次下放标记就可以。
对于对一个集合取mex,我们从高位往低位贪心,如果左儿子是满二叉树,那么就往右儿子走,否则走左儿子,类似于一个线段树二分。判断是不是满二叉树可以维护标记来完成。
然后还需要支持集合合并和插入一个数,就不用多说了。
这样时间复杂度为\(O(nlogn)\)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
const int N = 1e5;
using namespace std;
int n,c[N + 5],sg[N + 5],edge[N * 2 + 5],nxt[N * 2 + 5],head[N + 5],edge_cnt,ans[N + 5],m,rt[N + 5];
void add_edge(int u,int v)
{
edge[++edge_cnt] = v;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
struct Trie
{
int ch[N * 40 + 5][2],cov[N * 40 + 5],node_cnt,tag[N * 40 + 5],dep[N * 40 + 5];
void pushup(int k)
{
cov[k] = cov[ch[k][0]] & cov[ch[k][1]];
}
void insert(int &k,int x,int d)
{
if (!k)
{
k = ++node_cnt;
dep[k] = d;
}
if (d < 0)
{
cov[k] = 1;
return;
}
insert(ch[k][x >> d & 1],x,d - 1);
pushup(k);
}
void modify(int k,int x)
{
if (dep[k] < 0)
return;
if (x >> dep[k] & 1)
swap(ch[k][0],ch[k][1]);
tag[k] ^= x;
}
void pushdown(int k)
{
if (tag[k])
{
modify(ch[k][0],tag[k]);
modify(ch[k][1],tag[k]);
tag[k] = 0;
}
}
int merge(int x,int y)
{
if (!x || !y)
return x + y;
if (dep[x] < 0)
{
cov[x] |= cov[y];
return x;
}
pushdown(x);
pushdown(y);
ch[x][0] = merge(ch[x][0],ch[y][0]);
ch[x][1] = merge(ch[x][1],ch[y][1]);
pushup(x);
return x;
}
int mex(int k)
{
if (dep[k] < 0)
return 0;
if (!k)
return 0;
pushdown(k);
if (cov[ch[k][0]])
return (1 << dep[k]) | mex(ch[k][1]);
return mex(ch[k][0]);
}
}tree;
void dfs(int u,int fa)
{
int xx = 0;
for (int i = head[u];i;i = nxt[i])
{
int v = edge[i];
if (v == fa)
continue;
dfs(v,u);
xx ^= sg[v];
}
for (int i = head[u];i;i = nxt[i])
{
int v = edge[i];
if (v == fa)
continue;
tree.modify(rt[v],xx ^ sg[v]);
rt[u] = tree.merge(rt[u],rt[v]);
}
if (!c[u])
tree.insert(rt[u],xx,20);
sg[u] = tree.mex(rt[u]);
}
void dfs2(int u,int fa,int x)
{
for (int i = head[u];i;i = nxt[i])
{
int v = edge[i];
if (v == fa)
continue;
x ^= sg[v];
}
if (!c[u] && !x)
ans[++m] = u;
for (int i = head[u];i;i = nxt[i])
{
int v = edge[i];
if (v == fa)
continue;
dfs2(v,u,x ^ sg[v]);
}
}
inline int read()
{
int X(0),w(0);char ch(0);
while (!isdigit(ch))w|=ch=='-',ch=getchar();
while (isdigit(ch))X=(X<<3)+(X<<1)+(ch^48),ch=getchar();
return w?-X:X;
}
int main()
{
//freopen("sp11414.in","r",stdin);
//freopen("a1.out","w",stdout);
n = read();
for (int i = 1;i <= n;i++)
c[i] = read();
int u,v;
for (int i = 1;i < n;i++)
{
u = read();
v = read();
add_edge(u,v);
add_edge(v,u);
}
dfs(1,0);
if (!sg[1])
cout<<-1<<endl;
else
{
dfs2(1,0,0);
sort(ans + 1,ans + m + 1);
for (int i = 1;i <= m;i++)
printf("%d ",ans[i]);
}
return 0;
}