P9745 「KDOI-06-S」树上异或 (位运算trick+树形 dp)

P9745 「KDOI-06-S」树上异或

2024.11.15

位运算trick+树形 dp

求出每个方案的答案肯定不现实。考虑转化贡献,我们求连通块对每个方案的贡献就行。计数问题当然首想 dp。

位运算又可以考虑按位计算。这样每个连通块就只有 0/1 两种贡献了,直接可以放到状态里。当然每个方案也就只有 0/1

维护什么答案?如果我们知道当前所有方案的权值之和,那么新添加的连通块就可以看做一个乘数直接乘在答案上,这是很好维护的。

注意到以 u 为根的连通块在还没枚举完子树前,无法确定贡献,所以就先不考虑它。

所以自然就设 gu,0/1 表示 u 子树内部的所有断边方案的权值之和(除 u 所在连通块),u 所在连通块的贡献为 0/1。那么对应的最终要求的就是 fu 表示 u 子树内部的所有断边方案的权值之和。

转移之一:gu,0=gu,0×gv,0+gu,1×gv,1+gu,0×fv

如果考虑的是第 i 位,以 u 为根的连通块对 fu 的贡献就是 2i×gu,1

按位考虑完全可以放在 dfs 中,所以 g 的状态再加一位描述哪一位就行。

答案是 f1

看到题目中贡献的计算,可以想到乘法分配律,也就是一个连通块的乘积可以直接乘在当前所有方案的权值之和上

可以考虑特殊性质:。那么树的问题就变成了序列问题。容易设 fi 表示 i 以前的节点的所有断边方案权值和。转移直接枚举断边:fi=fj×(sisj)si=j=1iaj

复杂度是 O(n2)。瓶颈在于断边的枚举,同时又注意到位运算的相关性质还没有用到,考虑这个方面的优化,一个经典 trick 就是按二进制位计算贡献,这样做的原理一个是位运算是按位计算,一个是答案的计算满足分配律。

可以设 gi,j,0/1 表示考虑到第 i 个位置,与 i 相连的连通块在第 j 位二进制位上的值为 0/1,并且强制 i 向后连边的断边方案权值和(不计算 i 的影响)。这样做的好处就是转移不再需要枚举断边。转移有:

gi,j,0=gi,j,0×(fi1+gi1,j,0)+gi,j,1×gi1,j,1

gi,j,1=gi,j,1×(fi1+gi1,j,0)+gi,j,0×gi1,j,1

fi=j=0632j×gi,j,1

实际上在 g 转移时,fi 转移的就是 i 单独成为一个连通块的情况,而 g 转移的就是与前面的连通块相连时的贡献,可以发现这样的两种情况是不存在交集的。最后 fi 的计算就是计算 i 所在连通块每一位上的贡献(与先前的枚举断边的方法等价),这里转换了计算贡献的角度。

考虑树上的做法,其实完全类似,就是把式子搬到树上,这里就不列举,重点还是序列问题时的思路。

具体在写转移的时候要注意把当前状态存起来。

复杂度 O(nlogV)

#include <bits/stdc++.h>
#define pii std::pair<int, int>
#define fi first
#define se second
#define pb push_back

typedef long long i64;
const i64 iinf = 0x3f3f3f3f, linf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e5 + 10, mod = 998244353;
int n;
i64 a[N];
i64 f[N];
int g[N][65][2];
std::vector<int> e[N];
void dfs(int u, int fa) {
	for(int i = 0; i <= 63; i++) g[u][i][(a[u] >> i) & 1] = 1;
	for(auto v : e[u]) {
		if(v == fa) continue;
		dfs(v, u);
		for(int i = 0; i <= 63; i++) {
			i64 u0 = g[u][i][0], u1 = g[u][i][1]; //先存起来,防止转移错误
			g[u][i][0] = (u0 * (g[v][i][0] + f[v]) % mod + u1 * g[v][i][1] % mod) % mod;
			g[u][i][1] = (u0 * g[v][i][1] % mod + u1 * (f[v] + g[v][i][0]) % mod) % mod; 
		}
	}
	for(int i = 0; i <= 63; i++) f[u] = (f[u] + (1ll << i) % mod * g[u][i][1]) % mod;
}
void Solve() {
	std::cin >> n;
	for(int i = 1; i <= n; i++) std::cin >> a[i];
	for(int i = 2; i <= n; i++) {
		int u;
		std::cin >> u;
		e[u].pb(i), e[i].pb(u);
	}
	dfs(1, 0);
	std::cout << f[1] << "\n";
}
int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
	Solve();

	return 0;
}
posted @   Fire_Raku  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
点击右上角即可分享
微信分享提示