每日算法 — 使用java实现笛卡尔树:线性构建与RMQ最优解查询

一、算法背景与核心思想

笛卡尔树(Cartesian Tree)是一种同时满足堆性质与二叉搜索树性质的特殊数据结构。对于给定序列,笛卡尔树以序列最小值(或最大值)为根,左子树递归构建于最小值左侧子序列,右子树递归构建于最小值右侧子序列。这使得它天然适合解决区间最值查询(RMQ)问题。

与传统线段树 O(n) 建树、O(log n) 查询相比,笛卡尔树可以在 O(n) 时间内完成构建,并配合 O(1) 的LCA(最近公共祖先)查询,实现 RMQ 的常数级响应。本文将详细讲解其线性构建算法,并提供可直接运行的完整 Java 实现。

二、笛卡尔树的定义与性质

给定一个序列 A[0..n-1],其笛卡尔树满足以下两条核心性质:

  1. 堆性质(Heap Property):每个节点的值不大于(或不小于)其子节点的值。以最小值堆为例,根节点为全局最小值。
  2. 中序遍历有序(BST Property):对笛卡尔树进行中序遍历,得到的序列恰好是原序列 A[0..n-1]。

以序列 [3, 1, 4, 1, 5, 9, 2, 6] 为例,其最小堆笛卡尔树结构如下:

  • 根节点为全局最小值 1(索引1处的值)
  • 左子树由 [3] 构建,右子树由 [4, 1, 5, 9, 2, 6] 构建
  • 右子树的根为右半部分最小值 1(索引3处的值)
  • 依此类推递归构建

三、基于单调栈的线性构建算法

3.1 算法思路

若采用递归分治构建,时间复杂度为 O(n log n)。而利用单调栈,我们可以在 O(n) 时间内完成构建。

核心观察:按序列从左到右依次插入节点时,新节点 A[i] 会沿着当前树的最右链向上”爬升”,直到找到值比它小(最小堆)的节点作为父节点。这一”最右链爬升”过程恰好可以用单调栈维护:

  • 维护一个值递增的单调栈
  • 每来一个新节点,弹出栈顶所有比它大的节点
  • 最后被弹出的节点成为新节点的左孩子
  • 新节点成为当前栈顶节点的右孩子

3.2 算法步骤详解

初始化空栈
for i = 0 to n-1:
    last = null
    while 栈非空 且 栈顶节点值 > A[i]:
        last = 栈顶弹出
    if 栈非空:
        栈顶节点的右孩子 = i
    if last != null:
        i的左孩子 = last
    将i压入栈
栈底元素即为根节点

四、RMQ问题的笛卡尔树解法

4.1 核心定理

对于序列 A 构建的最小堆笛卡尔树,查询区间 [l, r] 的最小值,等价于求原序列中 l 位置和 r 位置对应节点在笛卡尔树上的最近公共祖先(LCA)。该LCA节点对应的值,即为区间 [l, r] 的最小值。

4.2 LCA预处理

为支持 O(1) RMQ 查询,需要对笛卡尔树进行LCA预处理:

  1. 欧拉遍历:DFS遍历树,记录访问的节点序列和对应深度
  2. 首次出现数组:记录每个节点在欧拉序列中首次出现的位置
  3. RMQ(Sparse Table):在欧拉序列的深度数组上构建ST表,实现O(1)的LCA查询

由于欧拉序列长度为 2n-1,ST表构建复杂度为 O(n log n),查询为 O(1)。

五、完整Java实现

import java.util.*;

/**
 * 笛卡尔树实现:线性构建 + RMQ查询
 * 基于单调栈的O(n)建树算法,配合欧拉序+ST表实现O(1)区间最值查询
 */
public class CartesianTree {

    // ========== 笛卡尔树节点定义 ==========
    static class Node {
        int val;      // 节点值
        int index;    // 在原序列中的索引
        int left;     // 左孩子索引,-1表示空
        int right;    // 右孩子索引,-1表示空
        int parent;   // 父节点索引,-1表示根

        Node(int val, int index) {
            this.val = val;
            this.index = index;
            this.left = -1;
            this.right = -1;
            this.parent = -1;
        }
    }

    private final int n;           // 序列长度
    private final int[] arr;       // 原序列
    private final Node[] nodes;    // 笛卡尔树节点数组
    private int root;              // 根节点索引

    // ========== LCA预处理相关数组 ==========
    private int[] euler;           // 欧拉遍历序列(节点索引)
    private int[] depthEuler;      // 欧拉序列对应深度
    private int[] firstOccur;      // 每个节点在欧拉序列中首次出现的位置
    private int eulerSize;         // 欧拉序列长度
    private int[][] st;            // Sparse Table
    private int[] logTable;        // 预计算的对数表

    /**
     * 构造函数:构建笛卡尔树
     * @param arr 输入序列
     */
    public CartesianTree(int[] arr) {
        this.n = arr.length;
        this.arr = arr.clone();
        this.nodes = new Node[n];
        for (int i = 0; i < n; i++) {
            nodes[i] = new Node(arr[i], i);
        }
        buildCartesianTree();
        preprocessLCA();
    }

    /**
     * 基于单调栈的O(n)笛卡尔树构建算法
     * 维护值递增的栈,确保最小堆性质
     */
    private void buildCartesianTree() {
        Deque<Integer> stack = new ArrayDeque<>();

        for (int i = 0; i < n; i++) {
            int last = -1;

            // 弹出所有值大于当前节点的栈顶元素
            while (!stack.isEmpty() && nodes[stack.peek()].val > nodes[i].val) {
                last = stack.pop();
            }

            // 当前栈顶成为当前节点的父节点(右孩子关系)
            if (!stack.isEmpty()) {
                nodes[i].parent = stack.peek();
                nodes[stack.peek()].right = i;
            }

            // 最后被弹出的节点成为当前节点的左孩子
            if (last != -1) {
                nodes[last].parent = i;
                nodes[i].left = last;
            }

            stack.push(i);
        }

        // 栈底元素为根节点(单调栈中最后剩下的最小值)
        while (stack.size() > 1) {
            stack.pop();
        }
        root = stack.isEmpty() ? -1 : stack.peek();
    }

    /**
     * LCA预处理:欧拉遍历 + Sparse Table
     */
    private void preprocessLCA() {
        euler = new int[2 * n];
        depthEuler = new int[2 * n];
        firstOccur = new int[n];
        Arrays.fill(firstOccur, -1);

        eulerSize = 0;
        dfs(root, 0);

        // 构建对数表
        logTable = new int[eulerSize + 1];
        logTable[1] = 0;
        for (int i = 2; i <= eulerSize; i++) {
            logTable[i] = logTable[i / 2] + 1;
        }

        // 构建Sparse Table
        int k = logTable[eulerSize] + 1;
        st = new int[eulerSize][k];
        for (int i = 0; i < eulerSize; i++) {
            st[i][0] = i;  // 初始时每个位置的最小值就是自身
        }

        for (int j = 1; j < k; j++) {
            for (int i = 0; i + (1 << j) <= eulerSize; i++) {
                int left = st[i][j - 1];
                int right = st[i + (1 << (j - 1))][j - 1];
                st[i][j] = depthEuler[left] < depthEuler[right] ? left : right;
            }
        }
    }

    /**
     * DFS生成欧拉遍历序列
     * @param u 当前节点索引
     * @param depth 当前深度
     */
    private void dfs(int u, int depth) {
        if (u == -1) return;

        // 记录首次出现位置
        if (firstOccur[u] == -1) {
            firstOccur[u] = eulerSize;
        }

        euler[eulerSize] = u;
        depthEuler[eulerSize] = depth;
        eulerSize++;

        if (nodes[u].left != -1) {
            dfs(nodes[u].left, depth + 1);
            euler[eulerSize] = u;
            depthEuler[eulerSize] = depth;
            eulerSize++;
        }

        if (nodes[u].right != -1) {
            dfs(nodes[u].right, depth + 1);
            euler[eulerSize] = u;
            depthEuler[eulerSize] = depth;
            eulerSize++;
        }
    }

    /**
     * 查询区间[l, r]的最小值(基于LCA的O(1)查询)
     * @param l 左边界(包含)
     * @param r 右边界(包含)
     * @return 区间最小值
     */
    public int queryMin(int l, int r) {
        if (l > r) {
            int tmp = l;
            l = r;
            r = tmp;
        }
        int fl = firstOccur[l];
        int fr = firstOccur[r];
        if (fl > fr) {
            int tmp = fl;
            fl = fr;
            fr = tmp;
        }
        int j = logTable[fr - fl + 1];
        int left = st[fl][j];
        int right = st[fr - (1 << j) + 1][j];
        int lcaIndex = depthEuler[left] < depthEuler[right] ? left : right;
        return arr[euler[lcaIndex]];
    }

    /**
     * 获取笛卡尔树的根节点索引
     */
    public int getRoot() {
        return root;
    }

    /**
     * 打印树的结构(用于调试)
     */
    public void printTree() {
        System.out.println("笛卡尔树结构(根节点索引: " + root + ", 值: " + (root >= 0 ? nodes[root].val : "null") + ")");
        for (int i = 0; i < n; i++) {
            System.out.printf("  节点[%d] val=%d, left=%d, right=%d, parent=%d%n",
                    i, nodes[i].val, nodes[i].left, nodes[i].right, nodes[i].parent);
        }
    }

    // ========== 主程序:测试与演示 ==========
    public static void main(String[] args) {
        // 测试序列
        int[] arr = {3, 1, 4, 1, 5, 9, 2, 6};
        System.out.println("原序列: " + Arrays.toString(arr));
        System.out.println();

        CartesianTree ct = new CartesianTree(arr);
        ct.printTree();
        System.out.println();

        // RMQ查询测试
        int[][] queries = {
                {0, 7}, {0, 2}, {2, 5}, {4, 7}, {1, 3}, {5, 5}
        };

        System.out.println("RMQ查询结果:");
        for (int[] q : queries) {
            int l = q[0], r = q[1];
            int min = ct.queryMin(l, r);
            System.out.printf("  区间[%d, %d] 最小值 = %d%n", l, r, min);
        }

        // 验证正确性:暴力对比
        System.out.println("\n暴力验证:");
        for (int[] q : queries) {
            int l = q[0], r = q[1];
            int brute = arr[l];
            for (int i = l + 1; i <= r; i++) {
                brute = Math.min(brute, arr[i]);
            }
            int tree = ct.queryMin(l, r);
            System.out.printf("  [%d, %d] 暴力=%d, 笛卡尔树=%d, %s%n",
                    l, r, brute, tree, brute == tree ? "正确" : "错误!");
        }

        // 大规模性能测试
        System.out.println("\n大规模性能测试 (n=100000, 100000次查询):");
        int n = 100000;
        int[] largeArr = new int[n];
        Random rand = new Random(42);
        for (int i = 0; i < n; i++) {
            largeArr[i] = rand.nextInt(1000000);
        }

        long startBuild = System.currentTimeMillis();
        CartesianTree largeCt = new CartesianTree(largeArr);
        long buildTime = System.currentTimeMillis() - startBuild;
        System.out.println("  建树时间: " + buildTime + " ms");

        int qCount = 100000;
        long startQuery = System.currentTimeMillis();
        for (int i = 0; i < qCount; i++) {
            int l = rand.nextInt(n);
            int r = rand.nextInt(n);
            if (l > r) {
                int tmp = l;
                l = r;
                r = tmp;
            }
            largeCt.queryMin(l, r);
        }
        long queryTime = System.currentTimeMillis() - startQuery;
        System.out.println("  " + qCount + "次查询时间: " + queryTime + " ms");
        System.out.println("  平均单次查询: " + (queryTime * 1000000.0 / qCount) + " ns");
    }
}

六、复杂度分析

操作 时间复杂度 空间复杂度 说明
建树 O(n) O(n) 单调栈保证每个节点最多入栈出栈各一次
LCA预处理 O(n log n) O(n log n) 欧拉序列长度 2n-1,ST表空间
单次RMQ查询 O(1) O(1) ST表上的区间最值查询

若只需建树而不需要频繁查询,可以省略LCA预处理。若查询次数 Q 远大于 n,则笛卡尔树 + LCA 的 O(n log n + Q) 总复杂度显著优于线段树的 O(n + Q log n)。

七、算法扩展与应用场景

  1. 最大堆笛卡尔树:将单调栈中的比较符号反转即可构建以最大值为根的笛卡尔树。
  2. 子数组最值贡献:利用笛卡尔树可以高效统计每个元素作为区间最值的子数组个数,常用于组合计数问题。
  3. 单调栈的等价视角:笛卡尔树的构建过程本质上就是单调栈维护”最右链”的过程,理解笛卡尔树有助于深化对单调栈本质的认识。
  4. 与Treap的对比:笛卡尔树是确定的(由序列唯一确定),而Treap引入随机优先级以保证期望平衡,两者都利用了堆+BST的双重性质。

八、总结

笛卡尔树通过巧妙地融合堆性质与二叉搜索树性质,将序列的区间最值查询转化为树上的LCA问题。基于单调栈的线性构建算法是其核心亮点——每个节点仅入栈出栈一次,实现了理论最优的 O(n) 建树效率。配合欧拉遍历与Sparse Table的LCA预处理,RMQ查询可以降至常数时间。掌握笛卡尔树,不仅能高效解决区间最值问题,更能加深对单调栈、树形结构和LCA之间内在联系的理解。