树的遍历
例题
例题 - Subtree Queries CSES - 普及/提高- | |
例题 - 请努力完成本题,再阅读以下内容。 | 解答 |
题目简单翻译:
n个节点的s树,节点1为根,每个节点有一个值。 你的任务是处理以下的查询:
- 将节点的值改为
- 计算节点的子树中所有值的和
输入:
第一行是, (查询数),第二行是,每个节点的值,之后行是树的边。
最后行是查询,或者是" ",或者是" "。
输出:打印类型2的查询的答案。
树的遍历数组 (traversal array)
树上的不少问题,都可以通过遍历数组(tree traversal array)的方式,转化为数组上的区间问题来处理。
最常用的遍历数组是DFN遍历数组,就是用DFS遍历时首次到达的节点顺序组成的数组。在DFN序中,每个节点出现一次。如下图所示:
容易看到,在DFN遍历数组下,每棵子树在数组中是连续的区间。所以DFN遍历数组经常可以用来解子树相关的问题。DFN遍历数组长度为N(节点数)。
题解 - Subtree Queries
利用DFN遍历数组,问题就转化为单点更改数组中的值(查询1),以及区间求和(查询2),是线段树或者树状数组的基础应用。
下面是用线段树的解法:
Subtree Queries题解代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, Q;
int v[200005];
vector<int> adj[200005];
int st[200005], en[200005]; // start and end of each subtree
ll tree[400005];
void set(int k, ll x) {
k += n;
tree[k] = x;
for (k /= 2; k >= 1; k /= 2) {
tree[k] = tree[2*k]+tree[2*k+1];
}
}
ll sum(int a, int b) {
a += n; b += n;
ll s = 0;
while (a <= b) {
if (a%2 == 1) s += tree[a++];
if (b%2 == 0) s += tree[b--];
a /= 2; b /= 2;
}
return s;
}
int ti;
void dfs(int s, int e) {
st[s] = ti++;
for (int u: adj[s]) {
if (u == e) continue;
dfs(u, s);
}
en[s] = ti-1;
}
int main() {
scanf("%d %d", &n, &Q);
for (int i = 0; i < n; i++)
scanf("%d", &v[i]);
for (int i = 0; i < n-1; i++) {
int u,v;
scanf("%d %d", &u, &v);
u--, v--;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(0,-1);
for (int i = 0; i < n; i++)
set(st[i], v[i]);
for (int i = 0; i < Q; i++) {
int c,a,b;
// queries
scanf("%d %d", &c, &a);
a--;
if (c == 1) {
scanf("%d", &b);
set(st[a], b);
} else {
printf("%lld\n", sum(st[a], en[a]));
}
}
return 0;
}
欧拉回路
另外一种遍历数组是欧拉回路(Euler tour)遍历数组,在DFS的每一步,包括初次访问,去下一节点前,以及再次回到此节点时,都记录一次访问。如下图所示。
欧拉回路遍历数组长度为2N-1.
欧拉回路可以用来求LCA,如上图,两个节点间的区间中,深度最小的点,就是这两个节点的LCA。
相关章节
树上DP与树上遍历密切相关,可以对照来练习。
习题
习题 | ||||||
---|---|---|---|---|---|---|
P5658 | CSP-S | 括号树 | 2019 | 普及+/提高 | ||
T241758 | 小图灵 | 树上两点间距离之和 | 普及+/提高 | |||
P6098 | USACO Gold | Cow Land | 2019 | 提高+/省选- | 解答 | |
P3605 | USACO Plat | Promotion Counting | 2017 | 提高+/省选- | 解答 |