P3401 洛谷树 (树链剖分) 好题

题目链接: P3401 洛谷树

大致题意

给定一棵有 n n n个节点的树, 每条边有边权 w i w_i wi. 有 m m m次如下操作:

1 a b 询问 a , b a, b a,b路径上, 所有子路径边权异或和的和.

2 a b c ( a , b ) (a, b) (a,b)这条边的权值修改为 c c c.

特别的, 每一时刻树中边权均 ∈ [ 0 , 1023 ] \in[0, 1023] [0,1023]

解题思路

思维

首先对于树上问题, 我们考虑化为序列问题求解.
我们发现并不好在10进制下处理出所有区间异或和. 因此考虑拆位.

到此, 我们把问题变为:

给定一个长度为 n n n01序列, 有 m m m次如下操作:
1 l r 询问 [ l , r ] [l, r] [l,r]区间的所有连续子数组的异或和的和.
2 l r c [ l , r ] [l, r] [l,r]区间的权值修改为 c c c.

我们发现, 难点就是在于询问操作. 朴素的暴力方式会到达 O ( n 2 ) O(n^2) O(n2)的复杂度.


考虑优化: 我们发现其实这是一个求区间异或值的问题. 一般对于这种情况, 我们不妨处理出前缀和, 对其前缀和形式进行讨论.

对于前缀和数组而言, 一段区间的异或和相当于两点的异或和.
由此我们进行转化: 区间的所有连续子数组异或和 -> 前缀和数组中任意两点的异或和.

我们不难发现, 对于前缀和数组而言, 只有当 0   x o r   1 0 \ xor \ 1 0 xor 1时会产生贡献, 因此产生的总贡献为 c o u 0 × c o u 1 cou0 \times cou1 cou0×cou1. (0的数目 乘 1的数目)

再加上区间修改操作, 我们不难想到可以通过线段树来维护信息.

再考虑到扩展版本, 如果不是 01 01 01序列, 我们可以拆位来用多棵线段树维护.


树链剖分

对于本题的序列版本我们已经在上文中解决了, 因此我们不难想到用树链剖分把树上问题转化为序列问题求解.

但是本题还是有很多细节在里面:

  1. 首先所有的权值在边上, 而不是在点上, 因此我们需要把树中边权 -> 点权. 然后处理出树中前缀异或和.
  2. 对于修改操作, 我们发现在树中影响的节点会是一棵子树, 若修改前节点的值为 x x x, 修改后为 y y y, 则我们需要对 x x x y y y中二进制位不相同的位进行修改.
    对其子树的影响: 假设为第 k k k位, 那么当前节点及其子树 前缀和第 k k k位必然会翻转. (因此树中维护一个区间翻转懒标记即可)
  3. 对于查询操作, 我们虽然把树中的边权化为了点权, 但是由于我们统计的是其前缀和数组中的 0 0 0 1 1 1的个数, 因此 L C A LCA LCA位置的信息仍然需要统计.

AC代码

#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 3E4 + 10, B = 9;
int w[N], val[N];
struct EDGE { int to, c; };
vector<EDGE> edge[N]; //树上各个点之间的边

int p[N], dep[N], sz[N], son[N];
// 父节点   深度   节点大小 重儿子
void dfs1(int x = 1, int fa = 0) { // x = 树根节点
	p[x] = fa, dep[x] = dep[fa] + 1, sz[x] = 1;
	for (auto& [to, c] : edge[x]) {
		if (to == fa) continue;

		val[to] = c; w[to] = w[x] ^ c;
		dfs1(to, x);
		sz[x] += sz[to];		// 特别的, 如果边权->点权, 应记录w[to] = 边权.
		if (sz[to] > sz[son[x]]) son[x] = to; //更新重儿子
	}
}
int id[N], nw[N], top[N], ind;
//  新编号  新值    重链顶 当前用到的编号
void dfs2(int x = 1, int tp = 1) { // x = 树根节点, tp = 树根节点
	id[x] = ++ind, nw[ind] = w[x], top[x] = tp;

	if (!son[x]) return; //叶子结点
	dfs2(son[x], tp); //先遍历重儿子

	for (auto& [to, c] : edge[x]) {
		if (to == p[x] or to == son[x]) continue;
		dfs2(to, to);
	}
}


struct node {
	int l, r;
	int cou[B + 1];
	int flag;
}t[N << 2];
void pushdown(node& op, int b) {
	op.cou[b] = op.r - op.l + 1 - op.cou[b];
	op.flag ^= 1 << b;
}
void pushdown(int x) {
	if (!t[x].flag) return;
	for (int i = 0; i <= B; ++i) {
		if (t[x].flag >> i & 1) pushdown(t[x << 1], i), pushdown(t[x << 1 | 1], i);
	}
	t[x].flag = 0;
}

void pushup(node& p, node& l, node& r) { for (int i = 0; i <= B; ++i) p.cou[i] = l.cou[i] + r.cou[i]; }
void pushup(int x) { pushup(t[x], t[x << 1], t[x << 1 | 1]); }

void build(int l, int r, int x = 1) {
	t[x] = { l, r }; t[x].flag = 0;
	if (l == r) {
		for (int i = 0; i <= B; ++i) t[x].cou[i] = nw[l] >> i & 1;
		return;
	}
	int mid = l + r >> 1;
	build(l, mid, x << 1), build(mid + 1, r, x << 1 | 1);
	pushup(x);
}

void modify(int l, int r, int c, int x = 1) {
	if (l <= t[x].l and r >= t[x].r) {
		pushdown(t[x], c);
		return;
	}
	pushdown(x);
	int mid = t[x].l + t[x].r >> 1;
	if (l <= mid) modify(l, r, c, x << 1);
	if (r > mid) modify(l, r, c, x << 1 | 1);
	pushup(x);
}
auto ask(int l, int r, int x = 1) {
	if (l <= t[x].l and r >= t[x].r) return t[x];
	pushdown(x);
	int mid = t[x].l + t[x].r >> 1;
	if (r <= mid) return ask(l, r, x << 1);
	if (l > mid) return ask(l, r, x << 1 | 1);
	node left = ask(l, r, x << 1), right = ask(l, r, x << 1 | 1);
	node res = { left.l, right.r };
	pushup(res, left, right);
	return res;
}


void modify_route(int a, int b, int c) {
	if (dep[a] < dep[b]) swap(a, b);
	for (int i = 0; i <= B; ++i) {
		if ((val[a] ^ c) >> i & 1) {
			modify(id[a], id[a] + sz[a] - 1, i);
		}
	}
	val[a] = c;
}

int cou1[B + 1], cou0[B + 1];
ll ask_route(int a, int b) {
	while (top[a] != top[b]) {
		if (dep[top[a]] < dep[top[b]]) swap(a, b);
		auto qaq = ask(id[top[a]], id[a]);
		for (int i = 0; i <= B; ++i) {
			cou1[i] += qaq.cou[i];
			cou0[i] += (qaq.r - qaq.l + 1) - qaq.cou[i];
		}

		a = p[top[a]];
	}

	if (id[a] > id[b]) swap(a, b);
	auto qaq = ask(id[a], id[b]);

	ll res = 0;
	for (int i = 0; i <= B; ++i) {
		cou1[i] += qaq.cou[i];
		cou0[i] += (qaq.r - qaq.l + 1) - qaq.cou[i];

		res += 1ll * cou1[i] * cou0[i] * (1 << i);
		cou1[i] = cou0[i] = 0;
	}

	return res;
}
int main()
{
	int n, m; cin >> n >> m;
	rep(i, n - 1) {
		int a, b, c; scanf("%d %d %d", &a, &b, &c);
		edge[a].push_back({ b, c }), edge[b].push_back({ a, c });
	}

	dfs1();
	dfs2();
	build(1, n);

	rep(i, m) {
		int tp; scanf("%d", &tp);
		if (tp == 1) {
			int a, b; scanf("%d %d", &a, &b);
			printf("%lld\n", ask_route(a, b));
		}
		else {
			int a, b, c; scanf("%d %d %d", &a, &b, &c);
			modify_route(a, b, c);
		}
	}

	return 0;
}

END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逍遥Fau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值