合并相邻石子堆的最小得分问题(每次合并的得分为两堆石子数量之和的平方,最小化总得分)
题目描述
给定一个长度为 n 的数组 stones,其中 stones[i] 表示第 i 堆石子的数量。你可以重复进行以下操作,直到只剩下一堆石子:选择相邻的两堆石子,将它们合并成一堆,合并的成本(也称为得分)为这两堆石子的数量之和的平方。你的目标是找出一种合并顺序,使得所有合并操作的总得分最小。返回这个最小总得分。
示例:
- 输入:
stones = [1, 2, 3, 4] - 输出:
116 - 解释:
- 一种最优合并顺序是:先合并
1和2(得分(1+2)^2 = 9),得到[3, 3, 4]。 - 然后合并
3和3(得分(3+3)^2 = 36),得到[6, 4]。 - 最后合并
6和4(得分(6+4)^2 = 100),得到[10]。 - 总得分 =
9 + 36 + 100 = 145。但这是其中一种顺序,实际最小总得分可以通过动态规划找到更优的116(具体顺序稍后推导)。
- 一种最优合并顺序是:先合并
问题分析
这个问题与经典的“合并石子”问题类似,但经典的合并成本通常是两堆石子数量之和(线性成本),而这里是平方成本。平方成本使得合并的“代价”随着石子数量的增加而急剧增大,因此我们需要更谨慎地选择合并顺序,避免过早合并大堆石子。
由于每次只能合并相邻的两堆,这符合区间动态规划的特征:我们将原问题分解为对某个区间 [i, j] 的石子进行合并的子问题。
解题思路
-
状态定义:
设dp[i][j]表示将区间[i, j]内的所有石子合并成一堆的最小总得分。
其中0 <= i <= j < n。 -
状态转移:
考虑区间[i, j]的最后一次合并。最后一次合并会将[i, j]分成两个子区间:[i, k]和[k+1, j],其中i <= k < j。在这最后一次合并之前,左右两个子区间已经各自合并成了一堆石子,其石子数量分别为sum(i, k)和sum(k+1, j),其中sum(l, r)表示从第l堆到第r堆的石子总数。最后一次合并的得分为(sum(i, k) + sum(k+1, j))^2,即(sum(i, j))^2。因此,状态转移方程为:
\[ dp[i][j] = \min_{i \le k < j} \left( dp[i][k] + dp[k+1][j] + (sum(i, j))^2 \right) \]
其中 sum(i, j) 是区间 [i, j] 的石子总数,可以通过前缀和数组在 O(1) 时间内计算。
- 初始化:
当区间长度为 1 时,即i == j,不需要合并,得分为 0。所以:
\[ dp[i][i] = 0 \]
-
计算顺序:
区间 DP 通常按区间长度从小到大计算。我们首先计算所有长度为 2 的区间,然后长度 3,依此类推,直到长度为 n。 -
答案:
最终答案为dp[0][n-1]。
逐步演算(以示例 stones = [1, 2, 3, 4] 为例)
-
计算前缀和:
为了方便计算区间和,先计算前缀和数组prefix,使得prefix[i]表示前 i 堆石子的总数(prefix[0] = 0)。stones = [1, 2, 3, 4]prefix = [0, 1, 3, 6, 10]- 区间和
sum(i, j) = prefix[j+1] - prefix[i]。
-
初始化:
dp[i][i] = 0对所有 i。 -
长度 = 2:
- 区间
[0,1]:sum = 1+2 = 3,dp[0][1] = dp[0][0] + dp[1][1] + 3^2 = 0 + 0 + 9 = 9。 - 区间
[1,2]:sum = 2+3 = 5,dp[1][2] = 0 + 0 + 25 = 25。 - 区间
[2,3]:sum = 3+4 = 7,dp[2][3] = 0 + 0 + 49 = 49。
- 区间
-
长度 = 3:
- 区间
[0,2]:可能的分割点k=0或k=1。k=0:左区间[0,0],右区间[1,2],sum(0,2)=1+2+3=6,得分 =dp[0][0] + dp[1][2] + 6^2 = 0 + 25 + 36 = 61。k=1:左区间[0,1],右区间[2,2],得分 =dp[0][1] + dp[2][2] + 6^2 = 9 + 0 + 36 = 45。- 最小值为
45,所以dp[0][2] = 45。
- 区间
[1,3]:类似计算。k=1:得分 =dp[1][1] + dp[2][3] + (2+3+4)^2 = 0 + 49 + 81 = 130。k=2:得分 =dp[1][2] + dp[3][3] + 9^2 = 25 + 0 + 81 = 106。- 最小值为
106,所以dp[1][3] = 106。
- 区间
-
长度 = 4:
- 区间
[0,3]:分割点k=0,1,2。sum(0,3) = 1+2+3+4 = 10,平方为 100。k=0:得分 =dp[0][0] + dp[1][3] + 100 = 0 + 106 + 100 = 206。k=1:得分 =dp[0][1] + dp[2][3] + 100 = 9 + 49 + 100 = 158。k=2:得分 =dp[0][2] + dp[3][3] + 100 = 45 + 0 + 100 = 145。- 最小值是
145?但我们之前题目描述中说最小总得分是 116,这里出现了不一致。实际上,这是因为在长度=4时,我们漏掉了一种可能性:可能先合并中间的两堆,再合并两边,但我们的状态定义dp[i][j]已经涵盖了所有合并顺序,因为最后一次合并一定是将两个子区间合并,而这两个子区间内部的最优解已经通过 dp 求出。然而,我们计算出的 145 并不是最优的。让我们重新检查。
- 区间
-
重新审视状态转移:
我们的状态转移方程假设最后一次合并的得分是(sum(i,j))^2,这没错。但问题在于,在计算dp[0][3]时,我们考虑的k分割点对应的是最后一次合并前,左区间是[0,k]合并成的一堆,右区间是[k+1,3]合并成的一堆。然而,在平方成本下,合并顺序会影响子区间合并成单堆时的石子总数吗? 会的!因为子区间内部合并时,每次合并的得分取决于当时的两堆石子数量之和的平方,而不同的合并顺序会导致子区间在最终合并前的“总石子数”虽然相同(都是sum(i,k)),但子区间内部合并的总得分可能不同。我们的 dp 状态已经考虑了子区间内部合并的最小得分,所以状态转移是正确的。那为什么我们算出 145,但题目示例说最优是 116?其实题目描述中的示例解释给出的 145 是一种合并顺序的得分,但并不是最小得分。让我们用 DP 找出真正的顺序:
实际上,对于
stones = [1,2,3,4],最小总得分确实是 116。对应的合并顺序是:- 先合并 2 和 3:得分
(2+3)^2 = 25,数组变为[1, 5, 4]。 - 再合并 1 和 5:得分
(1+5)^2 = 36,数组变为[6, 4]。 - 最后合并 6 和 4:得分
(6+4)^2 = 100,数组变为[10]。 - 总得分 =
25 + 36 + 100 = 161?等等,这也不是 116。我意识到我犯了一个错误。
实际上,平方成本下的最优顺序往往不是从中间开始合并。让我们用 DP 表格正确计算:
正确计算:
- 长度=2 我们已经算过:
dp[0][1]=9,dp[1][2]=25,dp[2][3]=49。 - 长度=3:
[0,2]:sum=6- k=0:
dp[0][0]+dp[1][2]+36 = 0+25+36=61 - k=1:
dp[0][1]+dp[2][2]+36 = 9+0+36=45 - 所以
dp[0][2]=45
- k=0:
[1,3]:sum=9- k=1:
dp[1][1]+dp[2][3]+81 = 0+49+81=130 - k=2:
dp[1][2]+dp[3][3]+81 = 25+0+81=106 - 所以
dp[1][3]=106
- k=1:
- 长度=4:
[0,3],sum=10,平方=100。- k=0:
dp[0][0]+dp[1][3]+100 = 0+106+100=206 - k=1:
dp[0][1]+dp[2][3]+100 = 9+49+100=158 - k=2:
dp[0][2]+dp[3][3]+100 = 45+0+100=145 - 最小值是 145。
- k=0:
所以 DP 算出最小总得分是 145,不是 116。但题目描述中说输出 116,这可能是因为我最初给的示例解释有误。实际上,对于平方成本,最小总得分就是 145。让我们验证另一种顺序:先合并 3 和 4(得分 49),得
[1,2,7];再合并 1 和 2(得分 9),得[3,7];最后合并 3 和 7(得分 100),总得分 158。先合并 1 和 2(得分 9),得[3,3,4];再合并 3 和 3(得分 36),得[6,4];最后合并 6 和 4(得分 100),总得分 145。没有更小的了。因此,对于示例 [1,2,3,4],最小总得分是 145。题目描述中的 116 可能是笔误,或者是另一种成本计算方式(比如线性成本)。在标准“合并相邻石子堆的最小得分问题(平方成本)”中,我们 DP 给出的 145 是正确的。
- 先合并 2 和 3:得分
算法步骤总结
- 计算前缀和数组
prefix,用于快速计算区间和。 - 初始化
dp数组,大小为n x n,所有元素设为 0(因为长度为 1 时得分为 0)。 - 按区间长度
len从 2 到 n 遍历:- 对于每个起点
i,终点j = i + len - 1(j < n):- 计算区间和
total = prefix[j+1] - prefix[i]。 - 初始化
dp[i][j]为一个很大的数。 - 对于每个分割点
k从i到j-1:- 计算
score = dp[i][k] + dp[k+1][j] + total * total。 - 更新
dp[i][j] = min(dp[i][j], score)。
- 计算
- 计算区间和
- 对于每个起点
- 返回
dp[0][n-1]。
复杂度分析
- 时间复杂度:O(n³),因为有三层循环:区间长度、起点、分割点。
- 空间复杂度:O(n²),用于存储 dp 表。
关键点
- 平方成本使得合并大堆石子代价很高,因此 DP 会倾向于让大堆石子尽量晚合并。
- 状态转移方程中的
(sum(i,j))^2是固定的,与分割点无关,但子问题的得分dp[i][k]和dp[k+1][j]会随着分割点变化。 - 这个问题是区间 DP 的典型应用,通过枚举区间最后一步的合并方式来分解问题。
扩展思考
如果成本改为两堆石子数量之和的立方,或者任意函数 f(sum),只需在状态转移中将平方项替换为相应的函数即可。另外,如果石子堆是环形的(即首尾相连),可以通过将数组复制一倍,然后对每个长度为 n 的区间进行 DP,取最小值,时间复杂度会升至 O(n³)。