CF570D Tree Requests
传送门
题目大意
给定一颗以节点\(1\)为根的树,包含\(n\)个节点,每个节点上都有一个字符\(c_i\)。
特别的,\(1\)号节点的深度为\(1\)。
共有\(m\)个问询,每次询问:在子树\(v_i\)中所有深度(相对于根)\(h_i\)的所有节点能否构成回文串。
数据范围
- \(1\leq n,m\leq 5\times10^5\)
- \(1\leq v_i,h_i\leq n\)
样例
输入输出样例 #1
输入 #1
6 5
1 1 1 3 3
zacccd
1 1
3 3
4 1
6 1
1 2
输出 #1
Yes
No
Yes
Yes
Yes
思路
dsu on tree 板子题。
首先分析构成回文串的要求,可以用一个cnt[dep][ch]数组统计当前子树中深度为\(dep\)的字符\(ch\)出现次数。
离线统计问询中\(v_i=u\)的情况,在\(\text{dfs}\)到子树\(u\)[1]时只需将所有问询为\(u,d_{u,j}\)对应的cnt[d_u,j][0~25]全部&=1再加到\(res\)上,若最终\(res\leq 1\)则说明可以构成回文串。
例
6 5
1 1 1 3 3
zacccd
1 1
3 3
4 1
6 1
1 2
中,将1 1和1 2挂到节点1上处理
dsu on tree
由于空间限制,必须重复利用cnt数组,为此,在每颗树内,除了最后一个被枚举到的子树不会影响到后续(根本就没有后续)之外全部都要清空。
那么为什么需要清空子树呢?
虽然某颗子树不清空不会影响原树的统计,但是会影响到原树下的其他子树(在其后枚举的子树)
为了子树之间不影响,必须在dfs完时将其cnt清空。
在统计完最后一颗子树时,就要统计原树了。
那么很显然如果将重子树放在最后处理,清空的量是最少的。
虽然感觉很暴力玄学,但是时间复杂度仍然达到了惊人的\(\text{O(NlogN)}\)!
证明看这里
此时再把前面的子树贡献加回来,补全原树。
最后特判根节点贡献就行了。
代码:
struct QUERY
{
int id; // 此问询在问询列表中的位置
int h; // 此问询的查询深度
};
vector<QUERY> query[N]; // query[u]: 节点u的问询
vector<int> e[N]; // 原题输入给的是父亲表示法,此处转为儿子表示法
void merge(int u)
{
++cnt[dep[u]][a[u]];
for (auto j: e[u]) merge(j);
}
void delet(int u)
{
--cnt[dep[u][a[u]]; // 或者直接等于0
for (auto j: e[u]) delet(j);
}
// dfs1是树剖,用于确定重儿子和深度
dfs2(int u, bool ishvy) // 当前枚举到子树u,及是否为重儿子
{
for (auto j: e[u]) { // 枚举u的儿子
if (j == hson[u]) continue;
dfs2(j, 0);
}
// 注意判断u是否为叶子节点,没有重儿子
if (hson[u]) dfs2(hson[u], 1);
for (auto j: e[u]) {
if (j == hson[u]) continue;
merge(j);
}
++cnt[dep[u]][a[u]];
int check = 0;
for (auto i: query[u]) {
for (int j = 0; j < 26; ++j) check += cnt[i.h][j] & 1;
if (check <= 1) ans = 1;
}
if (!ishvy) delet(u);
}
坑
写cnt的时候千万不要将cnt[i][j]错当成子树i了,我灵(脑)光(子)乍(抽)现(了)搞错了结果过了样例(好玄学???)然后WA了#TestPoint 2……
输入的时候要警惕#TP41,输入多打了一个回车,那些没有使用以下代码来跳过回车/空格
while (c == '\n || c == ' ') c = getchar();
的人估计就是这个点被HACK了
CF的hack机制还是太难绷了,居然有人用这种数据叉别人
完整代码
/*
AUTHOR: peter_code
DATE: 25/08/08
PROBLEM: CF570D
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int n, m, a[N]; // 节点数、问询数、节点权
vector<int> e[N]; // 节点i所有的儿子
int hson[N]; // 重儿子
int sz[N]; // 子树大小
int dep[N]; // 节点深度
bool ans[N]; // 记录"Yes"/"No"结果
int cnt[N][26]; // cnt[i][ch]: 深度为i,权为ch的节点出现次数
// 节点u上的问询维护:
struct QUERY
{
int id; // 第 id 个问询
int h; // 查询树中深度为h[i]的回文串构成情况
};
vector<QUERY> query[N];
int dfs1(int u, int d)
{
hson[u] = 0;
sz[hson[u]] = 0;
dep[u] = d;
sz[u] = 1;
for (auto i: e[u]) {
sz[u] += dfs1(i, d + 1);
if (sz[i] > sz[hson[u]]) hson[u] = i;
}
return sz[u];
}
void merge(int u) // 合并根为u的子树
{
++cnt[dep[u]][a[u]];
for (auto v: e[u]) merge(v);
}
void delet(int u) // 消除子树u的贡献
{
cnt[dep[u]][a[u]] = 0;
for (auto v: e[u]) delet(v);
}
void dfs2(int u, bool hvy) // 节点x,是否为重儿子(是否需保留信息)
{
for (auto v: e[u]) {
if (v == hson[u]) continue; // 优先处理轻儿子,重儿子最后处理
dfs2(v, 0);
}
// 统计u重儿子的贡献(不会被清)
if (hson[u]) dfs2(hson[u], 1); // 注意u为叶子节点无重儿子情况
for (auto v: e[u]) {
if (v == hson[u]) continue; // 重儿子已经统计过了
merge(v); // 统计轻儿子贡献
}
++cnt[dep[u]][a[u]]; // 特判根节点贡献
// 统计挂在节点u上的答案
//printf("In Sub-tree %d:\n", u);
for (auto i: query[u]) {
int res = 0;
for (int j = 0; j < 26; ++j) {
//printf("\tThere are %d '%c' on depth %d.\n", cnt[u][i.h]);
res += cnt[i.h][j] & 1;
}
ans[i.id] = res <= 1;
}
if (!hvy) delet(u);
}
int main()
{
//freopen("D.in", "r", stdin);
scanf("%d%d", &n, &m);
for (int i = 2; i <= n; ++i) {
int tmp;
scanf("%d", &tmp);
e[tmp].push_back(i);
}
char c = getchar();
while (c == '\n' || c == ' ') c = getchar();
for (int i = 1; i <= n; ++i) {
a[i] = c - 'a';
c = getchar();
}
for (int i = 1; i <= m; ++i) {
int tmpx, tmpy;
scanf("%d%d", &tmpx, &tmpy);
query[tmpx].push_back({i, tmpy});
}
dfs1(1, 1);
dfs2(1, 1);
for (int i = 1; i <= m; ++i) {
if (ans[i]) printf("Yes\n");
else printf("No\n");
}
return 0;
}
后记
本人第一次写蓝题题解,若有不满,可在评论区提出,我一定尽力改正,谢谢!
除特殊说明外,文中子树\(u\)皆表示以\(u\)为根节点的子树 ↩︎

浙公网安备 33010602011771号