CFP1709E XOR Tree 学习笔记
CFP1709E XOR Tree 学习笔记
前言
这不得不说是一道结合了树上启发式合并、位运算、贪心的好题。
以及,谁说树上启发式合并一定要有重剖要素的?“把小的并到大的”都能叫启发式合并!这题就是一个例证。
题意简述
给定一棵 \(n\) 个结点的无根树,每个点 \(u\) 有一个点权 \(a_u\)。定义一条路径 \(P(u,v)\) 的权值为经过的所有点的点权的异或和。
可以任意次修改某个点的点权为任意正整数。问最少修改几次,才能使这棵树不存在任何权值为 \(0\) 的简单路径。
\(n\le 2\times 10^5,a_u\le 2^{30}\)。
做法解析
不妨令 \(1\) 为根。首先一个常识是 \(u\to v\) 路径上点权的异或和等于 \(d_u\oplus d_v\oplus a_{\text{lca}(u,v)}\),其中 \(d_u\) 等于 \(u\) 到 \(1\) 的路径上所有点的点权异或和。证明易。
另外你要意识到“任意正整数”中的“任意”意味着什么——意味着你的数甚至可以超过 \(a_i\) 初值范围的限制——意味着当你只要遵循着修改 \(u\) 点权时将 \(a_u\) 赋值为 \(2^{u+G}\)(其中 \(G\) 为极大值),就可以断绝任意经过 \(u\) 的路径点权异或和等于 \(0\) 的可能性!
原理很显然。因为这相当于修改后的点权二进制上有一位,只有它自己的点权才可能是 \(1\),所以这一位就不可能被另一个 \(1\) 异或掉。
我们知道,如果有 \(x,y\) 满足其简单路径上所有点权异或和为 \(0\),我们最晚要在其 LCA 处修改某个点点权。而当我们修改一个点 \(u\) 的点权,相当于我们不再需要考虑 \(u\) 子树内的所有结点。所以贪心的来看,当 \(x,y\) 的路径出现问题,在 \(\text{lca}(x,y)\) 处改点权是最优的(可以让我们免除考虑最多的结点)。所以我们对每个结点维护一个集合 \(s_u\),表示其子树内所有的 \(d_i\)。我们在 DFS 时一个个把 \(s_v\) 合并进 \(s_u\),若发现 \(s_u\) 存在 \(d_i\) 的同时 \(s_v\) 存在 \(d_j=d_i\oplus a_u\),说明我们需要改掉 \(a_u\) 的点权,之后我们就不用考虑 \(u\) 的子树了,也就是把 \(s_u\) 清空。
显然我们用启发式合并来合并 \(s_u,s_v\) 复杂度是 \(O(n\log n)\) 的,再乘上 set 操作自身 \(\log n\) 的复杂度。总时间复杂度 \(O(n\log^2 n)\)。
代码实现
放心,C++11 及以上,swap 两个 set 是 \(O(1)\) 的。
#include <bits/stdc++.h>
using namespace std;
using namespace obasic;
const int MaxN=2e5+5;
int N,X,Y,A[MaxN];
vector<int> Tr[MaxN];
void addudge(int u,int v){
Tr[u].push_back(v);
Tr[v].push_back(u);
}
int xsum[MaxN],ans;
set<int> sut[MaxN];
void dfs(int u,int f){
bool flag=0;
xsum[u]=xsum[f]^A[u],sut[u].insert(xsum[u]);
for(auto v : Tr[u]){
if(v==f)continue;dfs(v,u);
if(sut[v].size()>sut[u].size())swap(sut[v],sut[u]);
for(int x : sut[v])if(sut[u].find(x^A[u])!=sut[u].end())flag=1;
for(int x : sut[v])sut[u].insert(x);
sut[v].clear();
}
if(flag)ans++,sut[u].clear();
}
int main(){
readi(N);
for(int i=1;i<=N;i++)readi(A[i]);
for(int i=1;i<N;i++)readis(X,Y),addudge(X,Y);
dfs(1,0);writi(ans);
return 0;
}
浙公网安备 33010602011771号