题目地址:
https://www.lintcode.com/problem/minimum-cost-to-merge-stones/description
给定一个长 n n n数组 A A A,再给定一个大于 1 1 1的正整数 k k k,每次允许将 A A A中长 k k k的区间合并为区间数之和,代价就是这个和。问最终要将 A A A合并为一个数的最低代价。如果不存在这种方案则返回 − 1 -1 −1。
显然对于 m m m个数,经过若干次合并之后所剩下的数的最小个数是确定的。每次合并之后,数字就减少了 k − 1 k-1 k−1个,所以合并能得到的剩余数的最小个数就是 n m o d ( k − 1 ) n \mod (k-1) nmod(k−1)。如果要求最后剩下 1 1 1个数,就要求 n m o d ( k − 1 ) = 1 n \mod (k-1)=1 nmod(k−1)=1,也就是 n − 1 ≡ 0 ( m o d ( k − 1 ) ) n-1\equiv 0 (\mod (k-1)) n−1≡0(mod(k−1))。这样可以事先排除掉不可能的情况。
接着用动态规划。设 f [ i ] [ j ] f[i][j] f[i][j]是合并 A [ i : j ] A[i:j] A[i:j]得到最少个数的数的情况下,消耗的最小代价。当 j − i + 1 < k j-i+1< k j−i+1<k的时候,此时无法进行合并,所以 f [ i ] [ j ] = 0 f[i][j]=0 f[i][j]=0。对于别的情况,也就是区间长度大于等于 k k k的情况,可以根据最后合并的时候的分割点分类讨论。我们可以只讨论最后合并的分割点左边全合并为 1 1 1个数的情况(因为所有的合并方案,最后剩下的数必然存在第一个数,所以所有方案都可以按照“第一个数是怎么来的”来分类,也就是可以按照使得左边合并为 1 1 1个数的分割点来分类),要使得左边合并为 1 1 1个数,就是要左边数的个数满足上面的方程 n − 1 ≡ 0 ( m o d ( k − 1 ) ) n-1\equiv 0 (\mod (k-1)) n−1≡0(mod(k−1)),即它模 k − 1 k-1 k−1必须等于 1 1 1。此外,如果区间长度 l = j − i + 1 l=j-i+1 l=j−i+1满足 l − 1 ≡ 0 ( m o d ( k − 1 ) ) l-1\equiv 0(\mod (k-1)) l−1≡0(mod(k−1)),还可以再合并一次(因为在这种情况下,长 l l l的区间最后合并只会剩下一个数,而左边合并完剩下 1 1 1个数,所以右边合并完最少剩下的数一定是 k − 1 k-1 k−1,所以还能合并 1 1 1次),所以还需要再加上 ∑ A [ i : j ] \sum A[i:j] ∑A[i:j]。综上,有 f [ i ] [ j ] = 1 j − i ≡ 0 ( m o d ( k − 1 ) ) ∑ A [ i : j ] + min i ≤ s ≤ j ∧ s − i ≡ 0 ( m o d ( k − 1 ) ) ( f [ i ] [ s ] + f [ s ] [ j ] ) f[i][j]=1_{j-i\equiv 0(\mod (k-1))}\sum A[i:j]+\min_{i\le s\le j\land s-i\equiv 0(\mod (k-1))}(f[i][s]+f[s][j]) f[i][j]=1j−i≡0(mod(k−1))∑A[i:j]+i≤s≤j∧s−i≡0(mod(k−1))min(f[i][s]+f[s][j])代码如下:
public class Solution {
/**
* @param stones:
* @param K:
* @return: return a integer
*/
public int mergeStones(int[] stones, int K) {
// write your code here
int n = stones.length;
// 排除掉不可能最后剩下一个数的情况
if ((n - 1) % (K - 1) != 0) {
return -1;
}
// 后面要频繁计算区间和,所以这里预处理一下前缀和
int[] preSum = new int[n + 1];
for (int i = 0; i < n; i++) {
preSum[i + 1] = preSum[i] + stones[i];
}
int[][] dp = new int[n][n];
// 总长度小于K的时候无法合并,代价就是0,不用算了直接略过
for (int len = K; len <= n; len++) {
// 枚举左端点
for (int l = 0; l + len - 1 < n; l++) {
// 算出右端点
int r = l + len - 1;
dp[l][r] = Integer.MAX_VALUE / 2;
// 枚举使得A[l:r]可以合并成1个数的分割点
for (int i = l; i < r; i += K - 1) {
dp[l][r] = Math.min(dp[l][r], dp[l][i] + dp[i + 1][r]);
}
// 如果总长度满足合并只剩一个数的条件,则可以再合并一次
if ((len - 1) % (K - 1) == 0) {
dp[l][r] += preSum[r + 1] - preSum[l];
}
}
}
return dp[0][n - 1];
}
}
时空复杂度 O ( n 3 / k ) O(n^3/k) O(n3/k),空间 O ( n 2 ) O(n^2) O(n2)。