把安装与卸载操作转成根到点路径设为 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 直接模拟:
- 安装就一路跳父亲到根
- 卸载就暴力遍历整棵子树
这个思路完全正确,但最坏会退化成每次
这题最关键的转化是:
install x本质是把根到x的路径整体设为1uninstall 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;
}复杂度
树链剖分预处理是
每次 install 需要 uninstall 需要
总复杂度可以写成
总结
这题表面是软件包依赖,实质是树上状态维护。
真正要抓住的是两句话:
- 安装是“根到点路径设为 1”
- 卸载是“子树设为 0”
一旦做完这个翻译,后面就是树链剖分加线段树的标准套路。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

