题目链接: 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;
}