CF741D Arpa’s letter-marked tree and Mehrdad’s (Dsu on tree)

题目链接: Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

大致题意

给定一棵有根树, 有n个顶点. 原树根节点是1号顶点. 每条边连接两个点, 边的权值为一个小写字母(仅包含字母a~v).

现在让你分别在以1~n为根节点的子树中, 找到一条路径, 使得路径上的所有字母随意排列后组成的回文串尽可能的长. 最终对于每一个根节点, 输出最长回文串长度.

解题思路

dsu on tree (算法发明者出的题, 当然要用树上启发式合并啦)

以下思路基于你想到了用dsu on tree来处理子树问题

我们首先考虑回文串, 即: 这一串字母中, 出现次数为奇数的字母不超过1个. 我们很容易想到把每个字母压位来统计情况. 这样我们在判断一个字符串序列是否合法, 我们可以判断其是否为2的整次幂 或 0.

我们考虑如何去统计答案, 对于一个根节点x而言, 其答案的来源有三种:
① 以其轻儿子为根时, 得到的答案
② 以其重儿子为根时, 得到的答案
③ 以当前节点x为根时, 得到的答案

其实对于前两种, 我们直接递归处理, 和子树的答案取max即可(有点分治那味儿了)

对于第三种情况, 以当前子树为根时, 我们又可以分出两种情况:
​ ① 形成折链, 即: 存在一条路径, 穿过当前根节点x, 起始(结束)于两条不同的树链内.
​ ② 形成直链, 即: 存在一条路径, 为根节点x到子树内某一节点.

到这里, 我们就已经分析完所有答案来源, 以及符合答案的情况了. 我们需要考虑如何去处理维护


本题有一点比较烦, 就是他的值不在点上, 而在边上, 我们其实可以通过跑第一遍dfs的时候, 把所有边的信息维护到点上. 即: 把fa->x这条边上的权值维护到点x上.

此部分内容可以不看

但此时我们很快发现了一个问题, 就是当把轻儿子信息合并到重儿子上时, 由于我们只维护的值是两条边上的权值, 当进行合并时, 我们原本记录的路径值是针对于当前根节点的, 合并后根节点发生了变化, 我们此时记录的路径值就不对了.

综上所述, 我们发现这样把一条边的值维护到点上不好.

我们正确的做法应该是, 把从1号节点开始, 整个路径上的边权维护在当前节点上. 这样做的一个好处就是, 由于我们记录的信息是1号点到当前点的, 我们根节点改变后, 从重儿子处继承的信息仍可以正确利用.


现在我们该考虑如何去统计以当前节点x为根时的答案了.

当我们访问到一个点y时, 我们需要找到另外一个点z, 使得y -> z路径合法. 考虑到我们最开始提到的, 我们需要判断这一段路径的值是不是2的整次幂 或者 0, 我们发现, 当我们把y和z路径的值异或后, 1->x路径的值会被抵消掉. 因此我们可以直接暴力枚举所有2的整次幂(假设为k), 看看是否存在k⊕yval, 若存在, 则表明存在一个合法点z, y->z路径满足要求.

那么我们如何找到一个最大回文串长度呢?
我们可以用其深度来计算, 串长 = depth[y] + depth[z] - 2 * depth[x].

根据上面的分析, 我们发现我们需要维护一个桶, 桶的大小为(1 << 22), 桶内记录对于当前值, 最大的节点深度是多少.


感觉好难说明白, 看代码吧QAQ.

AC代码

#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
#define debug(a) cout << #a << " = " << a << endl;
using namespace std;
typedef long long ll;
const int N = 5E5 + 10, B = 22;
int w[N]; vector<pair<int, int>> edge[N];

int son[N], sz[N], depth[N];
void dfs1(int x, int fa, int c) {
	sz[x] = 1, depth[x] = depth[fa] + 1;
	w[x] = w[fa] ^ (1 << c);

	for (auto& [to, val] : edge[x]) {
		if (to == fa) continue;
		dfs1(to, x, val);
		sz[x] += sz[to];
		if (sz[to] > sz[son[x]]) son[x] = to;
	}
}


int res[N]; int lca; //根节点, 相当于上文的x节点.
vector<int> v; //经过的点
int have[1 << B];
void calc(int x, int fa) { //当前遍历到y节点了
	v.push_back(x);

	if (have[w[x]]) res[lca] = max(res[lca], depth[x] + have[w[x]] - 2 * depth[lca]); //0的情况
	for (int i = 0; i < B; ++i) { //暴力枚举2的整次方
		int target = 1 << i;
		int need = target ^ w[x]; // 我们想找到z节点
		if (have[need]) res[lca] = max(res[lca], depth[x] + have[need] - 2 * depth[lca]);
	}

	for (auto& [to, val] : edge[x]) {
		if (to == fa) continue;
		calc(to, x);
	}
}

void del(int x, int fa) { //轻儿子, 删除子树贡献.
	have[w[x]] = 0;
	for (auto& [to, val] : edge[x]) if (to != fa) del(to, x);
}
void dfs2(int x, int fa, int tp) {
	for (auto& [to, val] : edge[x]) {
		if (to == fa or to == son[x]) continue;
		dfs2(to, x, 0);
		res[x] = max(res[x], res[to]);
	}

	if (son[x]) {
		dfs2(son[x], x, 1);
		res[x] = max(res[x], res[son[x]]);
	}

	lca = x; //统计以当前节点为根的情况
	for (auto& [to, val] : edge[x]) { //计算折链的情况
		if (to == fa or to == son[x]) continue;
		calc(to, x);
		for (auto& op : v) have[w[op]] = max(have[w[op]], depth[op]);
		v.clear();
	}

	/* 计算直链的情况 */ //这里同calc函数内
	if (have[w[x]]) res[lca] = max(res[lca], depth[x] + have[w[x]] - 2 * depth[lca]);
	for (int i = 0; i < B; ++i) {
		int target = 1 << i;
		int need = target ^ w[x];
		if (have[need]) res[x] = max(res[x], have[need] - depth[x]);
	}

	have[w[x]] = max(have[w[x]], depth[x]);
	if (!tp) del(x, fa);
}

int main()
{
	int n; cin >> n;
	for (int i = 2; i <= n; ++i) {
		int p; char s[2]; scanf("%d %s", &p, s);
		s[0] -= 'a';
		edge[p].push_back({ i, *s }), edge[i].push_back({ p, *s });
	}

	dfs1(1, 0, 0); 
	dfs2(1, 0, 1);

	rep(i, n) printf("%d%c", res[i], " \n"[i == n]);
	return 0;
}

END

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

逍遥Fau

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

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

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

打赏作者

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

抵扣说明:

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

余额充值