题目地址:
给定一个数组,代表一列石头的重量。允许对相邻的两个石头合并,花费的能量是两个石头的重量之和。合并后,这两堆石头化成一堆,位置仍然在原位。现在需要将这列石头两两合并,直到合并成为一个石头,问所花费的最小能量是多少。
这是一道区间DP问题,我们需要用区间表示状态来递推。设
s
s
s是表示石头重量的数组,设
f
[
i
]
[
j
]
f[i][j]
f[i][j]是将
s
[
i
,
.
.
.
,
j
]
s[i,...,j]
s[i,...,j]的石头合并成一个所需的最少能量,那么这个最少能量按照最后一步合并的分界线可以分为以下几种情况:
1、最后一步是
s
[
i
]
s[i]
s[i]和
s
[
i
+
1
,
.
.
.
,
j
]
s[i+1,...,j]
s[i+1,...,j]合并,此时需要的最少能量是
f
[
i
+
1
]
[
j
]
+
∑
k
=
i
j
s
[
k
]
f[i+1][j]+\sum_{k=i}^{j}s[k]
f[i+1][j]+∑k=ijs[k],第一项是合并后者需要的能量,第二项是最后一次合并所需要的能量。
s
[
i
]
s[i]
s[i]自己只有一个石头,不需要合并;
2、最后一步是
s
[
i
:
i
+
1
]
s[i:i+1]
s[i:i+1]和
s
[
i
+
2
,
.
.
.
,
j
]
s[i+2,...,j]
s[i+2,...,j]合并,此时需要的最少能量是
f
[
i
]
[
i
+
1
]
+
f
[
i
+
2
]
[
j
]
+
∑
k
=
i
j
s
[
k
]
f[i][i+1]+f[i+2][j]+\sum_{k=i}^{j}s[k]
f[i][i+1]+f[i+2][j]+∑k=ijs[k],第一项是合并前两个石头需要的能量,第二项是合并后半区间石头需要的能量,最后一项是最后一次合并需要的能量;
…
从上面我们可以看出一个规律, f [ i ] [ j ] f[i][j] f[i][j]应该是所有区间分法中前一半区间的石头合并需要的总能量加上后半区间的总能量再加上最后一次合并需要的能量。
法1:记忆化搜索。代码如下:
public class Solution {
/**
* @param A: An integer array
* @return: An integer
*/
public int stoneGame(int[] A) {
// write your code here
// 如果没有石头,就不需要耗费能量,返回0
if (A == null || A.length == 0) {
return 0;
}
// 开一个记忆化数组
int[][] dp = new int[A.length][A.length];
// 开一个数组算前缀和,可以O(1)时间返回区间和
// A[i,...,j]区间和等于prefixSum[j + 1] - prefixSum[i]
int[] prefixSum = new int[A.length + 1];
for (int i = 0; i < A.length; i++) {
prefixSum[i + 1] = prefixSum[i] + A[i];
}
return dfs(A, 0, A.length - 1, dp, prefixSum);
}
// 返回A[i,...,j]合并为一堆石头需要的最少能量
private int dfs(int[] A, int i, int j, int[][] dp, int[] prefixSum) {
// 有记忆则调取记忆
if (dp[i][j] != 0) {
return dp[i][j];
}
if (j - i == 1) {
// 如果石头只剩下两堆,则返回它们的和
dp[i][j] = A[i] + A[j];
return dp[i][j];
} else if (i == j){
// 如果只剩下一堆石头,不需要能量,返回0
return 0;
}
// 用min来记录当前得到的最小能量
int min = Integer.MAX_VALUE;
// total记录最后一次合并需要的能量
int total = prefixSum[j + 1] - prefixSum[i];
for (int k = i; k < j; k++) {
// 如果有记忆则调取记忆,否则继续暴搜
int part1 = dp[i][k] == 0 ? dfs(A, i, k, dp, prefixSum) : dp[i][k];
int part2 = dp[k + 1][j] == 0 ? dfs(A, k + 1, j, dp, prefixSum) : dp[k + 1][j];
// 每次得到一个值就更新最小值
min = Math.min(min, part1 + part2);
}
// 返回之前把结果加入记忆
dp[i][j] = min + total;
return dp[i][j];
}
}
时间复杂度 O ( n 3 ) O(n^3) O(n3),空间 O ( n 2 ) O(n^2) O(n2)。可以理解为记忆矩阵的每一个entry最多只会被搜到一次(因为搜到第二次的时候直接调取记忆了),但在算每个entry的时候需要 O ( n ) O(n) O(n)的时间,所以时间复杂度就是 O ( n 3 ) O(n^3) O(n3)。
法2:动态规划。注意区间DP的递推顺序,区间DP一般按照区间长度进行递推。这与之前的思考方式也是吻合的,我们一直都是已知短区间的结果,来递推长区间的结果。代码如下:
public class Solution {
/**
* @param A: An integer array
* @return: An integer
*/
public int stoneGame(int[] A) {
// write your code here
if (A == null || A.length == 0) {
return 0;
}
int[][] dp = new int[A.length][A.length];
int[] prefixSum = new int[A.length + 1];
for (int i = 0; i < A.length; i++) {
prefixSum[i + 1] = prefixSum[i] + A[i];
}
// 长度从2开始即可,因为长度为1的时候结果是0,dp初始化的时候默认就是0,没必要赋值
for (int len = 2; len <= A.length; len++) {
// i枚举的是正在枚举的区间的左端点
for (int i = 0; i + len - 1 < A.length; i++) {
// 正在枚举的区间左端点是i,右端点是i + len - 1
int l = i, r = i + len - 1;
// 在求最小的时候,需要初始化成一个很大的数,然后不断更新
dp[l][r] = Integer.MAX_VALUE;
for (int j = l; j < r; j++) {
dp[l][r] = Math.min(dp[l][r], dp[l][j] + dp[j + 1][r] + prefixSum[r + 1] - prefixSum[l]);
}
}
}
return dp[0][A.length - 1];
}
}
时空复杂度与上同。