树的DFS序

例题:P9305 「DTOI-5」校门外的枯树
定义 \(S_i\) 为按照 DFS 顺序访问节点时,到达节点 \(i\) 时累计的边权前缀和(注意这里的 DFS 顺序需要严格按照题目给定的“从左往右”遍历子节点的顺序),设 \(s_i\) 为从根节点到节点 \(i\) 的路径上的边权和。
当选择路径 \(u \to v\) 作为分割路径时:
- 左半部分的边权和 \(L\):由于 DFS 是先遍历左子树,再遍历右子树。对于路径上的任意节点,其左侧所有的分支(即已经遍历过的部分)的边权和实际上被包含在 \(S_v - S_u\) 中。但 \(S_v - S_u\) 同时也包含了路径 \(u \to v\) 本身的边权,因此,分割后的左半部分边权和 \(L = (S_v - S_u) - (s_v - s_u)\)。
- 右半部分的边权和 \(R\):右半部分包含了路径右侧的所有分支,这些分支将在访问完 \(v\) 之后被遍历,直到 \(u\) 的子树遍历结束。设 \(l_u\) 为 \(u\) 子树中 DFS 序最大的叶子节点(这可以预处理),那么 \(S_{l_u}\) 就代表了遍历完 \(u\) 的整个子树后的累计边权和。因此,右半部分边权和 \(R = S_{l_u} - S_v\)。
对于每个节点 \(u\),需要在其子树的所有叶子节点 \(v\) 中,找到一个 \(v\),使得 \(|L-R|\) 最小。
注意到随着 DFS 序的增加,选取的叶子节点 \(v\) 在子树中从左向右移动,\(L\) 会逐渐增加(左边的分支变多),\(R\) 会逐渐减少(右边的分支变少),这意味着 \(L-R\) 是关于叶子节点 DFS 序单调递增的。
因此,可以利用二分查找。
参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cmath>
using std::min;
using std::abs;
using std::vector;
const int N = 1e5 + 5;
const int INF = 1e9 + 5;
// 定义边的结构体,包含目标顶点 v 和权重 m
struct Edge {
int v, m;
};
// 邻接表存储图
vector<Edge> g[N];
// dfn_sum[v]: 按照DFS顺序遍历时,到达节点 v 时累计的边权总和
// sum[v]: 从根节点到节点 v 的路径边权和
// leaf[i]: 存储第 i 个叶子节点的编号
// leaf_cnt: 叶子节点的计数器
// bg[u]: 以 u 为根的子树中,包含的叶子节点在 leaf 数组中的起始下标
// ed[u]: 以 u 为根的子树中,包含的叶子节点在 leaf 数组中的结束下标
// tot: DFS 过程中的全局累加器,用于计算 dfn_sum
int dfn_sum[N], sum[N], leaf[N], leaf_cnt, bg[N], ed[N], tot;
// 深度优先搜索,用于预处理各项数据
void dfs(int u) {
for (Edge e : g[u]) {
int v = e.v, m = e.m;
// tot 是按照 DFS 访问顺序累加的边权和
// 这里的逻辑是:进入子树前加边权
tot += m;
dfn_sum[v] = tot;
// sum[v] 是从根到 v 的路径权值和
sum[v] = sum[u] + m;
dfs(v);
// 更新 u 子树的叶子区间
// 如果 u 还没有记录起始叶子(即访问第一个子节点时),则继承该子节点的起始叶子
if (bg[u] == 0) bg[u] = bg[v];
// 每次访问完一个子节点,都更新 u 的结束叶子为当前子节点的结束叶子
ed[u] = ed[v];
}
// 如果 u 是叶子节点(没有子节点)
if (g[u].size() == 0) {
leaf_cnt++;
leaf[leaf_cnt] = u;
// 叶子节点的叶子区间就是它自己
bg[u] = ed[u] = leaf_cnt;
}
}
// 初始化函数,清空上一组数据
void init(int n) {
for (int i = 1; i <= n; i++) {
g[i].clear();
bg[i] = 0;
}
tot = 0;
leaf_cnt = 0;
}
// 计算不平衡度的核心函数
// 参数 u: 当前子树的根节点
// 参数 v: u 子树中的某个叶子节点,代表选择的分割路径终点
// 返回值: 路径 (u -> v) 将 u 子树分割为左右两部分后的 (左权值和 - 右权值和)
int imbalance(int u, int v) {
int l = dfn_sum[v] - dfn_sum[u] - (sum[v] - sum[u]);
int r = dfn_sum[leaf[ed[u]]] - dfn_sum[v];
return l - r;
}
// 计算节点 u 的子树的最小不平衡度
// 利用二分查找在有序的叶子节点序列中寻找最优解
int calc(int u) {
int l = bg[u], r = ed[u];
// 叶子节点的 imbalance 值随 DFS 序单调递增(因为左边部分越来越多,右边越来越少)
// 要找 imbalance 最接近 0 的点
while (l <= r) {
int mid = (l + r) / 2;
// 如果 imbalance < 0,说明左边轻右边重,需要往右找(增加左边,减少右边)
if (imbalance(u, leaf[mid]) < 0) {
l = mid + 1;
} else {
// 否则往左找
r = mid - 1;
}
}
int res = INF;
// 检查二分结束位置附近的两个点,取绝对值最小的
if (l <= ed[u]) res = min(res, abs(imbalance(u, leaf[l])));
if (r >= bg[u]) res = min(res, abs(imbalance(u, leaf[r])));
return res;
}
void solve(int k) {
int n;
scanf("%d", &n);
init(n);
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
for (int j = 1; j <= x; j++) {
int v, m;
scanf("%d%d", &v, &m);
g[i].push_back({v, m});
}
}
// 根节点从 1 开始 DFS
dfs(1);
if (k == 1) {
// k=1 时只计算整棵树的不平衡度
printf("%d\n", calc(1));
} else {
// k=2 时计算每个节点子树的不平衡度
for (int i = 1; i <= n; i++)
printf("%d ", calc(i));
printf("\n");
}
}
int main() {
int t, k;
scanf("%d%d", &t, &k);
for (int i = 1; i <= t; i++) {
solve(k);
}
return 0;
}
例题:P3459 [POI2007] MEG-Megalopolis
给定一棵 \(n\) 个节点的树,根节点为 \(1\),开始每条边边权为 \(1\)。有 \(m+n-1\) 次操作,每次修改操作使得某条边边权为 \(0\),每次查询操作询问 \(1\) 到某个点的边权和。
数据范围:\(n \le 250000\)。
如果从 DFS 序列的角度考虑,将每个节点在 DFS 中的第一个出现位置看作 +1,第二个位置看作 -1,则每次查询相当于查询序列的前缀和,而修改操作相当于对该条边的子节点在 DFS 序列中两次出现的位置做单点更新。
#include <cstdio>
#include <vector>
using std::vector;
const int N = 250005;
vector<int> tree[N];
int n, in[N], out[N], idx, bit[N * 2];
char op[5];
int lowbit(int x) {
return x & -x;
}
void update(int x, int d) {
while (x <= 2 * n) {
bit[x] += d; x += lowbit(x);
}
}
int query(int x) {
int res = 0;
while (x > 0) {
res += bit[x];
x -= lowbit(x);
}
return res;
}
void dfs(int u, int fa) {
idx++; in[u] = idx;
update(idx, 1);
for (int v : tree[u]) {
if (v == fa) continue;
dfs(v, u);
}
idx++; out[u] = idx;
update(idx, -1);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int a, b; scanf("%d%d", &a, &b);
tree[a].push_back(b); tree[b].push_back(a);
}
dfs(1, 0);
int m; scanf("%d", &m);
for (int i = 1; i <= n + m - 1; i++) {
scanf("%s", op);
if (op[0] == 'A') {
int x, y; scanf("%d%d", &x, &y);
int z = in[x] < in[y] ? y : x;
update(in[z], -1); update(out[z], 1);
} else {
int x; scanf("%d", &x);
printf("%d\n", query(in[x]) - 1);
}
}
return 0;
}

浙公网安备 33010602011771号