有 N 堆石头排成一排,第 i 堆中有 stones[i] 块石头。
每次移动(move)需要将连续的 K 堆石头合并为一堆,而这个移动的成本为这 K 堆石头的总数。
找出把所有石头合并成一堆的最低成本。如果不可能,返回 -1 。
示例 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,这是可能的最小值。
提示:
1 <= stones.length <= 302 <= K <= 301 <= stones[i] <= 100- class Solution {
- public int mergeStones(int[] stones, int k) {
- // 过滤无效参数
- if (stones == null || stones.length == 0 || k == 0) {
- return -1;
- }
- // 数组长度
- int n = stones.length;
- // 这个条件很重要,存在一些数组长度n和k的组合压根就合并不成1个,直接返回-1
- if ((n - 1) % (k - 1) > 0) {
- return -1;
- }
- // 构造前缀和数组,加速求任意区间累加和的速度
- int[] preSum = new int[n];
- preSum[0] = stones[0];
- for (int i = 1; i < n; i++) {
- preSum[i] = preSum[i - 1] + stones[i];
- }
- // 构造三维dp缓存 第三维下标在递归中最多赋值就是k,所以长度开到k+1即可
- int[][][] dp = new int[n][n][k + 1];
-
- // 将数组0~n-1范围合并成1份,每一次将相邻的k个合并
- return process(stones, 0, n - 1, 1, k, preSum, dp);
- }
-
- // arr[l..r] 一定要弄出p份,返回最低代价
- // 固定参数是k和preSum,preSum是用来加快求累加和速度的
- public int process(int[] stones, int l, int r, int p, int k, int[] preSum, int[][][] dp) {
- // 先看是否有缓存,有的话直接返回
- if (dp[l][r][p] != 0) {
- return dp[l][r][p];
- }
-
- // basecase 如果此时l==r,说明本身就是1个数了,如果要划分的目标p也是1,那么就不需要再合并了,代价直接返回0,如果目标p不是1,但是因为此时l~r范围只有一个数了,肯定是合并不出来p目标了,直接返回-1
- if (l == r) {
- dp[l][r][p] = p == 1 ? 0 : -1;
- return dp[l][r][p];
- }
-
- // 说明当前就是最后一次合并了,合并完就要求是1个,如果无法一次合并成1个,就返回-1
- if (p == 1) {
- // 当前是最后一次合并了,如果要想是最后一次合并,此时合并完只能由k个数,所以要判断当前的状况能不能合并出k份
- int next = process(stones, l, r, k, k, preSum, dp);
- // 如果不能合并成k份,那么一定就无法最后合并成一份,直接返回-1
- if (next == -1) {
- dp[l][r][p] = -1;
- return dp[l][r][p];
- // 可以合并出三份
- } else {
- // 代价就是合并出三分所需要的总代价,加上l~r范围的累加和,因为将3份数合并,合并代价就是这三部分的累加和
- dp[l][r][p] = next + (l == 0 ? preSum[r] : preSum[r] - preSum[l - 1]);
- return dp[l][r][p];
- }
- // 当前目标并不是分成1份,说明合并还没有到最后
- } else {
- // 先将最小代价初始化为系统最大
- int min = Integer.MAX_VALUE;
- // 开始遍历,尝试所有可能的左右部分划分情况,我们就规定左部分划分出1份,右部分划分出p-1份
- // 划分遍历步长就是k - 1,l最多到r - (p - 1),因为要保证右部分至少有p-1个数,不然就不可能划分出p-1个目标了
- for (int mid = l; mid <= r - (p - 1); mid += (k - 1)) {
- // 看左部分合并成1份最低代价
- int leftCost = process(stones, l, mid, 1, k, preSum, dp);
- int rightCost = -1;
- // 如果左部分确实可以划分出1份,就去看右部分能不能划分出p-1份。剪枝
- if (leftCost != -1) {
- // 右部分合并出p-1份的最低代价
- rightCost = process(stones, mid + 1, r, p - 1, k, preSum, dp);
- }
- // 如果右部分也可以划分出来,则计算这两部分的合并代价,是否是l~r范围所有划分情况中最低的
- if (rightCost != -1) {
- // 这里我们就是要将l~r范围合并出p份,并不会对这p份再做合并,所以这里的代价就是leftCost+rightCost,不需要加上l~r的累加和
- min = Math.min(min, leftCost + rightCost);
- }
- }
- // 返回所有划分中合并代价最小的值
- dp[l][r][p] = min;
- return dp[l][r][p];
- }
- }
- }
定义process函数:让L...R一定变为P份,返回合并成P份的最少代价是啥。
枚举最左边的一份是什么范围,这样就可以递归出左部分和右部分的最低代价。我们将每一层递归的所有可能的范围都枚举尝试一遍,最后就能求出来结果。
这里我们就认为左边搞成1份,右边搞成P-1份,因为总归是存在一种划分能让左边搞出来1份,我们把所有可能的左部分范围都尝试一遍,就能得到全部左部分和右部分的情况。而且必须要来一个左右部分,因为只有划分为1份的时候才是递归出口,所以我们就要想到让左边固定搞1份,这样才能让整个递归过程结束返回。