1. 题目链接
1000. 合并石头的最低成本 - 力扣(LeetCode)
2. 题目描述
有 n
堆石头排成一排,第 i
堆中有 stones[i]
块石头。
每次 移动 需要将 连续的 k
堆石头合并为一堆,而这次移动的成本为这 k
堆中石头的总数。
返回把所有石头合并成一堆的最低成本。如果无法合并成一堆,返回 -1
。
3. 题目示例
示例 1 :
输入:stones = [3,2,4,1], K = 2
输出:20
解释:
从 [3, 2, 4, 1] 开始。
合并 [3, 2],成本为 5,剩下 [5, 4, 1]。
合并 [4, 1],成本为 5,剩下 [5, 5]。
合并 [5, 5],成本为 10,剩下 [10]。
总成本 20,这是可能的最小值。
示例 2 :
输入:stones = [3,2,4,1], K = 3
输出:-1
解释:任何合并操作后,都会剩下 2 堆,我们无法再进行合并。所以这项任务是不可能完成的。
示例 3 :
输入:stones = [3,5,1,2,6], K = 3
输出:25
解释:
从 [3, 5, 1, 2, 6] 开始。
合并 [5, 1, 2],成本为 8,剩下 [3, 8, 6]。
合并 [3, 8, 6],成本为 17,剩下 [17]。
总成本 25,这是可能的最小值。
4. 解题思路
- 问题理解:
- 给定n堆石头,每次合并连续的k堆,成本为这些石头的总和
- 目标是将所有石头合并成一堆的最小总成本
- 如果不能合并成一堆,返回-1
- 关键思路:
- 检查可行性:(n-1)必须能被(k-1)整除
- 使用前缀和数组快速计算区间和
- 记忆化搜索避免重复计算
- 分治思想:将大问题分解为子问题
- 动态规划设计:
- 状态定义:dfs(i,j)表示合并区间[i,j]的最小成本
- 状态转移:尝试所有可能的分割点m(步长k-1)
- 合并条件:当区间长度减1能被k-1整除时才能合并
- 计算顺序:
- 自顶向下递归计算
- 使用记忆化存储中间结果
- 优先处理小区间,逐步扩大
5. 题解代码
class Solution {
private int[][] memo; // 记忆化数组,存储已计算过的区间结果
private int[] s; // 前缀和数组
private int k; // 每次合并的堆数
public int mergeStones(int[] stones, int k) {
int n = stones.length;
// 检查是否可以合并成一堆:(n-1)必须能被(k-1)整除
if ((n - 1) % (k - 1) > 0)
return -1;
// 初始化前缀和数组
s = new int[n + 1];
for (int i = 0; i < n; i++)
s[i + 1] = s[i] + stones[i]; // s[i]表示前i个元素的和
this.k = k;
memo = new int[n][n];
// 初始化记忆数组为-1,表示未计算
for (int i = 0; i < n; ++i)
Arrays.fill(memo[i], -1);
// 从整个数组范围开始递归计算
return dfs(0, n - 1);
}
private int dfs(int i, int j) {
// 基本情况:只有一个堆,无需合并
if (i == j) return 0;
// 如果已经计算过,直接返回结果
if (memo[i][j] != -1) return memo[i][j];
int res = Integer.MAX_VALUE;
// 尝试所有可能的分割点m(步长为k-1)
for (int m = i; m < j; m += k - 1)
res = Math.min(res, dfs(i, m) + dfs(m + 1, j));
// 如果当前区间可以合并成一堆,加上合并成本
if ((j - i) % (k - 1) == 0)
res += s[j + 1] - s[i];
// 存储结果并返回
return memo[i][j] = res;
}
}
6. 复杂度分析
时间复杂度:O(n³/k)
- 递归树深度:O(n/k)
- 每层计算量:O(n²)
- 记忆化确保每个状态只计算一次
空间复杂度:O(n²)
- 记忆化数组memo[n][n]
- 前缀和数组s[n+1]
- 递归调用栈深度O(n)