区间动态规划例题:合并石头的最低成本问题
字数 4518 2025-11-10 20:24:12

区间动态规划例题:合并石头的最低成本问题

题目描述

有 N 堆石头排成一排,第 i 堆石头的重量为 stones[i]。你的目标是将所有石头合并成一堆。每次合并操作需要将连续的 K 堆石头合并为一堆,这次合并的成本是这 K 堆石头的总重量。你需要找出将所有石头合并成一堆的最低总成本。如果无法完成合并(例如,N=1, K=2,无法从1堆合并成1堆),则返回 -1。

解题过程

  1. 问题分析与可行性判断

    • 核心观察:每次合并操作会减少 (K-1) 堆石头(K堆合并成1堆)。初始有 N 堆,最终目标是 1 堆。因此,总共需要减少 (N-1) 堆石头。每次操作减少 (K-1) 堆。所以,一个必要的条件是 (N - 1) % (K - 1) == 0。如果不满足这个条件,则无法合并成一堆,直接返回 -1。
    • 状态定义:我们使用一个二维动态规划数组 dp[i][j]。它的含义是:将下标从 ij(包含两端)的这些石头堆,尽可能地合并,直到剩下的堆数不能再进行 K 堆合并为止(即剩下的堆数小于 K),这个过程中所花费的最低成本。
      • 这里“尽可能地合并”是关键。dp[i][j] 并不一定代表最终合并成一堆(当区间长度 len = j-i+1 不满足 (len-1) % (K-1) == 0 时,是无法合并成一堆的)。它表示的是在这个区间内,能合并多少次就合并多少次。
      • 最终,我们的目标是 dp[0][N-1],因为整个区间 [0, N-1] 的长度 N 满足 (N-1) % (K-1) == 0(我们之前已经检查过可行性),所以它最终会被合并成一堆。
  2. 状态转移方程

    • 思路:区间 DP 的经典思路是,考虑将大区间 [i, j] 分割成两个部分。我们希望找到一个分割点 m,使得 [i, m][m+1, j] 这两个区间各自内部合并后,再将其合并起来。
    • 关键点:为了能够将 [i, j] 区间进行最后一次 K 堆合并,我们需要保证在合并前,这个区间恰好是 K 堆。这 K 堆是由其内部的子区间合并而来的。
    • 如何保证? 我们在遍历分割点 m 时,不是以步长 1 移动,而是以步长 (K-1) 移动。即 mi 开始,每次增加 (K-1)。这样,区间 [i, m] 的长度 len1 = m - i + 1 满足 (len1 - 1) % (K-1) == 0 的可能性最大。这意味着 [i, m] 这个区间内部经过若干次合并后,最终恰好能合并成一堆。同理,我们希望 [m+1, j] 能合并成 (K-1) 堆,这样左右两部分加起来就是 K 堆,可以进行最后一次合并。
    • 更通用的状态转移
      • dp[i][j] = min(dp[i][j], dp[i][m] + dp[m+1][j]),其中 mi 遍历到 j-1,步长为 (K-1)
      • 合并成本:如果区间 [i, j] 的长度 len 满足 (len - 1) % (K-1) == 0,那么说明这个区间最终可以合并成一堆。这次“最终合并”的成本,就是整个区间 [i, j] 所有石头的总重量。因此,我们需要加上这个总重量:dp[i][j] += prefixSum[j+1] - prefixSum[i]。这里 prefixSum 是石头重量的前缀和数组,用于快速计算区间和。
      • 如果 (len - 1) % (K-1) != 0,说明这个区间目前无法进行最后一次合并(堆数不够K堆),那么合并成本就只是左右两个子区间成本的和,不需要加上整个区间的和,因为还没有发生“跨左右子区间”的合并操作。
  3. 算法步骤

    • 步骤 1:检查可行性
      if (N - 1) % (K - 1) != 0: return -1
    • 步骤 2:初始化
      • 创建二维数组 dp,大小为 N x N,初始值设为 0 或一个很大的数(如 inf)。如果初始化为 0,对于长度为 1 的区间 [i, i],合并成本为 0,这是正确的。
      • 计算前缀和数组 prefixSumprefixSum[0] = 0, prefixSum[i] = stones[0] + stones[1] + ... + stones[i-1]。这样区间 [i, j] 的和就是 prefixSum[j+1] - prefixSum[i]
    • 步骤 3:动态规划计算(按区间长度从小到大)
      • 遍历区间长度 length,从 2 到 N(因为长度为1的区间成本为0)。
      • 对于每个起始点 i,计算终点 j = i + length - 1,如果 j >= N 则跳过。
      • 初始化 dp[i][j] = inf(或者一个很大的数)。
      • 遍历分割点 m,从 ij-1步长为 (K-1)
        • dp[i][j] = min(dp[i][j], dp[i][m] + dp[m+1][j])
      • 检查是否需要加上区间和:如果 (length - 1) % (K - 1) == 0
        • dp[i][j] += prefixSum[j+1] - prefixSum[i]
    • 步骤 4:返回结果
      • return dp[0][N-1]
  4. 举例说明 (N=4, K=2)

    • stones = [3, 2, 4, 1]

    • 可行性: (4-1) % (2-1) = 3 % 1 = 0,可行。

    • 前缀和: [0, 3, 5, 9, 10]

    • 计算过程:

      • length=2:
        • [0,1]: m 从 0 到 0,步长1。dp[0][1] = dp[0][0] + dp[1][1] = 0+0=0。因为 (2-1)%1=1%1=0,所以加上区间和 prefixSum[2]-prefixSum[0]=5-0=5dp[0][1]=5
        • [1,2]: 同理,dp[1][2] = 0 + 0 + (9-3)=6
        • [2,3]: dp[2][3] = 0 + 0 + (10-9)=5
      • length=3:
        • [0,2]: m 从 0 到 1,步长1。
          • m=0: dp[0][0]+dp[1][2] = 0+6=6
          • m=1: dp[0][1]+dp[2][2] = 5+0=5 -> min=5
          • (3-1)%1=2%1=1 !=0,不加区间和dp[0][2]=5
        • [1,3]: m 从 1 到 2,步长1。
          • m=1: dp[1][1]+dp[2][3]=0+5=5
          • m=2: dp[1][2]+dp[3][3]=6+0=6 -> min=5
          • (3-1)%1=2%1=1 !=0,不加区间和dp[1][3]=5
      • length=4:
        • [0,3]: m 从 0 到 2,步长1。
          • m=0: dp[0][0]+dp[1][3]=0+5=5
          • m=1: dp[0][1]+dp[2][3]=5+5=10
          • m=2: dp[0][2]+dp[3][3]=5+0=5 -> min=5
          • (4-1)%1=3%1=0,加上区间和 prefixSum[4]-prefixSum[0]=10-0=10dp[0][3]=5+10=15
      • 最低成本是 15。合并过程可以是:先合并(3,2)成本5得5,堆为[5,4,1];再合并(4,1)成本5得5,堆为[5,5];最后合并(5,5)成本10得10,总成本5+5+10=20。但这不是最优的。最优是:先合并(2,4)成本6得6,堆为[3,6,1];再合并(3,6)成本9得9,堆为[9,1];最后合并(9,1)成本10得10,总成本6+9+10=25?等等,计算有误。
      • 重新验算最优解:方案1: (3,2)=5, [5,4,1]; (4,1)=5, [5,5]; (5,5)=10; 总5+5+10=20。方案2: (2,4)=6, [3,6,1]; (6,1)=7, [3,7]; (3,7)=10; 总6+7+10=23。方案3: (3,2)=5, [5,4,1]; (5,4)=9, [9,1]; (9,1)=10; 总5+9+10=24。我们的结果15比这些都小?说明我们的状态转移或理解有误。
    • 错误修正与深入理解:问题出在对 dp[i][j] 的定义和状态转移的理解上。在 K=2 的特殊情况下,它退化为经典的相邻石子合并问题。让我们用经典的相邻石子合并DP来算一下:dp[i][j] = min(dp[i][k] + dp[k+1][j]) + sum(i,j),其中k任意分割。

      • dp[0][1]=5, dp[1][2]=6, dp[2][3]=5
      • dp[0][2] = min(dp[0][0]+dp[1][2], dp[0][1]+dp[2][2]) + sum(0,2)=min(0+6, 5+0)+9=min(6,5)+9=14
      • dp[1][3] = min(dp[1][1]+dp[2][3], dp[1][2]+dp[3][3]) + sum(1,3)=min(0+5,6+0)+7=min(5,6)+7=12
      • dp[0][3] = min(dp[0][0]+dp[1][3], dp[0][1]+dp[2][3], dp[0][2]+dp[3][3]) + sum(0,3) = min(0+12, 5+5, 14+0) + 10 = min(12,10,14) + 10 = 20。这才是正确结果20。
    • 修正通用算法:对于 K=2,每次合并两堆,相当于任意分割。对于 K>2,我们之前“步长为K-1”的遍历方式,是为了保证子区间能被合并成整数堆,以便进行K堆合并。但dp[i][j]的定义应该是:将[i,j]合并成不超过(K-1)堆的最小成本(因为如果合并成K堆,会立刻被合并掉)。而最后一次合并的成本是整个区间的和。一个更准确且通用的状态转移方程是:
      dp[i][j] = min( dp[i][j], dp[i][m] + dp[m+1][j] ) 对于所有 m in [i, j-1]
      如果 (j-i) % (K-1) == 0,则说明这个区间最终可以合并成一堆,需要加上最后一次合并的成本(即区间和):dp[i][j] += sum(i, j)
      这个修正后的算法对于 K=2 就退化成了经典的相邻合并问题。我们重新用这个修正算法计算 K=2, N=4 的例子,会发现结果就是正确的20。因此,最终的算法步骤中,遍历分割点 m 时,步长应为1,而不是K-1。 之前“步长为K-1”是一个优化,但在理解上容易产生偏差。为了保证正确性和通用性,使用步长1是更稳妥的做法。

区间动态规划例题:合并石头的最低成本问题 题目描述 有 N 堆石头排成一排,第 i 堆石头的重量为 stones[ i ]。你的目标是将所有石头合并成一堆。每次合并操作需要将连续的 K 堆石头合并为一堆,这次合并的成本是这 K 堆石头的总重量。你需要找出将所有石头合并成一堆的最低总成本。如果无法完成合并(例如,N=1, K=2,无法从1堆合并成1堆),则返回 -1。 解题过程 问题分析与可行性判断 核心观察 :每次合并操作会减少 (K-1) 堆石头(K堆合并成1堆)。初始有 N 堆,最终目标是 1 堆。因此,总共需要减少 (N-1) 堆石头。每次操作减少 (K-1) 堆。所以,一个必要的条件是 (N - 1) % (K - 1) == 0 。如果不满足这个条件,则无法合并成一堆,直接返回 -1。 状态定义 :我们使用一个二维动态规划数组 dp[i][j] 。它的含义是:将下标从 i 到 j (包含两端)的这些石头堆, 尽可能地合并 ,直到剩下的堆数不能再进行 K 堆合并为止(即剩下的堆数小于 K),这个过程中所花费的最低成本。 这里“尽可能地合并”是关键。 dp[i][j] 并不一定代表最终合并成一堆(当区间长度 len = j-i+1 不满足 (len-1) % (K-1) == 0 时,是无法合并成一堆的)。它表示的是在这个区间内,能合并多少次就合并多少次。 最终,我们的目标是 dp[0][N-1] ,因为整个区间 [0, N-1] 的长度 N 满足 (N-1) % (K-1) == 0 (我们之前已经检查过可行性),所以它最终会被合并成一堆。 状态转移方程 思路 :区间 DP 的经典思路是,考虑将大区间 [i, j] 分割成两个部分。我们希望找到一个分割点 m ,使得 [i, m] 和 [m+1, j] 这两个区间 各自内部合并后 ,再将其合并起来。 关键点 :为了能够将 [i, j] 区间进行最后一次 K 堆合并,我们需要保证在合并前,这个区间恰好是 K 堆。这 K 堆是由其内部的子区间合并而来的。 如何保证? 我们在遍历分割点 m 时,不是以步长 1 移动,而是以步长 (K-1) 移动。即 m 从 i 开始,每次增加 (K-1) 。这样,区间 [i, m] 的长度 len1 = m - i + 1 满足 (len1 - 1) % (K-1) == 0 的可能性最大。这意味着 [i, m] 这个区间内部经过若干次合并后,最终 恰好能合并成一堆 。同理,我们希望 [m+1, j] 能合并成 (K-1) 堆,这样左右两部分加起来就是 K 堆,可以进行最后一次合并。 更通用的状态转移 : dp[i][j] = min(dp[i][j], dp[i][m] + dp[m+1][j]) ,其中 m 从 i 遍历到 j-1 ,步长为 (K-1) 。 合并成本 :如果区间 [i, j] 的长度 len 满足 (len - 1) % (K-1) == 0 ,那么说明这个区间最终可以合并成一堆。这次“最终合并”的成本,就是整个区间 [i, j] 所有石头的总重量。因此,我们需要加上这个总重量: dp[i][j] += prefixSum[j+1] - prefixSum[i] 。这里 prefixSum 是石头重量的前缀和数组,用于快速计算区间和。 如果 (len - 1) % (K-1) != 0 ,说明这个区间目前无法进行最后一次合并(堆数不够K堆),那么合并成本就只是左右两个子区间成本的和,不需要加上整个区间的和,因为还没有发生“跨左右子区间”的合并操作。 算法步骤 步骤 1:检查可行性 if (N - 1) % (K - 1) != 0: return -1 步骤 2:初始化 创建二维数组 dp ,大小为 N x N ,初始值设为 0 或一个很大的数(如 inf )。如果初始化为 0,对于长度为 1 的区间 [i, i] ,合并成本为 0,这是正确的。 计算前缀和数组 prefixSum , prefixSum[0] = 0 , prefixSum[i] = stones[0] + stones[1] + ... + stones[i-1] 。这样区间 [i, j] 的和就是 prefixSum[j+1] - prefixSum[i] 。 步骤 3:动态规划计算(按区间长度从小到大) 遍历区间长度 length ,从 2 到 N(因为长度为1的区间成本为0)。 对于每个起始点 i ,计算终点 j = i + length - 1 ,如果 j >= N 则跳过。 初始化 dp[i][j] = inf (或者一个很大的数)。 遍历分割点 m ,从 i 到 j-1 , 步长为 (K-1) 。 dp[i][j] = min(dp[i][j], dp[i][m] + dp[m+1][j]) 检查是否需要加上区间和 :如果 (length - 1) % (K - 1) == 0 : dp[i][j] += prefixSum[j+1] - prefixSum[i] 步骤 4:返回结果 return dp[0][N-1] 举例说明 (N=4, K=2) stones = [ 3, 2, 4, 1 ] 可行性: (4-1) % (2-1) = 3 % 1 = 0,可行。 前缀和: [ 0, 3, 5, 9, 10 ] 计算过程 : length=2 : [0,1] : m 从 0 到 0,步长1。 dp[0][1] = dp[0][0] + dp[1][1] = 0+0=0 。因为 (2-1)%1=1%1=0,所以加上区间和 prefixSum[2]-prefixSum[0]=5-0=5 。 dp[0][1]=5 。 [1,2] : 同理, dp[1][2] = 0 + 0 + (9-3)=6 。 [2,3] : dp[2][3] = 0 + 0 + (10-9)=5 。 length=3 : [0,2] : m 从 0 到 1,步长1。 m=0: dp[0][0]+dp[1][2] = 0+6=6 m=1: dp[0][1]+dp[2][2] = 5+0=5 -> min=5 (3-1)%1=2%1=1 !=0, 不加区间和 。 dp[0][2]=5 。 [1,3] : m 从 1 到 2,步长1。 m=1: dp[1][1]+dp[2][3]=0+5=5 m=2: dp[1][2]+dp[3][3]=6+0=6 -> min=5 (3-1)%1=2%1=1 !=0, 不加区间和 。 dp[1][3]=5 。 length=4 : [0,3] : m 从 0 到 2,步长1。 m=0: dp[0][0]+dp[1][3]=0+5=5 m=1: dp[0][1]+dp[2][3]=5+5=10 m=2: dp[0][2]+dp[3][3]=5+0=5 -> min=5 (4-1)%1=3%1=0, 加上区间和 prefixSum[4]-prefixSum[0]=10-0=10 。 dp[0][3]=5+10=15 。 最低成本是 15。合并过程可以是:先合并(3,2)成本5得5,堆为[ 5,4,1];再合并(4,1)成本5得5,堆为[ 5,5];最后合并(5,5)成本10得10,总成本5+5+10=20。 但这不是最优的 。最优是:先合并(2,4)成本6得6,堆为[ 3,6,1];再合并(3,6)成本9得9,堆为[ 9,1 ];最后合并(9,1)成本10得10,总成本6+9+10=25?等等,计算有误。 重新验算最优解 :方案1: (3,2)=5, [ 5,4,1]; (4,1)=5, [ 5,5]; (5,5)=10; 总5+5+10=20。方案2: (2,4)=6, [ 3,6,1]; (6,1)=7, [ 3,7]; (3,7)=10; 总6+7+10=23。方案3: (3,2)=5, [ 5,4,1]; (5,4)=9, [ 9,1 ]; (9,1)=10; 总5+9+10=24。我们的结果15比这些都小?说明我们的状态转移或理解有误。 错误修正与深入理解 :问题出在对 dp[i][j] 的定义和状态转移的理解上。在 K=2 的特殊情况下,它退化为经典的相邻石子合并问题。让我们用经典的相邻石子合并DP来算一下: dp[i][j] = min(dp[i][k] + dp[k+1][j]) + sum(i,j) ,其中k任意分割。 dp[0][1]=5 , dp[1][2]=6 , dp[2][3]=5 dp[0][2] = min(dp[0][0]+dp[1][2], dp[0][1]+dp[2][2]) + sum(0,2)=min(0+6, 5+0)+9=min(6,5)+9=14 dp[1][3] = min(dp[1][1]+dp[2][3], dp[1][2]+dp[3][3]) + sum(1,3)=min(0+5,6+0)+7=min(5,6)+7=12 dp[0][3] = min(dp[0][0]+dp[1][3], dp[0][1]+dp[2][3], dp[0][2]+dp[3][3]) + sum(0,3) = min(0+12, 5+5, 14+0) + 10 = min(12,10,14) + 10 = 20 。这才是正确结果20。 修正通用算法 :对于 K=2 ,每次合并两堆,相当于任意分割。对于 K>2 ,我们之前“步长为K-1”的遍历方式,是为了保证子区间能被合并成整数堆,以便进行K堆合并。但 dp[i][j] 的定义应该是:将 [i,j] 合并成 不超过 (K-1) 堆的最小成本(因为如果合并成K堆,会立刻被合并掉)。而最后一次合并的成本是整个区间的和。一个更准确且通用的状态转移方程是: dp[i][j] = min( dp[i][j], dp[i][m] + dp[m+1][j] ) 对于所有 m in [i, j-1] 。 如果 (j-i) % (K-1) == 0 ,则说明这个区间最终可以合并成一堆,需要加上最后一次合并的成本(即区间和): dp[i][j] += sum(i, j) 。 这个修正后的算法对于 K=2 就退化成了经典的相邻合并问题。我们重新用这个修正算法计算 K=2, N=4 的例子,会发现结果就是正确的20。 因此,最终的算法步骤中,遍历分割点 m 时,步长应为1,而不是K-1。 之前“步长为K-1”是一个优化,但在理解上容易产生偏差。为了保证正确性和通用性,使用步长1是更稳妥的做法。