文件夹合并

用兄弟链表拼接被删除文件夹的子链表,并以 DFS 序差分维护访问路径上的存活文件夹数量。

OJ: shumeng

题目 ID: CSP202403E

难度:提高+/省选-

标签:链表DFS 序树状数组

日期: 2026-07-31 16:21

形式化题目

文件夹构成一棵根为 11 的树,每个文件夹有若干直接数据。两类操作:

  1. 合并文件夹 xx:删除 xx 当前的所有直接子文件夹,把这些子文件夹的子文件夹提升为 xx 的直接子文件夹,并把被删子文件夹的直接数据累加到 xx 上。输出合并后 xx 的直接子文件夹数和总数据量。
  2. 访问文件夹 xxxx 未被删除):输出从根到 xx 需要经过的存活文件夹个数(含根与 xx 自身,跳过已被删除的)。

思路

如果直接维护父子关系,合并时要把被删子文件夹的所有子文件夹重新挂到 xx 下,反复操作会退化。需要把“当前子节点集合”和“路径上哪些祖先已被删除”分开维护。

朴素做法:显式改父子关系

先看最直接的做法:每次合并都真的把子文件夹的子文件夹挂到被合并文件夹下,访问时沿父指针向上数存活节点。

cpp
/**
 * 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;
}

这个做法每次合并都要移动整棵子树,最坏 O(n)O(n) 一次,只适合小数据验证。

主解一:兄弟链表完成合并

给每个文件夹维护一条“当前直接子节点”的双向链表,记录首子 head、尾子 tail 以及每个节点的兄弟前驱 prev_sib 与后继 next_sib

合并 xx 时:

  1. 依次从 xx 链表头部取出一个子节点 yy
  2. folder_data[y] 累加到 folder_data[x],删除 yy
  3. yy 的整条子链表接到 xx 子链表的尾部。

链表拼接是 O(1)O(1) 的,不需要逐个移动被提升的节点。每个文件夹至多被删除一次,所以所有 pop 与拼接的总代价是 O(n)O(n)

主解二:DFS 序差分维护访问层数

删除节点不改变原树的祖先关系,只是让被删的祖先从访问路径中被跳过。于是访问 xx 的层数等于原树深度 + 1 - 路径上已删除祖先数

对原树做一遍 DFS,得到每个节点的子树区间 [tin,tout][tin, tout]。删除节点 yy 时,在树状数组上对区间 [tin[y],tout[y]][tin[y], tout[y]]+(1)+(-1)。这样 yy 的每个后代(在原树中)在查询时都会把这个已删除祖先计入。访问 xx 时对 tin[x] 做单点查询,就得到路径上删除祖先数量的相反数,答案即

depth[x]+1+query(tin[x])\text{depth}[x] + 1 + \text{query}(\text{tin}[x])。

代码

cpp
/**
 * 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;
}

复杂度

设节点数 nn、操作数 mm

  • 时间:DFS 与初始化 O(n)O(n);每个文件夹只被从链表中取出一次,合并操作总计 O(n)O(n) 次链表操作;每次合并或访问的树状数组操作为 O(logn)O(\log n),总时间复杂度 O((n+m)logn)O((n + m) \log n)
  • 空间:各数组均为 O(n)O(n),空间复杂度 O(n)O(n)

总结

动态树结构变化题,把问题拆成两半:用兄弟链表维护当前子节点集合,用原树 DFS 序 + 差分树状数组维护“哪些祖先已被删除”。前者让合并变成常数时间的链表拼接,后者把“沿路径数节点”变成一次单点查询,避免了逐个重挂子树的大代价。