AtCoder Beginner Contest 454 题解
AtCoder Beginner Contest 454 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2026-04-18 |
| 比赛链接 | https://atcoder.jp/contests/abc454 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场 A–G 难度递进合理:A 是区间计数热身题;B 考察"集合大小"的理解;C 是图论建模 + BFS;D 需要发现"规范形式(canonical form)"这一关键概念,用栈在线性时间内完成归约,是很有代表性的字符串题;E 是构造题,先用棋盘染色确定可行性条件,再通过"剥皮"式的递归构造出合法路径;F 通过差分数组把区间加操作转化为"两个位置的独立增减",再配合排序贪心求解,思维含量很高;G 是树上启发式合并(DSU on Tree)的经典应用,数据范围高达 ,对实现效率与内存都有要求。建议重点掌握 D、F、G,尤其是 F 的两步换元思想与 G 的数组化小到大合并技巧。
A - Closed interval
题目链接:https://atcoder.jp/contests/abc454/tasks/abc454_a
题目大意
给定两个整数 (),求闭区间 内一共有多少个整数,输出该个数。
思路
区间 内的整数为 ,个数显然是
关键是题目强调 inclusive(闭区间),所以不是 而是 。直接读入 并输出 即可,无需任何循环或特殊处理。
易错点:忘记 +1,或者把约束中 默认当作正确输入而不做处理——本题保证 ,所以公式恒为正数。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int L, R;
cin >> L >> R;
cout << R - L + 1 << "\n";
return 0;
}官方题解同时提供了 Python 版本:
L, R = map(int, input().split())
print(R - L + 1)复杂度分析
- 时间复杂度:
- 空间复杂度:
B - Mapping
题目链接:https://atcoder.jp/contests/abc454/tasks/abc454_b
题目大意
有 个人、 种衣服(编号 ),第 个人穿的衣服类型为 ()。回答两个 Yes/No 问题:
- 是否 个人穿的衣服类型两两不同?
- 是否每种衣服类型都至少有一个人在穿?
思路
设 为所有 构成的集合(去重), 表示其中不同元素的个数。则两个问题可以简洁地翻译成:
- 问题 1:是否 ?因为"所有人衣服两两不同"当且仅当 个 的值互不重复,即不同值的个数等于人数。
- 问题 2:是否 ?因为"每种类型都被穿到"当且仅当 中每个值都出现,即不同值的个数等于类型总数。
由于 ,必有 ,因此例如 时问题 1 必然为 No, 时问题 2 必然为 No(鸽巢原理),但统一用 判断即可覆盖所有情况。
实现上,用一个长为 的布尔数组 seen 记录每个类型是否出现过,边读边统计不同值的个数 distinct,最后与 、 分别比较输出。
易错点:问题是两行独立输出,且必须严格匹配大小写 Yes / No。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
cin >> N >> M;
vector<bool> seen(M + 1, false);
int distinct = 0;
for (int i = 0; i < N; ++i) {
int f;
cin >> f;
if (!seen[f]) {
seen[f] = true;
++distinct;
}
}
cout << (distinct == N ? "Yes" : "No") << "\n";
cout << (distinct == M ? "Yes" : "No") << "\n";
return 0;
}官方题解还给出使用 std::set 的写法(思路完全相同,只是借助标准库集合去重)。本题用布尔数组即可,常数更小。
复杂度分析
- 时间复杂度:
- 空间复杂度:
C - Straw Millionaire
题目链接:https://atcoder.jp/contests/abc454/tasks/abc454_c
题目大意
有 种物品(编号 ),高桥初始只拥有物品 1。他有 个朋友,若把物品 交给第 个朋友,就能换得物品 ()。求高桥最终能够拥有的物品种类总数(包含初始的物品 1)。
思路
把问题建模成有向图: 个顶点对应 种物品,每条交换关系 是一条有向边。
关键观察:高桥每次交换只能给出当前持有的那一件物品,并且换得物品后原来那件就不在了。因此任何一条合法的"获得物品 "的过程,都对应着图中从顶点 1 出发到达顶点 的一条有向路径:
其中第 步正是"把物品 给对应朋友,换得 "。反之,只要存在这样一条路径,高桥就能顺着它一步一步完成交换。路径中的顶点互不相同(取简单路径即可),所用的边对应不同的朋友,互不冲突。于是:
高桥能获得的物品集合 = 从顶点 1 出发在有向图上可达的全部顶点。
因此从顶点 1 做一次 BFS(或 DFS)统计可达顶点个数即可,答案包含顶点 1 本身。
易错点:这是有向图,不是无向图——朋友关系只能从 走向 ,方向不能反;样例 2 中边是 、,从 1 出发没有任何出边,答案就是 1。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
cin >> N >> M;
vector<vector<int>> g(N + 1);
for (int i = 0; i < M; ++i) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
}
vector<bool> vis(N + 1, false);
queue<int> q;
vis[1] = true;
q.push(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : g[u]) {
if (!vis[v]) {
vis[v] = true;
q.push(v);
}
}
}
int ans = 0;
for (int i = 1; i <= N; ++i) {
if (vis[i]) ++ans;
}
cout << ans << "\n";
return 0;
}官方题解提供的 Python BFS 版本:
import sys
from collections import deque
input = sys.stdin.readline
n, m = map(int, input().split())
g = [[] for _ in range(n)]
for _ in range(m):
x, y = map(int, input().split())
g[x - 1].append(y - 1)
q = deque()
d = [False] * n
q.append(0)
d[0] = True
while q:
x = q.popleft()
for i in g[x]:
if d[i]:
continue
d[i] = True
q.append(i)
print(d.count(True))复杂度分析
- 时间复杂度:(每个顶点、每条边各访问一次)
- 空间复杂度:(邻接表 + 访问标记)
D - (xx)
题目链接:https://atcoder.jp/contests/abc454/tasks/abc454_d
题目大意
给定由 (、x、) 组成的字符串 和 。可以对 任意次、任意顺序地执行下面两种操作:
- 把 中出现的某个子串
(xx)替换为xx; - 把 中出现的某个子串
xx替换为(xx)。
判断能否通过若干次操作把 变成 。共 组询问,所有询问的 之和不超过 。
思路
第一步:把问题化归为"规范形式"
注意到两种操作互为逆操作((xx) ↔ xx),所以" 能变成 "是一个等价关系: 当且仅当 与 可以归约到同一个不可再变的串。
对任意串 ,定义它的规范形式 为:反复把其中的子串 (xx) 替换为 xx,直到不存在 (xx) 为止所得的串。
先证明 与归约顺序无关(唯一性):
对 的长度归纳。任意两个
(xx)出现位置必然互不相交(它们若重叠,一个长度为 4 的子串不可能在内部同时容纳两个长度为 4 的相同子串)。若 中恰有一个(xx),替换它之后长度减少 2,由归纳假设余下归约唯一;若有两个(xx),记为 ,替换 不会影响 的存在性。先换 再处理 、与先换 再处理 ,两者都归结为对同一中间串继续归约,由归纳假设殊途同归。故归约顺序不影响最终结果。
进而有等价关系:
可以通过操作变成 。
证明也很直观:
- ()把 按归约序列变成 ,再把 的归约序列逆着执行(每次把
xx扩展回(xx)),就能从 变回 。 - ()单次操作保持 不变(由唯一性,无论对 还是对操作后的 归约都得到同一个 ),所以整条操作链首尾的 相同。
于是问题变成:计算 与 并比较是否相等。
第二步:用栈在线性时间内计算
维护一个字符串 ,从左到右扫描 :
- 把当前字符追加到 末尾;
- 若 的最后 4 个字符恰好是
(xx),就把它们替换成xx。
为什么"只检查末尾 4 个字符"就足够?因为每当某个 (xx) 的右括号 ) 被压入的瞬间,这 4 个字符正好位于 的末尾,一定会被立刻归约;归约后 以 xx 结尾,虽然可能为新的 (xx) 埋下伏笔(例如处理 ((xx)):压入第 5 个字符后得到 ((xx),末尾 4 个字符恰为 (xx),立即归约为 (xx,再压入最后一个 ) 后再次构成 (xx) 并归约为 xx),但新的 (xx) 必然以下一次压入的字符作为右括号,因此在下一次循环中必然被检查到。所以算法结束时 中不存在任何 (xx) 子串,且 是由 通过合法归约得到的,由唯一性知 。
复杂度:每个字符被压入一次,每次归约减少 2 个字符,归约次数不超过 ,故单串总时间为 。
易错点
(xx)的替换是"末尾 4 个字符",要正确写出末尾子串的比较(如T.compare(T.size()-4, 4, "(xx)"))。- 注意归约后可能连锁产生新的
(xx),所以每次压入字符后都要检查一次,而不是只在出现)时检查。
参考代码
#include <bits/stdc++.h>
using namespace std;
string canonical(const string& S) {
string T;
for (char ch : S) {
T.push_back(ch);
if (T.size() >= 4 && T.compare(T.size() - 4, 4, "(xx)") == 0) {
T.resize(T.size() - 4);
T += "xx";
}
}
return T;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
string A, B;
cin >> A >> B;
cout << (canonical(A) == canonical(B) ? "Yes" : "No") << "\n";
}
return 0;
}复杂度分析
- 时间复杂度:每组 ,总
- 空间复杂度:(临时串的长度不超过原串)
E - LRUD Moving
题目链接:https://atcoder.jp/contests/abc454/tasks/abc454_e
题目大意
在 网格上,棋子初始在 。执行恰好 次移动(每次移动到相邻的上/下/左/右格子),最终到达 ,且要求恰好访问一次除 外的每个格子(起点终点不能中途经过)。判断是否可行;可行时输出任意一条合法的 L/R/U/D 移动序列。共 组询问,。
思路
第一步:棋盘染色给出可行性必要条件
把格子按 的奇偶染色: 为偶数的格子记为黑,奇数的记为白。由于每次移动都会改变 的奇偶,棋子每走一步颜色必定翻转。
起点 与终点 的 都是偶数( 与 ),所以都是黑格。整个访问序列长为 (除 外全部格子),颜色序列为 黑→白→黑→白→…,且首尾都是黑。
- 若 为奇数: 为偶数,序列长为偶数且以黑开头,则最后一步落在白格;但终点 是黑格,矛盾,答案为
No。 - 若 为偶数: 为奇数,序列以黑开头、以黑结尾,其中黑格数 ,白格数 。网格中黑格总数恰为 ,因此被跳过的 必须是白格,即
若 为偶数,则 是黑格,访问的黑格数将变成 ,同样矛盾,答案为 No。
因此答案是 Yes 的必要条件为: 为偶数且 为奇数。官方题解证明了该条件同时也是充分的,下面给出构造。
第二步:递归"剥皮"构造
把问题推广为 网格、从 走到 、避开 的哈密顿路径。
- 剥行:若 (禁区不在最上面两行),可以用前缀
把最上面两行全部蛇形走完(沿第 1 行向右、下到第 2 行、向左、下到第 3 行),恰好落在 ,问题化为 。若 ,则禁区在最上面两行,改为在后缀上剥掉最下面两行:用上述串的逆序 作为结尾,问题化为 。反复执行直到 。
剥最下面两行作为"后缀"的理由:后缀在整条路径的末尾被逆序应用——先解决内部子问题到达 ,再用 走完最下面两行并停在 。
剥列: 后,若 ,用前缀
DRUR(在 网格中恰好走完左边一个 方块并落到下一列顶部),问题化为 ;若 ,用后缀RURD剥掉最右边两列。反复执行直到 。基元:剩 时,由于坐标每次减 2 不改变奇偶, 为奇数保证禁区在 中只能是 或 。前者从 出发用
DR(下、右),后者用RD(右、下),都能访问其余 3 格并停在 。
正确性:每次剥皮都保证被剥的 2 行/2 列中不含禁区(剥行时 或 ;剥列时 或 ,且过程保持 ),路径在剥下的带状区域内部蛇形走满、不重复;前缀与后缀分别从两头"包夹",最终拼接即完整覆盖除禁区外的所有格子。边界情况 时两个循环都不执行,直接落到基元,答案分别是 DR 或 RD。
输出格式:Yes 换行后输出移动串(长度为 )。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int N, A, B;
cin >> N >> A >> B;
if (N % 2 == 1 || (A + B) % 2 == 0) {
cout << "No\n";
continue;
}
int a = A - 1, b = B - 1; // 转为 0-indexed
vector<string> pre, suf;
// 行剥皮:剥顶 2 行(前缀)或底 2 行(后缀)
string row(N - 1, 'R');
row += "D";
row += string(N - 1, 'L');
row += "D";
for (int it = 0; it < N / 2 - 1; ++it) {
if (a >= 2) {
pre.push_back(row);
a -= 2;
} else {
string t = row;
reverse(t.begin(), t.end());
suf.push_back(t);
}
}
// 列剥皮:剥左 2 列(前缀)或右 2 列(后缀)
for (int it = 0; it < N / 2 - 1; ++it) {
if (b >= 2) {
pre.push_back("DRUR");
b -= 2;
} else {
suf.push_back("RURD");
}
}
// 基元 2x2
if (a == 0 && b == 1) pre.push_back("DR");
else pre.push_back("RD");
string ans;
for (const string& s : pre) ans += s;
for (int i = (int)suf.size() - 1; i >= 0; --i) ans += suf[i];
cout << "Yes\n" << ans << "\n";
}
return 0;
}官方题解提供的 Python 版本(逻辑相同):
for _ in range(int(input())):
n, a, b = map(int, input().split())
a, b = a - 1, b - 1
if n % 2 == 1 or (a + b) % 2 == 0:
print("No")
continue
s1 = []
s2 = []
for _ in range(n // 2 - 1):
s = "R" * (n - 1) + "D" + "L" * (n - 1) + "D"
if a >= 2:
s1.append(s)
a -= 2
else:
s2.append(s[::-1])
for _ in range(n // 2 - 1):
s = "DRUR"
if b >= 2:
s1.append(s)
b -= 2
else:
s2.append(s[::-1])
assert (a, b) in ((0, 1), (1, 0))
if (a, b) == (0, 1):
s1.append("DR")
else:
s1.append("RD")
ans = "".join(s1) + "".join(s2[::-1])
print("Yes")
print(ans)复杂度分析
- 时间复杂度:每个询问 (输出串本身就有 个字符),总
- 空间复杂度:(存储答案串)
F - Make it Palindrome 2
题目链接:https://atcoder.jp/contests/abc454/tasks/abc454_f
题目大意
给定长度 的整数序列 ()。每次操作选择 ,把 都加上 (对 取模)。求使 成为回文( 对所有 成立)所需的最少操作次数。共 组询问,, 可达 。
思路
第一步:禁止跨越中点的操作
先考虑"跨过 中点的操作"。记 ,定义差值序列
是回文当且仅当所有 。
一次区间加操作 (加 )对 的影响:左侧 的 加 (因为 变大);右侧镜像 的 减 (因为 变大)。若操作跨越中点(),则左侧区间是 ,右侧镜像区间是 ,两者在 上重叠并互相抵消,净效果等价于在 上加 ——这恰是一个不跨中点的左半区间操作( 为奇数时中点格子的加 1 不影响任何 ,同理)。因此任何跨越中点的操作都可以被替换为不跨越中点的操作,不影响最少次数。
于是操作只剩两类:在左半区间 上加 (或 ,等价地由右半操作提供)。问题转化为:
反复选择 和 ,把 每个都加 (模 ),使 全变为 的最少操作次数。
第二步:差分数组化区间加为单点
对 取差分:令 ,定义
对 整体加 ,等价于 加 、 减 (均模 )。于是问题再次转化为:
反复选择两个下标 ,把 加 (模 )、 减 (模 ),使全部 变为 的最少操作次数。
原问题天然满足 ;放宽为允许 (此时是空操作)不改变答案。记 。
第三步:按"只增/只减"划分下标
对同一元素既加又减没有意义(可以约去),所以最优解中每个下标要么只接受加、要么只接受减。设 为只被减的下标集合,其余下标只被加;把 的元素放进 (贡献为 0,放哪边都行)。
记 。由于
是 的整数倍。
- 减的总次数:。
- 加的总次数:。
一次操作同时贡献 1 次加和 1 次减,故总操作次数为两者取大:
第四步:贪心选取最优
当 固定时, 只通过 依赖 的具体构成,因此取 中最小的 个元素即可最小化 。
再看 的选取: 每增大 1, 至多增加 (新增元素值 );而在 时,第二项 会减少 。净效果 与 之差仍为负,说明增大 严格更优,直到 达到 ;此后第二项恒为 0,,再增大 只会增加和,不再更优。
故最优解取
此时操作次数恰为 中最小的 个元素之和。
总结算法
- 计算 ();
- 计算差分数组 (长 ,含 两端);
- 将 排序,求 ,输出前 小的元素之和。
易错点:所有模运算要保证结果非负(C++ 中 可为负,需 ); 是精确整除( 是 的倍数); 可能很大,要用 64 位整数。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n;
long long m;
cin >> n >> m;
vector<long long> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
int np = n / 2; // N'
vector<long long> c; // 差分数组 C,长 N'' = N' + 1
c.reserve(np + 1);
long long prev = 0; // B_0 = 0
for (int i = 0; i < np; ++i) {
long long bi = (a[i] - a[n - 1 - i]) % m;
if (bi < 0) bi += m;
long long ci = (bi - prev) % m;
if (ci < 0) ci += m;
c.push_back(ci);
prev = bi;
}
long long last = (m - prev) % m; // C_{N''} = (B_{N'+1} - B_{N'}) mod M, B_{N'+1}=0
c.push_back(last);
sort(c.begin(), c.end());
long long S = 0;
for (long long x : c) S += x;
long long k = (long long)c.size() - S / m;
long long ans = 0;
for (long long i = 0; i < k; ++i) ans += c[i];
cout << ans << "\n";
}
return 0;
}官方题解提供的 Python 版本(非常简洁):
for _ in range(int(input())):
n, m = map(int, input().split())
a = list(map(int, input().split()))
b = [0] + [(a[i] - a[n - 1 - i]) % m for i in range(n // 2)] + [0]
c = [(b[i + 1] - b[i]) % m for i in range(len(b) - 1)]
c.sort()
print(sum(c[:len(c) - sum(c) // m]))复杂度分析
- 时间复杂度:每个询问 (瓶颈为排序),总
- 空间复杂度:
G - Mode in the Subtree
题目链接:https://atcoder.jp/contests/abc454/tasks/abc454_g
题目大意
给定一棵以 1 为根的树,顶点 的父亲为 (保证 ),每个顶点有颜色 。对每个顶点 ,设 为子树 中颜色 的个数,令 为所有 的最大值, 为达到该最大值的颜色种数。输出
其中 为按位异或。数据范围 ,树与颜色由给定的伪随机生成器生成。
思路
第一步:重建输入
题目输入了 、 与 ,需要按如下伪代码重建 与 :
state = seed
for i = 2..N:
if i <= M: p[i] = q[i]
else:
p[i] = (state mod (i-1)) + 1
state = (state * 1103515245 + 12345) mod 2^31
for i = 1..N:
if i <= M: c[i] = d[i]
else:
c[i] = (state mod F) + 1
state = (state * 1103515245 + 12345) mod 2^31注意 state 的递推中乘法可能溢出 32 位,必须用 64 位整数保存;模 可用 % (1LL<<31) 或按位与 & ((1LL<<31)-1)。由于 ,树天然满足拓扑序,这给后续自底向上计算带来了便利。
第二步:DSU on Tree(树上启发式合并)
朴素做法:对每个顶点都开一个"颜色 次数"的哈希表并向上合并,总复杂度近似 且常数巨大。官方做法利用 DSU on Tree:每个顶点复用其重儿子(子树最大的儿子)的表,只把轻儿子的子树重新插入。
为每个顶点维护一张"当前子树表",需要支持三种操作:
add(v):把顶点 的颜色信息插入表;query(v):回答"整棵子树 的众数",即 ;reset:清空表(用于丢弃轻儿子算完的结果)。
递归框架如下:
dfs(c):
先对每个轻儿子 d 执行 dfs(d),随后 reset() # 轻儿子的表用完即弃
若 c 非叶,dfs(重儿子) # 重儿子的表保留复用
把每个轻儿子子树中的所有顶点 add 进表
add(c)
query(c)复杂度分析:顶点 被 add 的次数 =(根到 路径上的轻边数)+ 1。轻边的定义满足"沿轻边上跳子树大小至少减半",所以每个顶点至多经过 条轻边,总 add 调用数为 ;query 恰好 次,reset 为 次。只要三种操作本身够快,总复杂度即为 。
第三步:用数组实现三种操作
本题颜色编号不超过 ,无需哈希表,直接开数组即可:
cnt[x]:当前表中颜色 出现的次数;num[t]:当前恰好出现 次的颜色种数;mx:当前最大出现次数。add(v):设 ,令num[cnt[x]]--; cnt[x]++; num[cnt[x]]++; mx = max(mx, cnt[x])。query(v):答案即(mx, num[mx])。reset(子树 d):把cnt中属于子树 的颜色全部清零,并把num[0..mx]清零、mx=0。不需要清空整个数组:只要按欧拉序把子树 的每个顶点对应颜色清零即可,因为 reset 发生时表里恰好就是子树 的内容。
正确性:DSU on Tree 维护的"当前表"在每次 dfs(c) 开始时是空的(轻儿子处理完即 reset,重儿子递归前父节点的表也是空的);处理完重儿子后表 = 重儿子的整棵子树,再把各轻儿子子树顶点与 本身加入,正好构成子树 的完整信息。因此每个 query 得到的众数都是对的。
第四步:实现细节(针对 大的工程问题)
- 重儿子前置:利用 的拓扑序,逆序一趟求出子树大小
sub,同时找出每个顶点的重儿子,并把重儿子交换到孩子列表最前面(CSR 存储)。 - 欧拉序:用显式栈做 DFS(避免递归爆栈),得到
down[v]、up[v],使子树 的顶点落在欧拉序区间 。这样"把轻儿子整棵子树加入"和"reset 子树"都变成对欧拉序区间的循环,。 - DSU 遍历也改写成显式栈:用事件栈模拟递归(事件 = 顶点 + 阶段,阶段 0/1/2 对应"处理轻儿子 / 处理重儿子 / 合并并回答";另设 reset 事件)。树高最大 ,显式栈可避免栈溢出。
- 答案累加:使用 64 位整数,边乘边取模,防止溢出。
参考代码
#include <bits/stdc++.h>
using namespace std;
static const long long MOD = 998244353;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M, F;
long long seed;
cin >> N >> seed >> M >> F;
vector<int> q(M + 1), d(M + 1);
for (int i = 2; i <= M; ++i) cin >> q[i];
for (int i = 1; i <= M; ++i) cin >> d[i];
// 重建 p[1..N], col[1..N]
vector<int> p(N + 1, 0), col(N + 1, 0);
long long state = seed;
for (int i = 2; i <= N; ++i) {
if (i <= M) {
p[i] = q[i];
} else {
p[i] = (int)(state % (i - 1)) + 1;
state = (state * 1103515245 + 12345) % 2147483648LL;
}
}
for (int i = 1; i <= N; ++i) {
if (i <= M) {
col[i] = d[i];
} else {
col[i] = (int)(state % F) + 1;
state = (state * 1103515245 + 12345) % 2147483648LL;
}
}
// 孩子列表 CSR:start[v] .. start[v+1]-1
vector<int> deg(N + 2, 0);
for (int i = 2; i <= N; ++i) deg[p[i]]++;
vector<int> start(N + 3, 0);
for (int i = 1; i <= N + 1; ++i) start[i + 1] = start[i] + deg[i];
vector<int> child(N); // 共 N-1 条孩子边
{
vector<int> cur(N + 3);
for (int i = 1; i <= N + 1; ++i) cur[i] = start[i];
for (int i = 2; i <= N; ++i) child[cur[p[i]]++] = i;
}
// 自底向上求子树大小(利用 p_i < i 的拓扑序)
vector<int> sub(N + 1, 1);
for (int i = N; i >= 2; --i) sub[p[i]] += sub[i];
// 重儿子交换到孩子列表最前
for (int v = 1; v <= N; ++v) {
int best = start[v];
for (int j = start[v] + 1; j < start[v + 1]; ++j) {
if (sub[child[j]] > sub[child[best]]) best = j;
}
if (best != start[v]) swap(child[start[v]], child[best]);
}
// 欧拉序(显式栈),子树区间 [down[v], up[v])
vector<int> down(N + 1), up(N + 1), euler(N);
int idx = 0;
vector<int> st;
st.reserve(2 * N + 4);
st.push_back(0); // 根结束哨兵
st.push_back(1);
while (!st.empty()) {
int c = st.back();
st.pop_back();
if (c > 0) { // 顶点
down[c] = idx;
euler[idx] = c;
++idx;
for (int j = start[c + 1] - 1; j >= start[c]; --j) {
int d = child[j];
st.push_back(~d); // 结束哨兵
st.push_back(d);
}
} else if (c == 0) {
up[1] = idx;
} else {
up[~c] = idx;
}
}
// DSU on Tree(事件栈模拟递归)
vector<int> cnt(N + 1, 0); // cnt[颜色]
vector<int> num(N + 2, 0); // num[次数]
num[0] = N;
int mx = 0;
long long ans = 0;
auto add = [&](int v) {
int x = col[v];
--num[cnt[x]];
++cnt[x];
++num[cnt[x]];
if (cnt[x] > mx) mx = cnt[x];
};
auto reset_subtree = [&](int d) {
for (int i = down[d]; i < up[d]; ++i) cnt[col[euler[i]]] = 0;
for (int i = 0; i <= mx; ++i) num[i] = 0;
mx = 0;
};
// 事件 = (顶点<<2) | 阶段;阶段 0/1/2 分别处理轻儿子/重儿子/合并回答,3 = reset
vector<int> events;
events.reserve(2 * (N + 5));
events.push_back((1 << 2) | 0);
while (!events.empty()) {
int e = events.back();
events.pop_back();
int v = e >> 2, type = e & 3;
if (type == 0) {
events.push_back((v << 2) | 1);
for (int j = start[v + 1] - 1; j >= start[v] + 1; --j) {
int d = child[j];
events.push_back((d << 2) | 3); // 处理完 d 后 reset
events.push_back((d << 2) | 0);
}
} else if (type == 1) {
events.push_back((v << 2) | 2);
if (start[v] < start[v + 1]) {
events.push_back((child[start[v]] << 2) | 0); // 重儿子
}
} else if (type == 2) {
for (int j = start[v] + 1; j < start[v + 1]; ++j) {
int d = child[j];
for (int i = down[d]; i < up[d]; ++i) add(euler[i]);
}
add(v);
ans = (ans + (long long)(mx ^ v) * (num[mx] ^ v)) % MOD;
} else {
reset_subtree(v);
}
}
cout << ans << "\n";
return 0;
}说明:官方题解另给出了一份 Python 实现,思路与本代码完全一致;但本题 高达 ,树深可能极大,Python 的递归 DFS 需要显式提高递归上限,实践中更推荐使用本代码这类迭代写法。官方题解还特别指出:若采用 unordered_map 实现合并,会因过大的常数因子(时间与空间)而难以通过,因此数组化的 DSU on Tree 才是本题的可行方案。
复杂度分析
- 时间复杂度:(每个顶点被
add的次数为 1 加上其轻祖先个数,后者为 ) - 空间复杂度:(若干长度为 的整数数组)