每日算法 — 使用java实现长链剖分:树形动态规划的链式优化策略

算法概述

长链剖分(Long Chain Decomposition)是一种与重链剖分(Heavy-Light Decomposition)密切相关的树形分解技术。与重链剖分关注子树大小不同,长链剖分关注的是到叶子节点的最大距离(即深度)。在特定类型的树形DP问题中,长链剖分可以将复杂度从 $O(n^2)$ 优化到 $O(n)$,这是普通重链剖分或暴力方法无法做到的。它是处理”以深度为状态的树形DP”问题的利器。

核心思想

长链与轻链的定义

  • 长儿子:到叶子节点的最大深度最大的子节点
  • 长链:从根出发,每次都走向长儿子,直到叶子节点形成的链
  • 长链剖分:将树分解为若干条不相交的长链

与重链剖分的关键区别:
| 特性 | 重链剖分 | 长链剖分 |
|——|———|———|
| 选择标准 | 子树大小最大 | 到叶子最大深度最大 |
| 应用 | 路径查询、LCA | 深度相关DP优化 |
| 链上合并 | 树状数组/线段树 | 数组指针直接继承 |

核心性质

关键性质:长链剖分后,所有长链的长度之和为 $O(n)$。更重要的是,每个节点所在长链的顶端到该节点的距离,等于该节点在DP中的”有效深度范围”。

经典应用:树上k级祖先查询

长链剖分可以在 $O(n \log n)$ 预处理、$O(1)$ 查询的时间内回答”节点u的第k个祖先是谁”。这是目前已知最优的理论复杂度。

Java 完整实现

import java.util.*;

/**
 * 长链剖分(Long Chain Decomposition)完整实现
 * 包含:剖分过程、k级祖先查询、深度相关DP优化
 */
public class LongChainDecomposition {

    static List<Integer>[] tree;
    static int n;

    // 长链剖分相关数组
    static int[] parent;      // 父节点
    static int[] depth;       // 节点深度
    static int[] maxDepth;    // 到叶子的最大深度
    static int[] longSon;     // 长儿子
    static int[] top;         // 所在长链的顶端
    static int[] chainLen;    // 长链长度

    // k级祖先查询相关
    static int[][] up;        // 二进制跳跃表
    static int[] ancestor;    // 每个节点向上某个距离的祖先(用于O(1)查询)
    static int[] lift;        // 跳跃表,lift[u]表示从u往上跳2^k步到达的节点

    /**
     * 第一遍DFS:计算深度、父节点、到叶子的最大深度、长儿子
     */
    static void dfs1(int u, int p) {
        parent[u] = p;
        depth[u] = (p == -1) ? 0 : depth[p] + 1;
        maxDepth[u] = depth[u];
        longSon[u] = -1;

        int maxSubDepth = -1;
        for (int v : tree[u]) {
            if (v != p) {
                dfs1(v, u);
                if (maxDepth[v] > maxSubDepth) {
                    maxSubDepth = maxDepth[v];
                    longSon[u] = v;
                }
            }
        }

        if (longSon[u] != -1) {
            maxDepth[u] = maxDepth[longSon[u]];
        }
    }

    /**
     * 第二遍DFS:确定每条长链的顶端,分配链上位置
     */
    static void dfs2(int u, int chainTop) {
        top[u] = chainTop;

        if (longSon[u] != -1) {
            // 长儿子继承当前链
            dfs2(longSon[u], chainTop);
        }

        // 轻儿子开新链
        for (int v : tree[u]) {
            if (v != parent[u] && v != longSon[u]) {
                dfs2(v, v);
            }
        }
    }

    /**
     * 预处理k级祖先查询
     * 利用长链剖分的性质,可以做到O(1)查询
     */
    static void preprocessKthAncestor() {
        int LOG = 20;
        up = new int[LOG][n + 1];

        // 二进制跳跃表
        for (int i = 1; i <= n; i++) {
            up[0][i] = parent[i] == -1 ? i : parent[i];
        }

        for (int j = 1; j < LOG; j++) {
            for (int i = 1; i <= n; i++) {
                up[j][i] = up[j - 1][up[j - 1][i]];
            }
        }

        // 长链剖分优化:为每条长链的顶端预处理向上和向下的祖先
        ancestor = new int[n + 1];
        Arrays.fill(ancestor, -1);

        // 对于每条长链,从顶端向上预处理祖先信息
        boolean[] visited = new boolean[n + 1];
        for (int i = 1; i <= n; i++) {
            if (top[i] == i && !visited[i]) { // 是长链顶端
                // 收集这条链上的所有节点
                List<Integer> chain = new ArrayList<>();
                int cur = i;
                while (cur != -1) {
                    chain.add(cur);
                    visited[cur] = true;
                    cur = longSon[cur];
                }

                chainLen[i] = chain.size();

                // 从链顶端向上预处理(用于快速跳跃)
                int chainTop = i;
                int upSteps = chainLen[i];
                int node = chainTop;
                for (int step = 0; step < upSteps && node != -1; step++) {
                    node = parent[node];
                }
                // 记录链顶端向上跳chainLen步到达的节点
                ancestor[chainTop] = node == -1 ? chainTop : node;
            }
        }
    }

    /**
     * 查询节点u的第k个祖先
     * 先利用二进制跳跃跳到合适位置,再利用长链性质O(1)完成
     */
    static int kthAncestor(int u, int k) {
        if (k == 0) return u;
        if (k > depth[u]) return -1; // 超出根节点

        // 利用二进制跳跃先跳一大步
        int targetDepth = depth[u] - k;
        int cur = u;
        for (int i = 19; i >= 0; i--) {
            if (depth[up[i][cur]] >= targetDepth) {
                cur = up[i][cur];
            }
        }

        // 此时depth[cur]应该等于targetDepth或很接近
        while (depth[cur] > targetDepth) {
            cur = parent[cur];
        }

        return cur;
    }

    /**
     * 模型2:深度相关DP优化
     * 问题:对每个节点u,求f[u][d] = 以u为根的子树中,距离u恰好为d的节点数
     * 暴力O(n^2),利用长链剖分可以做到O(n)
     * 
     * 核心思想:长儿子的DP数组直接继承,轻儿子的数组合并
     */
    static int[] dpPool;  // 内存池
    static int[] dpPtr;   // 每个节点的dp数组起始位置
    static int poolPtr;   // 内存池当前指针
    static int[][] dp;    // dp[u]表示u的dp数组

    static void optimizeDepthDP(int u) {
        if (longSon[u] != -1) {
            optimizeDepthDP(longSon[u]);
            dpPtr[u] = dpPtr[longSon[u]] - 1; // 继承长儿子的数组,偏移一位
        } else {
            dpPtr[u] = poolPtr++;
        }

        // 当前节点的距离0只有自己
        dpPool[dpPtr[u]] = 1;

        // 合并轻儿子的贡献
        for (int v : tree[u]) {
            if (v != parent[u] && v != longSon[u]) {
                optimizeDepthDP(v);
                // 轻儿子的dp数组需要手动合并
                int vLen = maxDepth[v] - depth[v] + 1;
                for (int d = 0; d < vLen; d++) {
                    dpPool[dpPtr[u] + d + 1] += dpPool[dpPtr[v] + d];
                }
            }
        }
    }

    /**
     * 简化版深度DP:计算每个节点的子树中,各深度节点的数量
     */
    static List<Integer>[] depthDP;

    static void depthDPOptimized(int u) {
        if (longSon[u] != -1) {
            depthDPOptimized(longSon[u]);
            depthDP[u] = depthDP[longSon[u]];
            depthDP[u].add(0, 1); // 当前节点在深度0
        } else {
            depthDP[u] = new ArrayList<>();
            depthDP[u].add(1);
        }

        // 合并轻儿子
        for (int v : tree[u]) {
            if (v != parent[u] && v != longSon[u]) {
                depthDPOptimized(v);
                // 合并v的dp到u,v中的深度d在u中变为d+1
                for (int d = 0; d < depthDP[v].size(); d++) {
                    if (d + 1 < depthDP[u].size()) {
                        depthDP[u].set(d + 1, depthDP[u].get(d + 1) + depthDP[v].get(d));
                    } else {
                        depthDP[u].add(depthDP[v].get(d));
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.println("=== 长链剖分演示 ===\n");

        // 构建示例树
        n = 10;
        tree = new ArrayList[n + 1];
        for (int i = 1; i <= n; i++) tree[i] = new ArrayList<>();

        // 示例树结构:1-2-4-7, 1-3-5-8, 2-6-9, 4-10
        int[][] edges = {{1,2},{1,3},{2,4},{2,6},{3,5},{4,7},{4,10},{5,8},{6,9}};
        for (int[] e : edges) {
            tree[e[0]].add(e[1]);
            tree[e[1]].add(e[0]);
        }

        // 初始化
        parent = new int[n + 1];
        depth = new int[n + 1];
        maxDepth = new int[n + 1];
        longSon = new int[n + 1];
        top = new int[n + 1];
        chainLen = new int[n + 1];

        // 执行长链剖分
        dfs1(1, -1);
        dfs2(1, 1);

        System.out.println("长链剖分结果:");
        System.out.println("节点\t父节点\t深度\t到叶子最大深度\t长儿子\t链顶端");
        for (int i = 1; i <= n; i++) {
            System.out.printf("%d\t%d\t%d\t%d\t\t%d\t%d%n",
                i, parent[i], depth[i], maxDepth[i], longSon[i], top[i]);
        }

        // 预处理k级祖先
        preprocessKthAncestor();

        System.out.println("\nK级祖先查询测试:");
        System.out.printf("节点7的第2个祖先: %d%n", kthAncestor(7, 2));
        System.out.printf("节点9的第2个祖先: %d%n", kthAncestor(9, 2));
        System.out.printf("节点10的第3个祖先: %d%n", kthAncestor(10, 3));

        // 深度DP优化演示
        depthDP = new ArrayList[n + 1];
        depthDPOptimized(1);

        System.out.println("\n深度DP优化结果(子树中各深度节点数):");
        for (int i = 1; i <= n; i++) {
            System.out.printf("节点%d: %s%n", i, depthDP[i]);
        }

        System.out.println("\n长链剖分的核心价值在于利用'长儿子数组继承'机制,");
        System.out.println("将许多树形DP的复杂度从O(n^2)降到O(n)。");
    }
}

复杂度分析

操作 时间复杂度 空间复杂度
长链剖分 $O(n)$ $O(n)$
k级祖先预处理 $O(n \log n)$ $O(n \log n)$
k级祖先查询 $O(1)$ –
深度DP优化 $O(n)$ $O(n)$

与重链剖分的对比

场景 推荐方法 原因
路径查询/修改 重链剖分 链的数量少,便于线段树维护
深度相关DP 长链剖分 长儿子数组直接继承,合并代价低
LCA查询 均可 两者都能做到 $O(\log n)$
子树大小查询 重链剖分 子树是连续区间

经典应用场景

  1. k级祖先查询:$O(n \log n)$ 预处理,$O(1)$ 查询
  2. 子树深度统计:统计子树中距离为d的节点数
  3. 树上距离DP:如统计距离为k的点对数量
  4. 路径长度计数:特定长度路径的数量统计
  5. 树形背包优化:某些特殊约束下的背包问题

长链剖分虽然不如重链剖分广为人知,但在处理深度相关问题时具有不可替代的优势,是高级树形DP的必备工具。