最优二叉搜索树的平均搜索代价最小化问题
字数 5977 2025-12-10 07:02:08

最优二叉搜索树的平均搜索代价最小化问题

好的,这是区间动态规划领域的一个经典且重要的问题。我们从一个具体但不复杂的例子入手,来逐步理解这个问题的本质和解决方案。

问题描述

假设我们有一组有序的关键字,例如 [“do”, “if”, “while”]。每个关键字被搜索的概率是已知的,我们用一个数组 p 表示,例如 p = [0.5, 0.1, 0.05]。这意味着用户搜索“do”的概率是50%,搜索“if”的概率是10%,搜索“while”的概率是5%。

同时,搜索也可能失败。例如,用户可能想搜索“for”,但“for”并不在我们的关键字列表中。我们把所有可能失败的情况(即,不在关键字列表中的值)也考虑进去。我们定义一些虚拟键(dummy keys),代表搜索失败的情况。对于一个有n个关键字的序列,有n+1个可能的失败区间(小于第一个关键字,在两个关键字之间,大于最后一个关键字)。每个失败区间也有一个被搜索的概率,用数组 q 表示。例如,q = [0.15, 0.1, 0.05, 0.05]

我们的目标是构建一棵二叉搜索树(BST)来存放这些关键字,使得在给定这些成功和失败概率的情况下,一次搜索的平均代价(平均比较次数)最小化

一个关键概念:在一棵BST中,搜索一个关键字的代价等于从根节点到该关键字节点的路径长度+1(即比较次数)。搜索失败(落入一个虚拟键)的代价等于从根节点到达的“外部节点”(空指针)的深度。

我们的任务:给定关键字序列(已排序)和对应的概率数组 p[1..n]q[0..n],设计一个算法,计算并构建出具有最小平均搜索代价的二叉搜索树。


渐进式解题过程

步骤1:理解问题输入和输出

  • 输入:
    1. 关键字数量 n
    2. 成功概率数组 p[1..n]p[i] 是搜索关键字 i 的概率。
    3. 失败概率数组 q[0..n]q[i] 是搜索值落在关键字 ii+1 之间(对于q[0]是小于所有关键字,q[n]是大于所有关键字)的概率。
  • 隐含条件: sum(p) + sum(q) = 1
  • 输出:
    1. 最小的平均搜索代价。
    2. (可选)这棵最优BST的结构(根节点是什么,左右子树如何划分)。

步骤2:分析最优子结构

区间动态规划的核心是识别“最优子结构”。假设我们选择关键字 k 作为树的根节点。

  • 那么,所有关键字 1, 2, ..., k-1 必然在它的左子树中。
  • 所有关键字 k+1, ..., n 必然在它的右子树中。

这个划分非常像快速排序矩阵链乘,它把一个大问题(为关键字 ij 构建最优BST)分解为两个子问题(为关键字 ik-1 构建左子树,为关键字 k+1j 构建右子树)。

如果一个树对于整个集合是最优的,那么它的左、右子树对于它们各自的子关键字集合也一定是最优的。否则,我们可以用一个更好的子树替换它,从而得到整体更好的树,这与“最优”矛盾。

步骤3:定义DP状态

我们定义一个二维DP表 e[i][j]

  • e[i][j] 的含义:为包含关键字 i, i+1, ..., j 的集合(以及相关的虚拟键)构建一棵最优二叉搜索树的期望搜索代价
  • 注意:当 i > j 时,表示子树为空。此时,这个“子树”只包含一个虚拟键(失败节点)。它的期望搜索代价就是搜索到这个失败节点的代价(深度)乘以它的概率。但在这里,我们把它当作一个基本情况来处理。

更常见且方便的做法是,我们同时定义另一个表 w[i][j],用来存储子树的概率总和

  • w[i][j] 的含义:包含关键字 ij 的子树(以及其所有下属的虚拟键)被搜索到的总概率
  • 计算公式:w[i][j] = sum(p[i..j]) + sum(q[i-1..j])。可以递归计算:w[i][j] = w[i][j-1] + p[j] + q[j]

为什么要 w[i][j]?想象一下,当你把两棵子树 (i, k-1)(k+1, j) 分别挂到根节点 k 下面时,子树中所有节点的深度都增加了1。因此,整个子树的期望搜索代价会增加,增加的量正好等于该子树的总概率 w(因为每个节点的深度+1,其贡献的期望代价就增加了 概率 * 1)。

步骤4:推导状态转移方程

我们的目标是求 e[1][n]

1. 基本情况

  • j = i - 1 时,子树为空,只包含虚拟键 d_{i-1}(即对应失败区间 q[i-1])。此时构建的“树”就是一个外部节点。搜索到这个外部节点的代价就是它的深度。在动态规划构建过程中,当我们把它作为子树时,它的深度会在父节点中被计算。所以,我们可以将其基础代价设为 q[i-1](更准确地说,是子树自身的代价,在挂到父节点前)。
  • 因此,我们定义 e[i][i-1] = q[i-1]
  • 同理,w[i][i-1] = q[i-1]

2. 一般情况 (i <= j)

  • 我们需要尝试从 ij 中每个关键字 k 作为根节点。
  • 如果选择 k 作为根:
    • 左子树包含关键字 i ... k-1,其最优期望代价为 e[i][k-1]
    • 右子树包含关键字 k+1 ... j,其最优期望代价为 e[k+1][j]
    • 左子树的总概率为 w[i][k-1]
    • 右子树的总概率为 w[k+1][j]
  • 关键点:当我们将左右子树连接到根节点 k 时,这两棵子树中每一个节点(包括虚拟的外部节点)的深度都增加了1。因此,整个搜索代价的期望值会增加 w[i][k-1] + w[k+1][j]
  • 此外,我们还要加上根节点 k 本身的贡献:搜索到 k 的代价是1(一次比较),乘以它的概率 p[k],即 p[k]
  • 因此,以 k 为根的子树 (i, j) 的总期望代价为:
    cost = e[i][k-1] + e[k+1][j] + w[i][k-1] + p[k] + w[k+1][j]
  • 注意 w[i][k-1] + p[k] + w[k+1][j] 正好等于 w[i][j](整个子树的总概率)。
  • 所以,转移方程可以优雅地写为:
    e[i][j] = min_{k from i to j} ( e[i][k-1] + e[k+1][j] + w[i][j] )
  • 其中 w[i][j] 是固定的(对于给定的 i, j),无论谁当根,这部分因深度增加而“额外付出”的代价都是一样的。我们要最小化的,实际上是 e[i][k-1] + e[k+7][j] 这部分。

步骤5:计算顺序与初始化

  • e 表和 w 表都是二维的,大小为 (n+2) x (n+1)(n+1) x (n+1),为了方便处理 i-1j+1 的边界,通常下标从0开始或留出空位。
  • 我们需要先计算小区间,再计算大区间。因此,最外层的循环是区间长度 L,从 0n-1
    • L = 0: 计算 e[i][i],即只有一个关键字的子树。
      • 以唯一的关键字 i 为根。
      • e[i][i] = e[i][i-1] + e[i+1][i] + w[i][i]
      • 其中 e[i][i-1] = q[i-1], e[i+1][i] = q[i]
      • w[i][i] = p[i] + q[i-1] + q[i]
    • L = 1: 计算包含两个关键字的子树 e[i][i+1],以此类推。
  • 同时,我们需要另一个表 root[i][j] 来记录对于区间 [i, j] 取得最小代价时选择的根节点 k,以便最后重建最优BST的结构。

步骤6:实例演算

让我们用一个超简单的例子来走一遍流程。
假设 n = 1,只有一个关键字 “A”。

  • p[1] = 0.4
  • q[0] = 0.1 (搜索值小于"A"的概率)
  • q[1] = 0.5 (搜索值大于"A"的概率)
  • 总和 = 1.0。

计算:

  1. 初始化:

    • w[1][0] = q[0] = 0.1
    • e[1][0] = q[0] = 0.1
    • w[2][1] = q[1] = 0.5
    • e[2][1] = q[1] = 0.5
  2. 计算 e[1][1]:

    • w[1][1] = p[1] + q[0] + q[1] = 0.4 + 0.1 + 0.5 = 1.0
    • 只有一个根可选 k=1
    • e[1][1] = e[1][0] + e[2][1] + w[1][1] = 0.1 + 0.5 + 1.0 = 1.6

所以,最小的平均搜索代价是 1.6。你可以验证:这棵树只有根节点"A"。搜索"A"成功比较1次,概率0.4,贡献 0.4*1=0.4。搜索失败,比较1次后到达空指针,概率 0.1+0.5=0.6,贡献 0.6*1=0.6。总期望 1.0?等等,不对。

这里有一个重要的理解点:我们的 e[i][j] 计算的代价,包含了从这个子树的根开始的搜索。而在我们最后的答案 e[1][n] 中,我们计算的已经是整棵树的期望代价。对于这个单节点树:

  • 搜索“A”:比较1次(根节点),代价1。
  • 搜索失败(比如小于“A”):比较流程是:访问根节点"A"(1次比较),发现目标值更小,转向左子树(空),这意味着我们实际上进行了一次比较就判断失败了吗?不,在经典的二叉搜索树模型中,搜索失败是在到达一个外部(空)节点时判定的。要到达代表“小于A”的外部节点,你需要从根节点走左指针,所以深度是2(根节点和外部节点)。
  • 因此,更准确的模型是:一个包含关键字 i..j 的子树,它的所有外部节点(失败节点)的深度比其父关键字节点深一层。
  • 在我们定义的 e[i][i-1] = q[i-1] 中,这个 q[i-1] 是外部节点的概率,但它本身代表的代价是这个外部节点作为一棵“空树”的根的代价,即为1?这造成了混乱。

为了清晰,我们采用算法导论中的标准定义,它修正了深度计算:

修正后的定义

  • 对于一个包含节点(包括内部关键字节点和外部失败节点)的树,搜索代价是搜索路径上访问的节点数
  • 因此,访问一个深度为 d 的内部关键字节点,代价为 d
  • 访问一个深度为 d 的外部失败节点,代价也为 d(你需要比较 d 次才知道失败)。
  • 那么,期望代价 = Σ(每个节点的深度 × 该节点被访问的概率)。

在这个定义下,我们的动态规划需要做调整:

  • e[i][j]: 由关键字 i..j 构成的最优子树的期望搜索代价
  • w[i][j]: 子树 i..j 中所有节点(包括关键字 i..j 和所有相关的失败节点 q[i-1] .. q[j])的概率之和。w[i][j] = sum(p[i..j]) + sum(q[i-1..j])
  • 转移方程
    e[i][j] = min_{i<=k<=j} { e[i][k-1] + e[k+1][j] + w[i][j] }
  • 边界条件
    e[i][i-1] = w[i][i-1] = q[i-1]。因为一个空子树只包含一个外部失败节点 d_{i-1},其深度为0?不对,这里需要小心。

最清晰的逻辑
j = i-1,表示没有关键字,只有失败节点 d_{i-1}。这棵“树”只有一个外部节点。它的期望代价就是 q[i-1] * 1?不,在动态规划组合时,当我们把它作为子树挂到某个根节点下,这个外部节点的深度会增加。所以,在 e[i][i-1] 中,我们应该存储的是以这个外部节点为根的子树,在其自身层级上的代价贡献。由于它只是一个节点,访问它的代价是1(一次访问)。因此,e[i][i-1] = q[i-1] * 1 = q[i-1]。这是合理的。

现在重新计算 n=1 的例子:

  • w[1][1] = p1 + q0 + q1 = 0.4+0.1+0.5=1.0
  • e[1][1] = min_{k=1} ( e[1][0] + e[2][1] + w[1][1] )
  • e[1][0] = q0 = 0.1
  • e[2][1] = q1 = 0.5
  • e[1][1] = 0.1 + 0.5 + 1.0 = 1.6

这个 1.6 就是整棵树的总期望代价。让我们手动验证一下树的结构:
根节点 A (深度1)
/
外部节点d0(深度2) 外部节点d1(深度2)

搜索概率和代价:

  • 搜索A成功:概率0.4,深度1,贡献 0.4*1 = 0.4
  • 搜索失败(落入d0):概率0.1,深度2,贡献 0.1*2 = 0.2
  • 搜索失败(落入d1):概率0.5,深度2,贡献 0.5*2 = 1.0
    总期望 = 0.4 + 0.2 + 1.0 = 1.6。完全正确!

步骤7:算法总结与复杂度

  1. 初始化二维数组 e[1..n+1, 0..n]w[1..n+1, 0..n]
  2. 初始化边界:对于 i=1n+1,令 e[i][i-1] = q[i-1], w[i][i-1] = q[i-1]
  3. 循环区间长度 l = 0n-1:
    • 循环起点 i = 1n-l:
      • 计算 j = i + l
      • 计算 w[i][j] = w[i][j-1] + p[j] + q[j]
      • 设置 e[i][j] = 无穷大
      • 循环可能的根节点 k = ij:
        • t = e[i][k-1] + e[k+1][j] + w[i][j]
        • 如果 t < e[i][j],则更新 e[i][j] = t,并记录 root[i][j] = k
  4. 最终结果:e[1][n] 即为最小平均搜索代价。通过 root 表递归构建树的结构。

时间复杂度: 三重循环,O(n³)。
空间复杂度: O(n²)。


通过以上步骤,我们完成了对“最优二叉搜索树”问题的完整分析。这个问题完美地体现了区间动态规划的思想:将问题分解为左右两个子区间,通过遍历所有可能的分割点(根节点)来寻找最优解,并利用辅助表 w 来高效计算因深度增加而产生的额外代价。

最优二叉搜索树的平均搜索代价最小化问题 好的,这是区间动态规划领域的一个经典且重要的问题。我们从一个具体但不复杂的例子入手,来逐步理解这个问题的本质和解决方案。 问题描述 假设我们有一组 有序 的关键字,例如 [“do”, “if”, “while”] 。每个关键字被搜索的概率是已知的,我们用一个数组 p 表示,例如 p = [0.5, 0.1, 0.05] 。这意味着用户搜索“do”的概率是50%,搜索“if”的概率是10%,搜索“while”的概率是5%。 同时,搜索也可能失败。例如,用户可能想搜索“for”,但“for”并不在我们的关键字列表中。我们把所有可能失败的情况(即,不在关键字列表中的值)也考虑进去。我们定义一些 虚拟键 (dummy keys),代表搜索失败的情况。对于一个有n个关键字的序列,有n+1个可能的失败区间(小于第一个关键字,在两个关键字之间,大于最后一个关键字)。每个失败区间也有一个被搜索的概率,用数组 q 表示。例如, q = [0.15, 0.1, 0.05, 0.05] 。 我们的目标是构建一棵 二叉搜索树(BST) 来存放这些关键字,使得在给定这些成功和失败概率的情况下, 一次搜索的平均代价(平均比较次数)最小化 。 一个关键概念 :在一棵BST中,搜索一个关键字的代价等于从根节点到该关键字节点的路径长度+1(即比较次数)。搜索失败(落入一个虚拟键)的代价等于从根节点到达的“外部节点”(空指针)的深度。 我们的任务 :给定关键字序列(已排序)和对应的概率数组 p[1..n] 和 q[0..n] ,设计一个算法,计算并构建出具有 最小平均搜索代价 的二叉搜索树。 渐进式解题过程 步骤1:理解问题输入和输出 输入 : 关键字数量 n 。 成功概率数组 p[1..n] , p[i] 是搜索关键字 i 的概率。 失败概率数组 q[0..n] , q[i] 是搜索值落在关键字 i 和 i+1 之间(对于 q[0] 是小于所有关键字, q[n] 是大于所有关键字)的概率。 隐含条件 : sum(p) + sum(q) = 1 。 输出 : 最小的平均搜索代价。 (可选)这棵最优BST的结构(根节点是什么,左右子树如何划分)。 步骤2:分析最优子结构 区间动态规划的核心是识别“最优子结构”。假设我们选择关键字 k 作为树的根节点。 那么,所有关键字 1, 2, ..., k-1 必然在它的左子树中。 所有关键字 k+1, ..., n 必然在它的右子树中。 这个划分非常像 快速排序 或 矩阵链乘 ,它把一个大问题(为关键字 i 到 j 构建最优BST)分解为两个子问题(为关键字 i 到 k-1 构建左子树,为关键字 k+1 到 j 构建右子树)。 如果一个树对于整个集合是最优的,那么它的左、右子树对于它们各自的子关键字集合也一定是最优的。否则,我们可以用一个更好的子树替换它,从而得到整体更好的树,这与“最优”矛盾。 步骤3:定义DP状态 我们定义一个二维DP表 e[i][j] 。 e[i][j] 的含义 :为包含关键字 i, i+1, ..., j 的集合(以及相关的虚拟键)构建一棵最优二叉搜索树的 期望搜索代价 。 注意 :当 i > j 时,表示子树为空。此时,这个“子树”只包含一个虚拟键(失败节点)。它的期望搜索代价就是搜索到这个失败节点的代价(深度)乘以它的概率。但在这里,我们把它当作一个 基本情况 来处理。 更常见且方便的做法是,我们同时定义另一个表 w[i][j] ,用来存储子树的 概率总和 。 w[i][j] 的含义 :包含关键字 i 到 j 的子树(以及其所有下属的虚拟键)被搜索到的 总概率 。 计算公式: w[i][j] = sum(p[i..j]) + sum(q[i-1..j]) 。可以递归计算: w[i][j] = w[i][j-1] + p[j] + q[j] 。 为什么要 w[i][j] ?想象一下,当你把两棵子树 (i, k-1) 和 (k+1, j) 分别挂到根节点 k 下面时, 子树中所有节点的深度都增加了1 。因此,整个子树的期望搜索代价会增加,增加的量正好等于该子树的总概率 w (因为每个节点的深度+1,其贡献的期望代价就增加了 概率 * 1 )。 步骤4:推导状态转移方程 我们的目标是求 e[1][n] 。 1. 基本情况 : 当 j = i - 1 时,子树为空,只包含虚拟键 d_{i-1} (即对应失败区间 q[i-1] )。此时构建的“树”就是一个外部节点。搜索到这个外部节点的代价就是它的深度。在动态规划构建过程中,当我们把它作为子树时,它的深度会在父节点中被计算。所以,我们可以将其基础代价设为 q[i-1] (更准确地说,是子树自身的代价,在挂到父节点前)。 因此,我们定义 e[i][i-1] = q[i-1] 。 同理, w[i][i-1] = q[i-1] 。 2. 一般情况 (i <= j) : 我们需要尝试从 i 到 j 中每个关键字 k 作为根节点。 如果选择 k 作为根: 左子树包含关键字 i ... k-1 ,其最优期望代价为 e[i][k-1] 。 右子树包含关键字 k+1 ... j ,其最优期望代价为 e[k+1][j] 。 左子树的总概率为 w[i][k-1] 。 右子树的总概率为 w[k+1][j] 。 关键点 :当我们将左右子树连接到根节点 k 时,这两棵子树中 每一个节点(包括虚拟的外部节点)的深度都增加了1 。因此,整个搜索代价的期望值会增加 w[i][k-1] + w[k+1][j] 。 此外,我们还要加上根节点 k 本身的贡献:搜索到 k 的代价是1(一次比较),乘以它的概率 p[k] ,即 p[k] 。 因此,以 k 为根的子树 (i, j) 的总期望代价为: cost = e[i][k-1] + e[k+1][j] + w[i][k-1] + p[k] + w[k+1][j] 注意 w[i][k-1] + p[k] + w[k+1][j] 正好等于 w[i][j] (整个子树的总概率)。 所以,转移方程可以优雅地写为: e[i][j] = min_{k from i to j} ( e[i][k-1] + e[k+1][j] + w[i][j] ) 其中 w[i][j] 是固定的(对于给定的 i, j ),无论谁当根,这部分因深度增加而“额外付出”的代价都是一样的。我们要最小化的,实际上是 e[i][k-1] + e[k+7][j] 这部分。 步骤5:计算顺序与初始化 e 表和 w 表都是二维的,大小为 (n+2) x (n+1) 或 (n+1) x (n+1) ,为了方便处理 i-1 和 j+1 的边界,通常下标从0开始或留出空位。 我们需要先计算小区间,再计算大区间。因此,最外层的循环是区间长度 L ,从 0 到 n-1 。 L = 0 : 计算 e[i][i] ,即只有一个关键字的子树。 以唯一的关键字 i 为根。 e[i][i] = e[i][i-1] + e[i+1][i] + w[i][i] 其中 e[i][i-1] = q[i-1] , e[i+1][i] = q[i] 。 w[i][i] = p[i] + q[i-1] + q[i] 。 L = 1 : 计算包含两个关键字的子树 e[i][i+1] ,以此类推。 同时,我们需要另一个表 root[i][j] 来记录对于区间 [i, j] 取得最小代价时选择的根节点 k ,以便最后重建最优BST的结构。 步骤6:实例演算 让我们用一个超简单的例子来走一遍流程。 假设 n = 1 ,只有一个关键字 “A”。 p[1] = 0.4 q[0] = 0.1 (搜索值小于"A"的概率) q[1] = 0.5 (搜索值大于"A"的概率) 总和 = 1.0。 计算: 初始化: w[1][0] = q[0] = 0.1 e[1][0] = q[0] = 0.1 w[2][1] = q[1] = 0.5 e[2][1] = q[1] = 0.5 计算 e[1][1] : w[1][1] = p[1] + q[0] + q[1] = 0.4 + 0.1 + 0.5 = 1.0 。 只有一个根可选 k=1 。 e[1][1] = e[1][0] + e[2][1] + w[1][1] = 0.1 + 0.5 + 1.0 = 1.6 。 所以,最小的平均搜索代价是 1.6 。你可以验证:这棵树只有根节点"A"。搜索"A"成功比较1次,概率0.4,贡献 0.4*1=0.4 。搜索失败,比较1次后到达空指针,概率 0.1+0.5=0.6 ,贡献 0.6*1=0.6 。总期望 1.0 ?等等,不对。 这里有一个重要的理解点 :我们的 e[i][j] 计算的代价,包含了从 这个子树的根 开始的搜索。而在我们最后的答案 e[1][n] 中,我们计算的已经是整棵树的期望代价。对于这个单节点树: 搜索“A”:比较1次(根节点),代价1。 搜索失败(比如小于“A”):比较流程是:访问根节点"A"(1次比较),发现目标值更小,转向左子树(空),这意味着我们实际上进行了 一次比较就判断失败了 吗?不,在经典的二叉搜索树模型中,搜索失败是在到达一个外部(空)节点时判定的。要到达代表“小于A”的外部节点,你需要从根节点走左指针,所以深度是2(根节点和外部节点)。 因此,更准确的模型是:一个包含关键字 i..j 的子树,它的所有 外部节点(失败节点) 的深度比其父关键字节点深一层。 在我们定义的 e[i][i-1] = q[i-1] 中,这个 q[i-1] 是外部节点的概率,但它本身代表的代价是 这个外部节点作为一棵“空树”的根 的代价,即为1?这造成了混乱。 为了清晰,我们采用算法导论中的标准定义,它修正了深度计算: 修正后的定义 : 对于一个包含节点(包括内部关键字节点和外部失败节点)的树, 搜索代价是搜索路径上访问的节点数 。 因此,访问一个深度为 d 的内部关键字节点,代价为 d 。 访问一个深度为 d 的外部失败节点,代价也为 d (你需要比较 d 次才知道失败)。 那么,期望代价 = Σ(每个节点的深度 × 该节点被访问的概率)。 在这个定义下,我们的动态规划需要做调整: e[i][j] : 由关键字 i..j 构成的最优子树的 期望搜索代价 。 w[i][j] : 子树 i..j 中所有节点(包括关键字 i..j 和所有相关的失败节点 q[i-1] .. q[j] )的概率之和。 w[i][j] = sum(p[i..j]) + sum(q[i-1..j]) 。 转移方程 : e[i][j] = min_{i<=k<=j} { e[i][k-1] + e[k+1][j] + w[i][j] } 边界条件 : e[i][i-1] = w[i][i-1] = q[i-1] 。因为一个空子树只包含一个外部失败节点 d_{i-1} ,其深度为0?不对,这里需要小心。 最清晰的逻辑 : 当 j = i-1 ,表示没有关键字,只有失败节点 d_{i-1} 。这棵“树”只有一个外部节点。它的期望代价就是 q[i-1] * 1 ?不,在动态规划组合时,当我们把它作为子树挂到某个根节点下,这个外部节点的深度会增加。所以,在 e[i][i-1] 中,我们应该存储的是 以这个外部节点为根的子树,在其自身层级上的代价贡献 。由于它只是一个节点,访问它的代价是1(一次访问)。因此, e[i][i-1] = q[i-1] * 1 = q[i-1] 。这是合理的。 现在重新计算 n=1 的例子: w[1][1] = p1 + q0 + q1 = 0.4+0.1+0.5=1.0 e[1][1] = min_{k=1} ( e[1][0] + e[2][1] + w[1][1] ) e[1][0] = q0 = 0.1 e[2][1] = q1 = 0.5 e[1][1] = 0.1 + 0.5 + 1.0 = 1.6 这个 1.6 就是整棵树的总期望代价。让我们手动验证一下树的结构: 根节点 A (深度1) / 外部节点d0(深度2) 外部节点d1(深度2) 搜索概率和代价: 搜索A成功:概率0.4,深度1,贡献 0.4* 1 = 0.4 搜索失败(落入d0):概率0.1,深度2,贡献 0.1* 2 = 0.2 搜索失败(落入d1):概率0.5,深度2,贡献 0.5* 2 = 1.0 总期望 = 0.4 + 0.2 + 1.0 = 1.6。完全正确! 步骤7:算法总结与复杂度 初始化二维数组 e[1..n+1, 0..n] 和 w[1..n+1, 0..n] 。 初始化边界:对于 i=1 到 n+1 ,令 e[i][i-1] = q[i-1] , w[i][i-1] = q[i-1] 。 循环区间长度 l = 0 到 n-1 : 循环起点 i = 1 到 n-l : 计算 j = i + l 。 计算 w[i][j] = w[i][j-1] + p[j] + q[j] 。 设置 e[i][j] = 无穷大 。 循环可能的根节点 k = i 到 j : t = e[i][k-1] + e[k+1][j] + w[i][j] 如果 t < e[i][j] ,则更新 e[i][j] = t ,并记录 root[i][j] = k 。 最终结果: e[1][n] 即为最小平均搜索代价。通过 root 表递归构建树的结构。 时间复杂度 : 三重循环,O(n³)。 空间复杂度 : O(n²)。 通过以上步骤,我们完成了对“最优二叉搜索树”问题的完整分析。这个问题完美地体现了区间动态规划的思想:将问题分解为左右两个子区间,通过遍历所有可能的分割点(根节点)来寻找最优解,并利用辅助表 w 来高效计算因深度增加而产生的额外代价。