区间动态规划例题:合并石头的最低成本问题
题目描述
有 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(我们之前已经检查过可行性),所以它最终会被合并成一堆。
- 这里“尽可能地合并”是关键。
- 核心观察:每次合并操作会减少 (K-1) 堆石头(K堆合并成1堆)。初始有 N 堆,最终目标是 1 堆。因此,总共需要减少 (N-1) 堆石头。每次操作减少 (K-1) 堆。所以,一个必要的条件是
-
状态转移方程
- 思路:区间 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堆),那么合并成本就只是左右两个子区间成本的和,不需要加上整个区间的和,因为还没有发生“跨左右子区间”的合并操作。
- 思路:区间 DP 的经典思路是,考虑将大区间
-
算法步骤
- 步骤 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]
- 步骤 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。
- m=0:
[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。
- m=1:
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。
- m=0:
- 最低成本是 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]=5dp[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=14dp[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=12dp[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] )对于所有min[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是更稳妥的做法。
-