AT3913 XOR Tree
经过长时间的思考,我发现直接考虑对一条链进行修改是很难做出本题的,可能需要换一个方向。
可以发现本题中有操作的存在,是没有可以反过来做的做法的,因此正难则反这条路应该走不通。
那么唯一的办法就是简化这个操作或是转化问题了,不难发现前者应该是后者的基础,于是我们应该先将重心放在前者上。
可以发现本题是对一条链进行同一个操作,那么能否使用差分来使得多点操作变成单点操作呢?
不难发现由于差分的优秀性质是可以的,对于一次修改 \((x, y, k)\),我们只需要要在 \(x, y\) 处分别异或 \(k\),那么最终每条边的真实值就是这条边底下的点所在子树内修改的异或之和。
因为对于不在 \(x, y\) 链上的点,计算异或和时 \(x, y\) 会算两次相抵消。
但你很快就会发现一个问题,最终所有边全为 \(0\) 的条件是什么?
你会认为是所有点权为 \(0\),但事实上并非如此。
因为上述的差分事实上是将边权下方到点权的过程,那么根的权值应该是什么呢?
你会发现因为根上面没有边,所以这是一个未定义的问题,是不可以解决的。
那么只能再换一种思路了,不难发现我们我们的目的在于减少修改的点权,那么上面的过程暗示着我们要尽可能异或两次将中间的操作抵消。
不难发现的是,因为在一次链修改 \((x, y, k)\) 当中,除了 \(x, y\) 以外其他链上的点都连着恰好两条在链上的边。
那么我们只要定义每个点的点权包含这两点的异或和即可。
不难发现这个定义的点权要具有普适性,因此我们需要将每个点的点权定义为周边所有边的异或和。
那么这样每次操作只需修改两个点的点权即可。
你会发现上面的未定义问题也消失了,最终的状态不难发现就是所有点的点权为 \(0\)(从叶子开始向上归纳证明即可)。
那么现在的问题就转化成给定 \(n\) 个点权 \(a(a_i \le 15)\),每次操作选择两个点权异或上 \(x\) 求最小的操作次数使得所有点权变为 \(0\)。
那么你会发现每次选择两个相同的数直接异或这两个数是一定最优的,但这样也会产生一个问题:会不会经过这样一次操作后使得操作无解?
那么我们一定要思考的一个东西就是答案有解的充要条件。
直接寻找充分条件是不好找的,我们一般先寻找一些必要条件再组合成充要条件。
不难发现因为每次又是同时异或两个点,那么同样的总异或和还是不变的,那么一开始的总异或和为 \(0\) 是否是有解的充要条件呢?
不难发现是的,只需要每次选定两个点然后将其中一个消去即可。因为最终异或和为 \(0\),就一定不会剩下一个数落单的情况。
于是就可以发现不论我们进行什么操作,都是不会影响解的存在性的,那么就可放心地去寻找最优策略了。
回到最开始的想法,每次找到两个相同的权值直接同时消去显然是最优的。
那么这样一来最终我们剩下的点权就只有 \(16\) 个了。
还需要注意的是,\(0\) 是不需要消的。因为如果 \(0\) 和 \(a\) 消了一个 \(x\),再和 \(b\) 消回来,是等价于 \(a, b\) 消这个数的。
不难发现当点权没有重复时是不能直接贪心的,因为选择不一样的两个点会影响的状态也有所不同,那么只能考虑使用 \(dp\)。
因为点权非常少,可以直接考虑使用状压 \(dp\)。
很简单的,我们可以直接令 \(dp_S\) 为当前剩余数状态为 \(S\) 所需的最小操作次数。
那么每次转移需要枚举 \(S\) 中的两个点 \(i, j\),对 \(i, j\) 进行一次操作。
一种很直接的想法就是将 \(i, j\) 中的一个消去留下另一个,那么这样是否会更优呢?
不难发现确实是的,因为如果假设 \(a, b\) 异或上了 \(x\),最优情况下就是 \(S\) 中同时存在 \(c, d\) 使得 \(\rm a \ xor \ x = c, b \ xor \ x = d\)。
此时消去四个数也需要 \(3\) 次操作,不难发现每次至少消一个数也最多只要消 \(3\) 次。
于是每次转移到的状态就是 \(\rm S \ xor \ 2 ^ i \ xor \ 2 ^ j \ xor \ 2 ^ {i \ xor \ j}\),那么如果 \(S\) 中存在 \(\rm i \ xor \ j\),根据最开始的理论可知直接消去即可。
可以注意到的是每次转移到的状态总是比 \(S\) 小的,因此这个 \(dp\) 是有效的,从大往小转移即可。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define dep(i, l, r) for (int i = r; i >= l; --i)
const int N = 100000 + 5;
const int M = (1 << 16) + 5;
int n, u, v, w, ans, fir, a[N], dp[N], cnt[N];
int read() {
char c; int x = 0, f = 1;
c = getchar();
while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
int main() {
n = read(), memset(dp, 0x7f, sizeof(dp));
rep(i, 1, n - 1) u = read() + 1, v = read() + 1, w = read(), a[u] ^= w, a[v] ^= w;
rep(i, 1, n) ++cnt[a[i]];
rep(i, 1, 15) ans += cnt[i] / 2, cnt[i] %= 2;
rep(i, 1, 15) if(cnt[i]) fir += (1 << i);
dp[fir] = 0;
dep(S, 1, fir) if(dp[S] < n){
rep(i, 0, 15) if((1 << i) & S) {
rep(j, 0, 15) if((1 << j) & S) {
int val = 1, sta = S ^ (1 << i) ^ (1 << j) ^ (1 << (i ^ j));
if(S & (1 << (i ^ j))) ++val;
dp[sta] = min(dp[sta], dp[S] + val);
}
}
}
printf("%d", ans + dp[0]);
return 0;
}