树的DFS序

image

例题:P9305 「DTOI-5」校门外的枯树

定义 \(S_i\) 为按照 DFS 顺序访问节点时,到达节点 \(i\) 时累计的边权前缀和(注意这里的 DFS 顺序需要严格按照题目给定的“从左往右”遍历子节点的顺序),设 \(s_i\) 为从根节点到节点 \(i\) 的路径上的边权和。

当选择路径 \(u \to v\) 作为分割路径时:

  1. 左半部分的边权和 \(L\):由于 DFS 是先遍历左子树,再遍历右子树。对于路径上的任意节点,其左侧所有的分支(即已经遍历过的部分)的边权和实际上被包含在 \(S_v - S_u\) 中。但 \(S_v - S_u\) 同时也包含了路径 \(u \to v\) 本身的边权,因此,分割后的左半部分边权和 \(L = (S_v - S_u) - (s_v - s_u)\)
  2. 右半部分的边权和 \(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;
}
posted @ 2024-06-09 21:44  RonChen  阅读(65)  评论(0)    收藏  举报