Description
给定一棵 n n n 个点的树,每次操作可以选择两个点 x x x 和 y y y ,让 x → y x \to y x→y 的路径上的边权异或上一个随意的非负整数。求至少几次操作才能让树的边权都为 0 0 0。
1 ≤ n ≤ 2 × 1 0 5 , 0 ≤ a i ≤ 15 1 \leq n \leq 2 \times 10^5, 0 \leq a_i \leq 15 1≤n≤2×105,0≤ai≤15。
Solution
状压 dp,
n
n
n 是人类迷惑行为。
在边上处理太复杂了,那么能不能将边权转换为点权呢?可以发现,如果将每个点的点权设为它的出边的异或和,那么对于一条路径,中间的每个点都有两个在路径上的出边,因为 a ⊕ x ⊕ x = a a \oplus x \oplus x = a a⊕x⊕x=a,所以中间的点抵消点权不变,而两个端点异或了 x x x。
把点权 ≠ 0 \not= 0 =0 的点放在序列上,那么问题转换成了:每次操作找出序列的两个数异或上 x x x,求让序列全为 0 0 0 的最少操作次数。将相同的数配对,因为一次操作可以让它们为 0 0 0。因为 0 ≤ a i ≤ 15 0 \leq a_i \leq 15 0≤ai≤15,在配对完后,我们得到了一个集合,最多有 15 15 15 个元素且值域在 [ 1 , 15 ] [1,15] [1,15]。
因为 a ⊕ x ⊕ x = a a \oplus x \oplus x = a a⊕x⊕x=a,所以集合的异或和不变。这样意味着,如果一个子集能通过操作使得所有的数为 0 0 0,必须满足子集的异或和为 0 0 0。在这个前提下,有一个重要的性质:集合 S S S 如果有方案使得其全为 0 0 0,那么需要 ∣ S ∣ − 1 |S| - 1 ∣S∣−1 次操作。
略证:对于集合 S S S,将 ( S 1 , S 2 ) ⊕ S 1 (S_1,S_2) \oplus S_1 (S1,S2)⊕S1,得到新的 S 2 S_2 S2,再将 ( S 2 , S 3 ) ⊕ S 2 (S_2,S_3) \oplus S_2 (S2,S3)⊕S2,得到新的 S 3 S_3 S3,重复进行下去,直到 ( S n − 1 , S n ) ⊕ S n − 1 S n − 1 = ⊕ s = 1 n − 1 S i (S_{n-1},S_n) \oplus S_{n-1} \quad S_{n-1} = \oplus_{s=1}^{n-1} S_i (Sn−1,Sn)⊕Sn−1Sn−1=⊕s=1n−1Si,因为有方案所以 ⊕ s = 1 n S i = 0 \oplus_{s=1}^{n} S_i = 0 ⊕s=1nSi=0,又因为 a ⊕ a = 0 a \oplus a = 0 a⊕a=0,所以得到的 S n = 0 S_n = 0 Sn=0。一共进行了 ∣ S ∣ − 1 |S|-1 ∣S∣−1 次操作。
也就是说,一个异或和为 0 0 0 的子集需要 ∣ S ∣ − 1 |S|-1 ∣S∣−1 次操作,那么要将集合分成尽可能多的这样的子集。可以预处理出集合每个数状态的异或和,然后枚举子集转移。
时间复杂度为 O ( 3 16 + n ) O(3^{16} + n) O(316+n)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5, M = (1 << 15);
int val[N], cnt[N], sum[M], f[M];
int main() {
int n; scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int x, y, z; scanf("%d%d%d", &x, &y, &z);
val[++x] ^= z, val[++y] ^= z;
}
for (int i = 1; i <= n; ++i) cnt[val[i]]++;
int ans = n - cnt[0], mask = 0;
for (int i = 1; i <= 15; ++i) {
ans -= cnt[i] / 2 ;
mask |= (cnt[i] & 1) << (i - 1);
}
for (int i = 0; i < (1 << 15); ++i)
for (int j = 1; j <= 15; ++j)
if (i & (1 << (j - 1))) sum[i] ^= j;
for (int i = 1; i < (1 << 15); ++i)
for (int j = i; j; j = (j - 1) & i)
if (!sum[i]) f[i] = max(f[i], f[i ^ j] + 1);
printf("%d\n", ans - f[mask]);
return 0;
}