用兄弟链表拼接被删除文件夹的子链表,并以 DFS 序差分维护访问路径上的存活文件夹数量。
OJ: shumeng
题目 ID: CSP202403E
难度:提高+/省选-
标签:树链表DFS 序树状数组
日期: 2026-07-31 16:21
形式化题目
文件夹构成一棵根为
- 合并文件夹
:删除 当前的所有直接子文件夹,把这些子文件夹的子文件夹提升为 的直接子文件夹,并把被删子文件夹的直接数据累加到 上。输出合并后 的直接子文件夹数和总数据量。 - 访问文件夹
( 未被删除):输出从根到 需要经过的存活文件夹个数(含根与 自身,跳过已被删除的)。
思路
如果直接维护父子关系,合并时要把被删子文件夹的所有子文件夹重新挂到
朴素做法:显式改父子关系
先看最直接的做法:每次合并都真的把子文件夹的子文件夹挂到被合并文件夹下,访问时沿父指针向上数存活节点。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:21
* update_at: 2026-08-17 22:39
*/
// brute.cpp:小数据暴力解,显式修改父子关系并沿父指针计算深度,只适合小规模对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
int n, operation_count;
int parent[MAXN]; // 当前的父文件夹
vector<int> child[MAXN]; // 当前每个文件夹的直接子文件夹
long long folder_data[MAXN]; // 当前文件夹直接拥有的数据量
bool alive[MAXN]; // 文件夹是否仍然存在(未被合并删除)
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> operation_count;
for (int i = 2; i <= n; i++) {
cin >> parent[i];
child[parent[i]].push_back(i);
}
for (int i = 1; i <= n; i++) cin >> folder_data[i];
for (int i = 1; i <= n; i++) alive[i] = true;
while (operation_count--) {
int type, node;
cin >> type >> node;
if (type == 1) {
// 合并:删除 node 的所有直接子文件夹,把它们的子文件夹提升上来
vector<int> old_child = child[node];
child[node].clear();
for (int i = 0; i < (int)old_child.size(); i++) {
int removed = old_child[i];
folder_data[node] += folder_data[removed];
alive[removed] = false;
// 被删子文件夹的子文件夹改为 node 的直接子文件夹
for (int j = 0; j < (int)child[removed].size(); j++) {
int promoted = child[removed][j];
parent[promoted] = node;
child[node].push_back(promoted);
}
child[removed].clear();
}
cout << child[node].size() << ' ' << folder_data[node] << '\n';
} else {
// 访问:沿父指针向上走,统计路径上存活的文件夹数量
int count = 1;
int current = node;
while (parent[current] != 0) {
current = parent[current];
if (alive[current]) count++;
}
cout << count << '\n';
}
}
return 0;
}这个做法每次合并都要移动整棵子树,最坏
主解一:兄弟链表完成合并
给每个文件夹维护一条“当前直接子节点”的双向链表,记录首子 head、尾子 tail 以及每个节点的兄弟前驱 prev_sib 与后继 next_sib。
合并
- 依次从
链表头部取出一个子节点 ; - 把
folder_data[y]累加到folder_data[x],删除; - 把
的整条子链表接到 子链表的尾部。
链表拼接是 pop 与拼接的总代价是
主解二:DFS 序差分维护访问层数
删除节点不改变原树的祖先关系,只是让被删的祖先从访问路径中被跳过。于是访问
对原树做一遍 DFS,得到每个节点的子树区间 tin[x] 做单点查询,就得到路径上删除祖先数量的相反数,答案即
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:21
* update_at: 2026-08-17 22:39
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
int n, operation_count;
vector<int> original_child[MAXN]; // 原树的子节点,用来计算 DFS 序
int parent[MAXN], depth[MAXN], tin[MAXN], tout[MAXN]; // 原树的父、深度与 DFS 子树区间
// 当前树的兄弟链表:head/tail 是首子/尾子,prev_sib/next_sib 是兄弟前驱/后继
int head[MAXN], tail[MAXN], prev_sib[MAXN], next_sib[MAXN];
int child_count[MAXN]; // 当前文件夹的直接子节点数量
long long folder_data[MAXN]; // 当前文件夹直接拥有的数据量
int fenwick[MAXN]; // 树状数组,维护 DFS 序上的区间差分
int timer_count; // DFS 计时器
// 树状数组:单点加
void bit_add(int pos, int value) {
for (int i = pos; i <= n; i += i & -i) fenwick[i] += value;
}
// 树状数组:前缀和
int bit_query(int pos) {
int result = 0;
for (int i = pos; i > 0; i -= i & -i) result += fenwick[i];
return result;
}
// 树状数组差分:区间 [left, right] 加 value
void range_add(int left, int right, int value) {
bit_add(left, value);
bit_add(right + 1, -value);
}
// 迭代 DFS 计算每个节点的 DFS 子树区间,避免原树是长链时递归栈溢出
void dfs(int root) {
vector<int> next_child(n + 1, 0); // 记录每个节点已经处理到第几个子节点
vector<int> stack;
stack.push_back(root);
tin[root] = ++timer_count;
while (!stack.empty()) {
int current = stack.back();
if (next_child[current] < (int)original_child[current].size()) {
int child = original_child[current][next_child[current]];
next_child[current]++;
depth[child] = depth[current] + 1;
tin[child] = ++timer_count;
stack.push_back(child);
} else {
tout[current] = timer_count;
stack.pop_back();
}
}
}
// 从节点 node 的链表头部取走一个子节点并返回
int pop_child(int node) {
int child = head[node];
head[node] = next_sib[child];
if (head[node] == 0) {
tail[node] = 0;
} else {
prev_sib[head[node]] = 0;
}
next_sib[child] = 0;
prev_sib[child] = 0;
child_count[node]--;
return child;
}
// 把 source 的整条子链表接到 target 的子链表尾部,并清空 source
void append_children(int target, int source) {
if (head[source] == 0) return;
if (tail[target] == 0) {
head[target] = head[source];
tail[target] = tail[source];
prev_sib[head[target]] = 0;
} else {
next_sib[tail[target]] = head[source];
prev_sib[head[source]] = tail[target];
tail[target] = tail[source];
}
child_count[target] += child_count[source];
head[source] = 0;
tail[source] = 0;
child_count[source] = 0;
}
// 合并文件夹 node:删除其当前所有直接子节点,数据并入 node,子链表上提
void merge_folder(int node) {
int original_count = child_count[node];
for (int i = 0; i < original_count; i++) {
int child = pop_child(node);
folder_data[node] += folder_data[child];
append_children(node, child);
// 删除的 child 成为路径上被跳过的祖先,对它的原树后代区间加 -1
range_add(tin[child], tout[child], -1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> operation_count;
for (int i = 2; i <= n; i++) {
cin >> parent[i];
original_child[parent[i]].push_back(i);
}
for (int i = 1; i <= n; i++) cin >> folder_data[i];
dfs(1);
// 初始时按原父子关系建立兄弟链表
for (int node = 2; node <= n; node++) {
int p = parent[node];
if (head[p] == 0) {
head[p] = node;
} else {
prev_sib[node] = tail[p];
next_sib[tail[p]] = node;
}
tail[p] = node;
child_count[p]++;
}
while (operation_count--) {
int type, node;
cin >> type >> node;
if (type == 1) {
merge_folder(node);
cout << child_count[node] << ' ' << folder_data[node] << '\n';
} else {
// 当前层数 = 原树深度 + 1,减去路径上已删除的祖先数量
cout << depth[node] + 1 + bit_query(tin[node]) << '\n';
}
}
return 0;
}复杂度
设节点数
- 时间:DFS 与初始化
;每个文件夹只被从链表中取出一次,合并操作总计 次链表操作;每次合并或访问的树状数组操作为 ,总时间复杂度 。 - 空间:各数组均为
,空间复杂度 。
总结
动态树结构变化题,把问题拆成两半:用兄弟链表维护当前子节点集合,用原树 DFS 序 + 差分树状数组维护“哪些祖先已被删除”。前者让合并变成常数时间的链表拼接,后者把“沿路径数节点”变成一次单点查询,避免了逐个重挂子树的大代价。