AT3913 XOR Tree(边权转点权 + 状压 dp)

Description

给定一棵 n n n 个点的树,每次操作可以选择两个点 x x x y y y ,让 x → y x \to y xy 的路径上的边权异或上一个随意的非负整数。求至少几次操作才能让树的边权都为 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 1n2×105,0ai15

Solution

状压 dp, n n n 是人类迷惑行为

在边上处理太复杂了,那么能不能将边权转换为点权呢?可以发现,如果将每个点的点权设为它的出边的异或和,那么对于一条路径,中间的每个点都有两个在路径上的出边,因为 a ⊕ x ⊕ x = a a \oplus x \oplus x = a axx=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 0ai15,在配对完后,我们得到了一个集合,最多有 15 15 15 个元素且值域在 [ 1 , 15 ] [1,15] [1,15]

因为 a ⊕ x ⊕ x = a a \oplus x \oplus x = a axx=a,所以集合的异或和不变。这样意味着,如果一个子集能通过操作使得所有的数为 0 0 0,必须满足子集的异或和为 0 0 0。在这个前提下,有一个重要的性质:集合 S S S 如果有方案使得其全为 0 0 0,那么需要 ∣ S ∣ − 1 |S| - 1 S1 次操作。

略证:对于集合 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 (Sn1,Sn)Sn1Sn1=s=1n1Si,因为有方案所以 ⊕ s = 1 n S i = 0 \oplus_{s=1}^{n} S_i = 0 s=1nSi=0,又因为 a ⊕ a = 0 a \oplus a = 0 aa=0,所以得到的 S n = 0 S_n = 0 Sn=0。一共进行了 ∣ S ∣ − 1 |S|-1 S1 次操作。

也就是说,一个异或和为 0 0 0 的子集需要 ∣ S ∣ − 1 |S|-1 S1 次操作,那么要将集合分成尽可能多的这样的子集。可以预处理出集合每个数状态的异或和,然后枚举子集转移。

时间复杂度为 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; 
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值