Problem Description
给出一棵带权节点的树,树根为 111 。
你能将任意节点的值转变为非负整数,是为一次操作。
请问,最少操作多少次能使所有叶子节点到树根的路径上节点权值异或和为 000 。
Input
第一行输入一个整数 n(2≤n≤105)n(2 \le n \le 10^5)n(2≤n≤105) ,表示节点个数。
第二行输入 nnn 个整数 a1,a2,a3,…,an(1≤ai≤109)a_1,a_2,a_3,…,a_n(1 \le a_i \le 10^9)a1,a2,a3,…,an(1≤ai≤109) ,表示每个节点的权值。
接下来 n−1n-1n−1 行数每行输入两个整数 ui,vi(1≤ui,vi≤n,ui≠vi)u_i,v_i(1 \le u_i,v_i \le n,u_i \not= v_i)ui,vi(1≤ui,vi≤n,ui=vi) ,表示 ui,viu_i,v_iui,vi 之间有一条边。
Output
输出最小操作次数。
Solution
对于以下所有图而言,节点上的数字为节点编号,右侧的数字为其对应的从根到该节点的异或和。
我们先观察上图以 333 为父节点的子树,如果要让这棵子树中的叶子节点的异或和变为 000 ,由于异或和 555 的个数大于 222 的个数,显然有个简单的贪心策略为将节点 888 的权值异或上 (2⊕5)(2 \oplus 5)(2⊕5) ,那么对于这棵子树而言,只需要令从 111 到 333 的这条路径上的异或和最终再异或上 555 即可。此时我们可以将节点 333 视为异或和为 555 的叶子节点,然后和节点 2,4,52,4,52,4,5 再做一遍同样的上述操作递归到根节点即可。
但是可能一棵子树里的异或和并不是一种值最大,如上图所示,可能出现多个相同异或和个数。此时,如果我们将 333 视作是异或和为 555 的叶子节点,再下一次操作时节点 333 要被异或上 (3⊕2)(3 \oplus 2)(3⊕2) ,而如果我们将 333 视作为异或和为 222 的叶子节点,那么就不需要再被异或。所以当出现多个异或和个数相同时,我们需要将其全部存下后再在上一层进行判断。
但是如果我们暴力存复杂度将会达至 O(n2)O(n^2)O(n2) ,此时我们便通过启发式合并去优化此题,即将小的集合放入大的集合中。
我们使用 mapmapmap 来维护集合,最终复杂度为 O(nlognlogn)O(nlognlogn)O(nlognlogn) 。
Code
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
constexpr int N = 1e5 + 10, M = 2e5 + 10;
int n, a[N], ans = 0;
int head[N], e[M], ne[M], idx = 0;
inline void addedge(int a, int b) {
e[idx] = b, ne[idx] = head[a], head[a] = idx++;
}
map<int, int>mp[N];
void dfs(int u, int fa) {
int maxn = 1, sum = 0;//maxn为异或和个数的最大值,sum为儿子节点个数
for (int i = head[u]; i != -1; i = ne[i]) {
int v = e[i];
if (v == fa)continue;
a[v] ^= a[u], sum++;
dfs(v, u);
if (mp[u].size() < mp[v].size())swap(mp[u], mp[v]);//选择较小的集合合并入较大的集合
for (auto [x, y] : mp[v]) {
mp[u][x] += y;
maxn = max(maxn, mp[u][x]);
}
}
if (!sum) {mp[u][a[u]] = 1; return;}//为叶子节点时
ans += sum - maxn;//存下需要修改的点权个数
if (maxn != 1) {//为了防止所有异或和的个数都为1而导致反复下面的循环而TLE,需要加上maxn!=1
for (auto it = mp[u].begin(); it != mp[u].end();) {
if (it->second == maxn)it->second = 1, it++;
else it = mp[u].erase(it);
}
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
memset(head, -1, sizeof head);
cin >> n;
for (int i = 1; i <= n; i++)cin >> a[i];
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
addedge(a, b), addedge(b, a);
}
dfs(1, 0);
if (!mp[1].count(0))ans++;//判断递归到根节点的异或权值是否为0,不为0则要ans++
cout << ans << endl;
}