[NOI2015] 软件包管理器

GitHub跳转原题关系图返回列表

把安装与卸载操作转成根到点路径设为 1、子树设为 0,再用树链剖分配合线段树维护区间赋值和区间和。

OJ: luogu

题目 ID: P2146

难度:提高+/省选-

标签:树链剖分线段树dfs序建模

日期: 2026-06-21 03:03

题意

依赖关系构成一棵以 0 为根的树。

  • install x:需要把根到 x 的依赖链都安装上
  • uninstall x:需要把 x 以及所有依赖它的后代一起卸载

每次操作输出实际改变了多少个软件包的安装状态。

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100000 + 5;

int n;
int parent_arr[MAXN];
vector<int> children[MAXN];
int installed[MAXN];

int install_package(int x) {
    int changed = 0;
    while (x != -1) {
        if (!installed[x]) {
            installed[x] = 1;
            changed++;
        }
        x = parent_arr[x];
    }
    return changed;
}

int uninstall_package(int x) {
    int changed = 0;
    stack<int> st;
    st.push(x);

    while (!st.empty()) {
        int u = st.top();
        st.pop();

        if (installed[u]) {
            installed[u] = 0;
            changed++;
        }

        for (int v : children[u]) {
            st.push(v);
        }
    }

    return changed;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // 这是一个朴素模拟:
    // install 就沿父亲链往上装,uninstall 就把整棵子树暴力扫掉。
    cin >> n;
    parent_arr[0] = -1;
    for (int i = 1; i < n; i++) {
        cin >> parent_arr[i];
        children[parent_arr[i]].push_back(i);
    }

    int q;
    cin >> q;
    while (q--) {
        string op;
        int x;
        cin >> op >> x;
        if (op[0] == 'i') {
            cout << install_package(x) << '\n';
        } else {
            cout << uninstall_package(x) << '\n';
        }
    }

    return 0;
}

brute.cpp 直接模拟:

  • 安装就一路跳父亲到根
  • 卸载就暴力遍历整棵子树

这个思路完全正确,但最坏会退化成每次 O(n)O(n)

这题最关键的转化是:

  • install x 本质是把根到 x 的路径整体设为 1
  • uninstall x 本质是把 x 的子树整体设为 0

于是就变成了一个很标准的树链剖分问题。

树链剖分后:

  • 一条路径会拆成若干个 DFS 序连续区间
  • 一棵子树天然对应一个 DFS 序连续区间

再在线段树中维护每个点当前是否已安装,只需要支持:

  • 区间赋值为 0/1
  • 区间求和

为什么区间和就够?

  • 因为安装状态只有 0/1
  • 区间和就是当前已经安装了多少个软件包

所以:

  • install x 时,先查询路径和,路径长度减去它,就是新装上的软件包个数
  • uninstall x 时,先查询子树和,这就是本次会被卸载的个数

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100000 + 5;

int n;
int head[MAXN], to[MAXN], nxt[MAXN], edge_cnt;
int parent_arr[MAXN], depth_arr[MAXN];
int sub_size[MAXN], heavy_son[MAXN];
int top_arr[MAXN], dfn[MAXN], rev_dfn[MAXN], dfs_clock;
int seg_sum[MAXN << 2];
int lazy_tag[MAXN << 2];

void add_edge(int u, int v) {
    edge_cnt++;
    to[edge_cnt] = v;
    nxt[edge_cnt] = head[u];
    head[u] = edge_cnt;
}

void build_tree_info() {
    static int order[MAXN];
    static int stk[MAXN];

    int ord_cnt = 0;
    int top = 0;
    stk[++top] = 0;
    parent_arr[0] = -1;
    depth_arr[0] = 1;

    while (top > 0) {
        int u = stk[top--];
        order[++ord_cnt] = u;
        for (int i = head[u]; i != 0; i = nxt[i]) {
            int v = to[i];
            parent_arr[v] = u;
            depth_arr[v] = depth_arr[u] + 1;
            stk[++top] = v;
        }
    }

    for (int idx = ord_cnt; idx >= 1; idx--) {
        int u = order[idx];
        sub_size[u] = 1;
        heavy_son[u] = -1;
        int best_size = 0;

        for (int i = head[u]; i != 0; i = nxt[i]) {
            int v = to[i];
            sub_size[u] += sub_size[v];
            if (sub_size[v] > best_size) {
                best_size = sub_size[v];
                heavy_son[u] = v;
            }
        }
    }
}

void build_dfn() {
    static int stk_u[MAXN];
    static int stk_top[MAXN];

    int top = 0;
    stk_u[++top] = 0;
    stk_top[top] = 0;

    while (top > 0) {
        int u = stk_u[top];
        int chain_top = stk_top[top];
        top--;

        while (u != -1) {
            top_arr[u] = chain_top;
            dfn[u] = ++dfs_clock;
            rev_dfn[dfs_clock] = u;

            for (int i = head[u]; i != 0; i = nxt[i]) {
                int v = to[i];
                if (v == heavy_son[u]) {
                    continue;
                }
                stk_u[++top] = v;
                stk_top[top] = v;
            }

            u = heavy_son[u];
        }
    }
}

void build_seg(int u, int l, int r) {
    seg_sum[u] = 0;
    lazy_tag[u] = -1;
    if (l == r) {
        return;
    }

    int mid = (l + r) >> 1;
    build_seg(u << 1, l, mid);
    build_seg(u << 1 | 1, mid + 1, r);
}

void apply_set(int u, int l, int r, int val) {
    seg_sum[u] = (r - l + 1) * val;
    lazy_tag[u] = val;
}

void push_down(int u, int l, int r) {
    if (lazy_tag[u] == -1 || l == r) {
        return;
    }

    int mid = (l + r) >> 1;
    apply_set(u << 1, l, mid, lazy_tag[u]);
    apply_set(u << 1 | 1, mid + 1, r, lazy_tag[u]);
    lazy_tag[u] = -1;
}

void push_up(int u) {
    seg_sum[u] = seg_sum[u << 1] + seg_sum[u << 1 | 1];
}

void range_set(int u, int l, int r, int ql, int qr, int val) {
    if (ql <= l && r <= qr) {
        apply_set(u, l, r, val);
        return;
    }

    push_down(u, l, r);
    int mid = (l + r) >> 1;
    if (ql <= mid) {
        range_set(u << 1, l, mid, ql, qr, val);
    }
    if (qr > mid) {
        range_set(u << 1 | 1, mid + 1, r, ql, qr, val);
    }
    push_up(u);
}

int query_sum(int u, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) {
        return seg_sum[u];
    }

    push_down(u, l, r);
    int mid = (l + r) >> 1;
    int ans = 0;
    if (ql <= mid) {
        ans += query_sum(u << 1, l, mid, ql, qr);
    }
    if (qr > mid) {
        ans += query_sum(u << 1 | 1, mid + 1, r, ql, qr);
    }
    return ans;
}

int query_path_sum(int u, int v) {
    int ans = 0;
    while (top_arr[u] != top_arr[v]) {
        if (depth_arr[top_arr[u]] < depth_arr[top_arr[v]]) {
            swap(u, v);
        }
        ans += query_sum(1, 1, n, dfn[top_arr[u]], dfn[u]);
        u = parent_arr[top_arr[u]];
    }

    if (depth_arr[u] > depth_arr[v]) {
        swap(u, v);
    }
    ans += query_sum(1, 1, n, dfn[u], dfn[v]);
    return ans;
}

void set_path(int u, int v, int val) {
    while (top_arr[u] != top_arr[v]) {
        if (depth_arr[top_arr[u]] < depth_arr[top_arr[v]]) {
            swap(u, v);
        }
        range_set(1, 1, n, dfn[top_arr[u]], dfn[u], val);
        u = parent_arr[top_arr[u]];
    }

    if (depth_arr[u] > depth_arr[v]) {
        swap(u, v);
    }
    range_set(1, 1, n, dfn[u], dfn[v], val);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i < n; i++) {
        int p;
        cin >> p;
        add_edge(p, i);
    }

    build_tree_info();
    build_dfn();
    build_seg(1, 1, n);

    int q;
    cin >> q;
    while (q--) {
        string op;
        int x;
        cin >> op >> x;

        if (op[0] == 'i') {
            // 安装 x,相当于把根到 x 的整条依赖链全部设为 1。
            int installed_before = query_path_sum(0, x);
            int changed = depth_arr[x] - installed_before;
            set_path(0, x, 1);
            cout << changed << '\n';
        } else {
            // 卸载 x,相当于把 x 的整棵子树全部设为 0。
            int l = dfn[x];
            int r = dfn[x] + sub_size[x] - 1;
            int installed_before = query_sum(1, 1, n, l, r);
            range_set(1, 1, n, l, r, 0);
            cout << installed_before << '\n';
        }
    }

    return 0;
}

复杂度

树链剖分预处理是 O(n)O(n)

每次 install 需要 O(log2n)O(log^2 n),每次 uninstall 需要 O(logn)O(log n)

总复杂度可以写成 O(n+qlog2n)O(n + q log^2 n),空间复杂度是 O(n)O(n)

总结

这题表面是软件包依赖,实质是树上状态维护。

真正要抓住的是两句话:

  • 安装是“根到点路径设为 1”
  • 卸载是“子树设为 0”

一旦做完这个翻译,后面就是树链剖分加线段树的标准套路。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析