最优二叉搜索树(Optimal Binary Search Tree, OBST)问题
字数 4481 2025-12-12 00:19:42

最优二叉搜索树(Optimal Binary Search Tree, OBST)问题

题目描述
我们有一组有序的键(Key) K1 < K2 < ... < Kn,以及对应的搜索频率(概率)p1, p2, ..., pn。此外,我们还有一些不存在的键(即搜索目标不在这个集合中)的区间,可以视为一系列“虚键”(Dummy Keys)d0, d1, ..., dn,其中 d0 表示所有小于 K1 的键,di 表示所有介于 KiK_{i+1} 之间的键(1 <= i <= n-1),dn 表示所有大于 Kn 的键。每个虚键 di 也有一个被搜索到的频率(概率)q0, q1, ..., qn
所有频率满足:∑ pi (1 <= i <= n) + ∑ qi (0 <= i <= n) = 1

目标:构造一棵二叉搜索树(BST),以这些键(实键和虚键)为结点,使得总的期望搜索成本最小
期望搜索成本定义为:每个结点(实键或虚键)的深度 + 1(即从根到该结点的路径长度,根深度为0)乘以其频率,然后对所有结点求和。

用公式表达:
设结点 x 的频率为 freq(x),深度为 depth(x),则总成本 = ∑ freq(x) * (depth(x) + 1)
我们需要选择一种树的构造方式(即选取根,递归构造左右子树),最小化这个总成本。


解题思路
这是一个经典的区间动态规划问题,因为键是有序的,任何子树必然对应原键序列的一个连续子区间。

  1. 子问题定义
    定义 dp[i][j] 为:由实键 Ki, K_{i+1}, ..., Kj 构成的最优二叉搜索树的最小期望搜索成本(这里 1 <= i <= j <= n)。
    注意:子树中还包含虚键 d_{i-1}, d_i, ..., d_j(即这个子树对应的所有虚键)。

  2. 状态转移方程
    对于子区间 [i, j],我们需要选择一个根 Kri <= r <= j)。
    如果选择 Kr 为根,则:

    • 左子树包含实键 Ki, ..., K_{r-1} 以及虚键 d_{i-1}, ..., d_{r-1}
    • 右子树包含实键 K_{r+1}, ..., Kj 以及虚键 d_r, ..., d_j

    左子树的最小期望成本是 dp[i][r-1](当 r-1 < i 时,左子树为空,只有虚键 d_{i-1},其成本为 q_{i-1} 乘深度增量,但这里我们先不考虑深度增量,稍后统一处理)。

    更准确地说,dp[i][j] 表示的是:以这个子树为整体,其内部结点的深度是相对于这个子树的根来计算的。但在合并成大树时,子树的每个结点的深度会增加1(因为上面多了一层根)。
    所以,当我们计算以 Kr 为根的子树的总成本时,它包括:

    • Kr 的成本:pr * 1(根深度为0,所以深度+1=1)。
    • 左子树的成本:如果左子树非空,其期望成本原本是 dp[i][r-1],但左子树中每个结点的深度都增加了1(因为现在它们的根 Kr 是它们的父结点),所以左子树的贡献应该是 dp[i][r-1] + 左子树中所有结点的频率之和(因为每个结点的深度+1,导致成本增加其频率值一次)。
    • 右子树同理:dp[r+1][j] + 右子树中所有结点的频率之和。

    为了计算方便,我们定义 w[i][j] 为区间 [i, j] 中所有实键频率 pi..pj 加上虚键频率 q_{i-1}..q_j 的总和。即:
    w[i][j] = ∑_{t=i}^{j} pt + ∑_{t=i-1}^{j} qt

    那么,以 Kr 为根时,总成本为:
    cost = pr + (dp[i][r-1] + w[i][r-1]) + (dp[r+1][j] + w[r+1][j])
    这里 dp[i][r-1] 已经包含了左子树内部的成本,w[i][r-1] 是左子树所有结点的频率和,加上它是因为左子树每个结点的深度增加了1,成本增加其频率值。
    右子树同理。

    但注意:dp[i][r-1]r-1 < i 时(左子树为空),我们应将其视为0,且 w[i][r-1] 应视为 q_{i-1}(即只有虚键 d_{i-1} 的频率)。
    为了统一公式,我们约定:当 i > j 时,dp[i][j] = 0w[i][j] = q_j(?)
    更常见且方便的做法是:预处理前缀和,并在状态转移时直接计算

  3. 预处理频率和
    sum[i][j] 表示从 ij 的所有实键频率 + 从 i-1j 的所有虚键频率之和。
    但更简单的方法:
    freq[1..n] 为实键频率 p1..pndummy[0..n] 为虚键频率 q0..qn
    定义 total[i][j] = (∑_{t=i}^{j} pt) + (∑_{t=i-1}^{j} qt)
    我们可以用前缀和快速计算:
    P[i] = p1 + ... + piQ[j] = q0 + ... + qj,则
    total[i][j] = (P[j] - P[i-1]) + (Q[j] - Q[i-2])(注意边界,i=1Q[i-2] 为0)。

  4. 简化状态转移
    观察公式:
    dp[i][j] = min_{r=i..j} { dp[i][r-1] + dp[r+1][j] + (total[i][j]) }
    为什么?
    因为 pr 包含在 total[i][j] 中,而 total[i][j] 正是整个区间 [i, j] 所有结点的频率和。
    当根为 Kr 时,总成本 = 左子树成本 + 右子树成本 + 整个区间的频率和(因为根和所有子孙结点的深度都相对增加了,但这里 dp[i][r-1] 已经是左子树的最小成本(深度从0算),合并后,左子树的每个结点深度+1,所以成本增加左子树的频率和,这个频率和就是 total[i][r-1],同理右子树增加 total[r+1][j]
    把这些增加量加上 dp[i][r-1] + dp[r+1][j],再加根的成本 pr,会发现总和正好等于:
    dp[i][r-1] + dp[r+1][j] + total[i][j]
    因为 total[i][j] = pr + total[i][r-1] + total[r+1][j]

  5. 最终状态转移方程
    所以,递推式为:
    dp[i][j] = min_{r=i..j} { dp[i][r-1] + dp[r+1][j] } + total[i][j]
    其中 dp[i][r-1]r-1 < i 时取0,dp[r+1][j]r+1 > j 时取0。
    边界:dp[i][i-1] = 0(表示空区间,只有一个虚键,但虚键成本已经在 total 中体现,所以这里为0)。

  6. 计算顺序
    区间长度 len 从 1 到 n 递增。
    对每个 len,枚举起点 i,计算 j = i+len-1,然后枚举根 rij,更新 dp[i][j]

  7. 初始化
    当区间长度为1(即只有一个实键 Ki)时:
    dp[i][i] = min_{r=i..i} { dp[i][r-1] + dp[r+1][i] } + total[i][i] = dp[i][i-1] + dp[i+1][i] + total[i][i]
    由于 dp[i][i-1] = 0dp[i+1][i] = 0,所以 dp[i][i] = total[i][i]
    total[i][i] = pi + q_{i-1} + q_i

  8. 最终答案
    答案为 dp[1][n]


举例说明
假设 n=3,键 K1, K2, K3 频率 p=[0.1, 0.2, 0.3],虚键 d0,d1,d2,d3 频率 q=[0.05, 0.1, 0.05, 0.1]。

计算 total[i][j]:
total[1][1] = p1 + q0 + q1 = 0.1 + 0.05 + 0.1 = 0.25
total[1][2] = p1+p2 + q0+q1+q2 = 0.1+0.2 + 0.05+0.1+0.05 = 0.5
total[1][3] = 0.1+0.2+0.3 + 0.05+0.1+0.05+0.1 = 0.9
total[2][2] = 0.2 + 0.1+0.05 = 0.35
total[2][3] = 0.2+0.3 + 0.1+0.05+0.1 = 0.75
total[3][3] = 0.3 + 0.05+0.1 = 0.45

然后区间DP:
dp[1][1] = 0.25
dp[2][2] = 0.35
dp[3][3] = 0.45

len=2:
dp[1][2] = min{
r=1: dp[1][0] + dp[2][2] + total[1][2] = 0 + 0.35 + 0.5 = 0.85
r=2: dp[1][1] + dp[3][2] + total[1][2] = 0.25 + 0 + 0.5 = 0.75
} = 0.75
dp[2][3] = min{
r=2: dp[2][1] + dp[3][3] + total[2][3] = 0 + 0.45 + 0.75 = 1.2
r=3: dp[2][2] + dp[4][3] + total[2][3] = 0.35 + 0 + 0.75 = 1.1
} = 1.1

len=3:
dp[1][3] = min{
r=1: dp[1][0] + dp[2][3] + total[1][3] = 0 + 1.1 + 0.9 = 2.0
r=2: dp[1][1] + dp[3][3] + total[1][3] = 0.25 + 0.45 + 0.9 = 1.6
r=3: dp[1][2] + dp[4][3] + total[1][3] = 0.75 + 0 + 0.9 = 1.65
} = 1.6

所以最小期望搜索成本为 1.6。


复杂度
状态数 O(n²),每个状态枚举根 O(n),总 O(n³)。可以用四边形不等式优化到 O(n²),但这里不展开。

最优二叉搜索树(Optimal Binary Search Tree, OBST)问题 题目描述 我们有一组有序的键(Key) K1 < K2 < ... < Kn ,以及对应的搜索频率(概率) p1, p2, ..., pn 。此外,我们还有一些不存在的键(即搜索目标不在这个集合中)的区间,可以视为一系列“虚键”(Dummy Keys) d0, d1, ..., dn ,其中 d0 表示所有小于 K1 的键, di 表示所有介于 Ki 和 K_{i+1} 之间的键( 1 <= i <= n-1 ), dn 表示所有大于 Kn 的键。每个虚键 di 也有一个被搜索到的频率(概率) q0, q1, ..., qn 。 所有频率满足: ∑ pi (1 <= i <= n) + ∑ qi (0 <= i <= n) = 1 。 目标:构造一棵二叉搜索树(BST),以这些键(实键和虚键)为结点,使得 总的期望搜索成本最小 。 期望搜索成本定义为:每个结点(实键或虚键)的 深度 + 1 (即从根到该结点的路径长度,根深度为0)乘以其频率,然后对所有结点求和。 用公式表达: 设结点 x 的频率为 freq(x) ,深度为 depth(x) ,则总成本 = ∑ freq(x) * (depth(x) + 1) 。 我们需要选择一种树的构造方式(即选取根,递归构造左右子树),最小化这个总成本。 解题思路 这是一个经典的区间动态规划问题,因为键是有序的,任何子树必然对应原键序列的一个连续子区间。 子问题定义 定义 dp[i][j] 为:由实键 Ki, K_{i+1}, ..., Kj 构成的 最优二叉搜索树 的最小期望搜索成本(这里 1 <= i <= j <= n )。 注意:子树中还包含虚键 d_{i-1}, d_i, ..., d_j (即这个子树对应的所有虚键)。 状态转移方程 对于子区间 [i, j] ,我们需要选择一个根 Kr ( i <= r <= j )。 如果选择 Kr 为根,则: 左子树包含实键 Ki, ..., K_{r-1} 以及虚键 d_{i-1}, ..., d_{r-1} 。 右子树包含实键 K_{r+1}, ..., Kj 以及虚键 d_r, ..., d_j 。 左子树的最小期望成本是 dp[i][r-1] (当 r-1 < i 时,左子树为空,只有虚键 d_{i-1} ,其成本为 q_{i-1} 乘深度增量,但这里我们先不考虑深度增量,稍后统一处理)。 更准确地说, dp[i][j] 表示的是:以这个子树为整体,其内部结点的深度是相对于这个子树的根来计算的。但在合并成大树时,子树的每个结点的深度会增加1(因为上面多了一层根)。 所以,当我们计算以 Kr 为根的子树的总成本时,它包括: 根 Kr 的成本: pr * 1 (根深度为0,所以深度+1=1)。 左子树的成本:如果左子树非空,其期望成本原本是 dp[i][r-1] ,但左子树中 每个结点的深度都增加了1 (因为现在它们的根 Kr 是它们的父结点),所以左子树的贡献应该是 dp[i][r-1] + 左子树中所有结点的频率之和(因为每个结点的深度+1,导致成本增加其频率值一次)。 右子树同理: dp[r+1][j] + 右子树中所有结点的频率之和。 为了计算方便,我们定义 w[i][j] 为区间 [i, j] 中所有实键频率 pi..pj 加上虚键频率 q_{i-1}..q_j 的总和。即: w[i][j] = ∑_{t=i}^{j} pt + ∑_{t=i-1}^{j} qt 。 那么,以 Kr 为根时,总成本为: cost = pr + (dp[i][r-1] + w[i][r-1]) + (dp[r+1][j] + w[r+1][j]) 这里 dp[i][r-1] 已经包含了左子树内部的成本, w[i][r-1] 是左子树所有结点的频率和,加上它是因为左子树每个结点的深度增加了1,成本增加其频率值。 右子树同理。 但注意: dp[i][r-1] 在 r-1 < i 时(左子树为空),我们应将其视为0,且 w[i][r-1] 应视为 q_{i-1} (即只有虚键 d_{i-1} 的频率)。 为了统一公式,我们约定:当 i > j 时, dp[i][j] = 0 , w[i][j] = q_j (?) 更常见且方便的做法是:预处理前缀和,并 在状态转移时直接计算 。 预处理频率和 令 sum[i][j] 表示从 i 到 j 的所有实键频率 + 从 i-1 到 j 的所有虚键频率之和。 但更简单的方法: 设 freq[1..n] 为实键频率 p1..pn , dummy[0..n] 为虚键频率 q0..qn 。 定义 total[i][j] = (∑_{t=i}^{j} pt) + (∑_{t=i-1}^{j} qt) 。 我们可以用前缀和快速计算: 令 P[i] = p1 + ... + pi , Q[j] = q0 + ... + qj ,则 total[i][j] = (P[j] - P[i-1]) + (Q[j] - Q[i-2]) (注意边界, i=1 时 Q[i-2] 为0)。 简化状态转移 观察公式: dp[i][j] = min_{r=i..j} { dp[i][r-1] + dp[r+1][j] + (total[i][j]) } 为什么? 因为 pr 包含在 total[i][j] 中,而 total[i][j] 正是整个区间 [i, j] 所有结点的频率和。 当根为 Kr 时,总成本 = 左子树成本 + 右子树成本 + 整个区间的频率和(因为根和所有子孙结点的深度都相对增加了,但这里 dp[i][r-1] 已经是左子树的最小成本(深度从0算),合并后,左子树的每个结点深度+1,所以成本增加左子树的频率和,这个频率和就是 total[i][r-1] ,同理右子树增加 total[r+1][j] 。 把这些增加量加上 dp[i][r-1] + dp[r+1][j] ,再加根的成本 pr ,会发现总和正好等于: dp[i][r-1] + dp[r+1][j] + total[i][j] 。 因为 total[i][j] = pr + total[i][r-1] + total[r+1][j] 。 最终状态转移方程 所以,递推式为: dp[i][j] = min_{r=i..j} { dp[i][r-1] + dp[r+1][j] } + total[i][j] 其中 dp[i][r-1] 在 r-1 < i 时取0, dp[r+1][j] 在 r+1 > j 时取0。 边界: dp[i][i-1] = 0 (表示空区间,只有一个虚键,但虚键成本已经在 total 中体现,所以这里为0)。 计算顺序 区间长度 len 从 1 到 n 递增。 对每个 len ,枚举起点 i ,计算 j = i+len-1 ,然后枚举根 r 从 i 到 j ,更新 dp[i][j] 。 初始化 当区间长度为1(即只有一个实键 Ki)时: dp[i][i] = min_{r=i..i} { dp[i][r-1] + dp[r+1][i] } + total[i][i] = dp[i][i-1] + dp[i+1][i] + total[i][i] 由于 dp[i][i-1] = 0 , dp[i+1][i] = 0 ,所以 dp[i][i] = total[i][i] 。 而 total[i][i] = pi + q_{i-1} + q_i 。 最终答案 答案为 dp[1][n] 。 举例说明 假设 n=3,键 K1, K2, K3 频率 p=[ 0.1, 0.2, 0.3],虚键 d0,d1,d2,d3 频率 q=[ 0.05, 0.1, 0.05, 0.1 ]。 计算 total[ i][ j ]: total[ 1][ 1 ] = p1 + q0 + q1 = 0.1 + 0.05 + 0.1 = 0.25 total[ 1][ 2 ] = p1+p2 + q0+q1+q2 = 0.1+0.2 + 0.05+0.1+0.05 = 0.5 total[ 1][ 3 ] = 0.1+0.2+0.3 + 0.05+0.1+0.05+0.1 = 0.9 total[ 2][ 2 ] = 0.2 + 0.1+0.05 = 0.35 total[ 2][ 3 ] = 0.2+0.3 + 0.1+0.05+0.1 = 0.75 total[ 3][ 3 ] = 0.3 + 0.05+0.1 = 0.45 然后区间DP: dp[ 1][ 1 ] = 0.25 dp[ 2][ 2 ] = 0.35 dp[ 3][ 3 ] = 0.45 len=2: dp[ 1][ 2 ] = min{ r=1: dp[ 1][ 0] + dp[ 2][ 2] + total[ 1][ 2 ] = 0 + 0.35 + 0.5 = 0.85 r=2: dp[ 1][ 1] + dp[ 3][ 2] + total[ 1][ 2 ] = 0.25 + 0 + 0.5 = 0.75 } = 0.75 dp[ 2][ 3 ] = min{ r=2: dp[ 2][ 1] + dp[ 3][ 3] + total[ 2][ 3 ] = 0 + 0.45 + 0.75 = 1.2 r=3: dp[ 2][ 2] + dp[ 4][ 3] + total[ 2][ 3 ] = 0.35 + 0 + 0.75 = 1.1 } = 1.1 len=3: dp[ 1][ 3 ] = min{ r=1: dp[ 1][ 0] + dp[ 2][ 3] + total[ 1][ 3 ] = 0 + 1.1 + 0.9 = 2.0 r=2: dp[ 1][ 1] + dp[ 3][ 3] + total[ 1][ 3 ] = 0.25 + 0.45 + 0.9 = 1.6 r=3: dp[ 1][ 2] + dp[ 4][ 3] + total[ 1][ 3 ] = 0.75 + 0 + 0.9 = 1.65 } = 1.6 所以最小期望搜索成本为 1.6。 复杂度 状态数 O(n²),每个状态枚举根 O(n),总 O(n³)。可以用四边形不等式优化到 O(n²),但这里不展开。