CF1709E XOR Tree 题解 (启发式合并例题)
题面
给定一个\(n\)个节点的树,每个节点\(i\)上有一个数 \(a_i\)。
我们进行无限次操作:
- 修改任意一个节点上的数\(a_i\)为任意一个其他数
我们希望我们在进行诺干次操作后,对于任意一个点对\(u,v(u \neq v)\) , \(u\)到\(v\)的路径上经过的所有点(含\(u,v\))的异或和不为\(0\)
求操作的最小次数。
思考
我们先考虑,如何修改点的权值,使得一条路径上的异或和不为\(0\) 。
注意到,我们修改的值是不受限制的。同时,异或是按位运算的。这意味着,我们总可以构造一个数,使得一条路径上的异或和不为\(0\)。例如,目前树上所有的数字都不超过\(2^k\),那么我们就可以把这个数设为\(2^k\),那么从二进制视角,二进制的第\(k\)位为\(1\),且没有任何已经存在的值或多个值的异或和可以与其异或为\(0\) 。
综上,我们总可以破坏异或链使得不为\(0\)。而且,这是没有条件的。我们将这个记为结论\(A\)
我们考虑,怎么安排改变的点的数量。
注意到,异或是可以构造前缀和的。故尝试构造前缀和\(pre\),对于每个节点\(i\),\(pre_i\)代表从根节点到节点\(i\)的路径的异或和。
对于一条\(u\)到\(v\)路径,记\(lca=lca(u,v)\),则路径的异或和为\(pre_u \oplus pre_v \oplus a_{lca}\)
通过画图,我们注意到,如果将修改非\(lca\)的节点操作进行调整。总存在一个\(lca\)节点,使得将原先的非\(lca\)节点调整为这个节点后,答案不会更劣。
我们考虑只修改\(lca\)节点。
我们尝试从\(lca\)入手,枚举举\(lca\)节点,我们希望知道,有多少个\(u,v\)满足\(pre_u \oplus pre_v \oplus a_{lca}=0\)。同时要求\(u,v\)必须分别处于\(lca\)的不同子树。
考虑到,是对子树出现过的值的操作,考虑树上启发式合并
我们可以通过树上启发式合并。来进行判断,是否存在异或和为0的路径经过某点且该点作为路径的\(lca\)
如果不存在,那就不存在,这个点不需要修改值。
如果存在,则这个值需要修改。同时由于上面的结论结论\(A\),我们可以将这个节点修改为\(2\)的诺干次方。使得树上没有值和这个节点修改后的值异或为\(0\)。 因此,以这个节点为根的子树内的点永远不会与这个子树外部的节点构成非法路径。因此这个子树在以后的启发式合并就不需要考虑了。
现在我们可以很方便的知道一个节点作为\(lca\)时,会不会有路径的异或和为\(0\)
实现
由于每次判断以来子树的判断和合并。
所以递归处理。
使用\(set\)来维护子树内出现的值
同时\(pre_u \oplus pre_v \oplus a_{lca}=0\)明显是不好维护的。
变形为\(pre_u \oplus a_{lca}= pre_v\),这一我们在已知\(a_{lca},pre_u\),通过set查询是否存在\(pre_v\)来判断是否有非法路径了。
#include <bits/stdc++.h>
using std::cin;
using std::pair;
using std::max;
using std::min;
using std::swap;
using std::set;
typedef long long ll;
const ll inf=1e18;
const ll maxn=2e5+5;
struct Edge{
ll v,next;
};
ll heavy[maxn];
ll size[maxn];
Edge e[maxn<<1];
ll head[maxn];
set<ll> s[maxn];
ll pre[maxn];
ll w[maxn];
ll fa[maxn];
ll n,etot,ans;
void dfs(ll x,ll father) {
fa[x]=father;
size[x]=1;
pre[x]=pre[father]^w[x];
for(ll i=head[x];i;i=e[i].next) {
if(e[i].v==father) {
continue;
}
dfs(e[i].v,x);
size[x]+=size[e[i].v];
if(size[e[i].v] > size[heavy[x]]) {
heavy[x]=e[i].v;
}
}
}
void add_edge(ll u,ll v) {
++etot;
e[etot]={v,head[u]};
head[u]=etot;
return;
}
void solve(ll u) {
bool flag=false;
if(size[u]==1) {
s[u].insert(pre[u]);
return;
}
solve(heavy[u]);
//s[u]=s[heavy[u]];
//检查有没有可以直接异或为0的
std::swap(s[heavy[u]],s[u]); //这里主要!!要使用swap,因为这是使用指针之间指向原来的数据,而不是一个个搬运数据。这样快
if(s[u].count(pre[u]^w[u])) {
flag=true;
}
s[u].insert(pre[u]);
for(ll i=head[u];i;i=e[i].next) {
if(e[i].v==fa[u] || e[i].v==heavy[u]) {
continue;
}
//开始合并
solve(e[i].v);
for(ll j:s[e[i].v]) {
if(s[u].count(j^w[u])) {
flag=true;
}
}
//这个插入和匹配得分开搞,不然子树内配上了
for(ll j:s[e[i].v]) {
s[u].insert(j);
}
}
s[u].insert(pre[u]);
if(flag) {
s[u].clear();
++ans;
}
return;
}
int main() {
//freopen("input","r",stdin);
cin>>n;
for(ll i=1;i<=n;++i) {
cin>>w[i];
}
for(ll i=1;i<n;++i) {
ll u,v;
cin>>u>>v;
add_edge(u,v);
add_edge(v,u);
}
dfs(1,0);
solve(1);
printf("%lld\n",ans);
return 0;
}
难点思考
个人认为:
难点在与分析出可以通过只修改作为\(lcd\)的节点而获得最优解。然后在这个结论上贪心。 以后多画画图吧
启发式合并的细节注意下,递归的时候检查一下类似于break,continue的跳过的操作会不会跳了一些不该跳的。

浙公网安备 33010602011771号