leetcode410:分割数组的最大值_填表法求解动态规划
leetcode410
视频讲解B站地址: https://www.bilibili.com/video/BV16T4y1j7w9/
动态规划算法
动态规划算法有两个难点:
- 寻找状态转移方程: 这个需要一点直觉.
- 状态边界的确定: 如果状态边界确定不好就会带来各种bug,这个视频中我通过填表法解决这个问题.
动态规划数组的定义
定义动态规划数组dp[i][j]
表示将nums[0, ..., i]
区间内的数分为j
组得到的符合题意的结果.
下面以数组nums={7,3,5,10,8}
为例,演示对应dp
数组某几个位上的取值.
状态转移:状态之间的变换关系
dp[i][j]
表示将数组区间nums[0...i]
分为j
组,可以由dp[k][j-1]
得到:
也就是说先将数组区间的一部分nums[0..k]
分为j-1
组,再将剩下的nums[k+1, .., i]
自成一组,再去求各组和的最大值.即:
d
p
[
i
]
[
j
]
=
min
k
在
某
范
围
内
取
值
{
max
(
d
p
[
k
]
[
j
−
1
]
,
s
u
m
(
n
u
m
s
[
k
+
1
,
.
.
.
j
]
)
)
}
dp[i][j] = \min_{k在某范围内取值} \{ \max( dp[k][j-1], sum(nums[k+1,...j]) ) \}
dp[i][j]=k在某范围内取值min{max(dp[k][j−1],sum(nums[k+1,...j]))}
(这里的k
是可以遍历的有一定的范围,再后文我们再讨论k
具体的取值范围).
下面例子展示了如何由dp[1][2]
,dp[2][2]
,dp[3][2]
得到dp[4][3]
:
状态的边界: 填表法
分析状态的边界是动态规划问题最容易出现bug的地方,我在这里演示填表法,一种考虑边界的普适性的方法:
将dp
数组画成一张表格,确定i
轴和j
轴的范围,在本题中,i
轴的取值范围从0
到len(nums)-1
,j
轴的取值范围从0
到最大组数
.(当然不可能将元素分为0
组,在这里j
轴从0
开始是为了照顾编程语言索引从0开始,简化编程,因此j=0
这一列其实是无意义的).
下面分别考虑上表中的三个区域: dp
数组取值不存在的区域,dp
数组取值的边界区域,正常动态规划的区域.
dp
数组取值不存在的区域
dp
数组取值不存在有以下两种情况:
- 组数
j
为0
时,dp
取值不存在,因为没办法将一些元素分为0组. - 组数
j
大于元素个数i+1
时,dp
取值不存在,好比两颗糖没法分给三个孩子.
两个区域以红色标识出来,因为我们的动规取目标的是最小值,所以在编程时将dp
数组这些区域的值设为无穷大即可.
dp
数组取值的边界区域
dp
数组取值的边界,也就是说动规从这些位置开始,需要特殊考虑的情况,对于本题来说,有两个边界区域:
- 当
j==1
,即把所有元素仅分为一组时,dp
数组对应的值即为该组元素之和. - 当
j==(i+1)
,即组数和元素个数相同时,dp
数组对应的值即为区间内最大元素值.
正常状态转移的区域
回顾正常转移的状态转移公式,在该公式中我们要确定k
的取值范围:
d
p
[
i
]
[
j
]
=
min
k
在
某
范
围
内
取
值
{
max
(
d
p
[
k
]
[
j
−
1
]
,
s
u
m
(
n
u
m
s
[
k
+
1
,
.
.
.
j
]
)
)
}
dp[i][j] = \min_{k在某范围内取值} \{\max(dp[k][j-1],sum(nums[k+1,...j]))\}
dp[i][j]=k在某范围内取值min{max(dp[k][j−1],sum(nums[k+1,...j]))}
下图的红色箭头和蓝色箭头分别表示求取dp[3][2]
和dp[4][3]
时的转移情况:
i=3
,j=2
时,k
取到了{0,1,2}
.i=4
,j=3
时,k
取到了{1,2,3}
根据上图分析,当求取dp[i][j]
时,k
的取值范围应为: k<i
(毕竟要留下一个给最后一组)且k+1>=j-1
(元素数必须大于等于组数)
代码实现
伪代码
class Solution {
public int splitArray(int[] nums, int m) {
// dp[i][j]表示将数组nums[0, ..., i]这(i+1)个数分为j组的情况下,得到的符合题意的最小组和
int[][] dp = new int[nums.length][m + 1];
// 1. 处理dp值不存在的状态
for (int i = 0; i < nums.length; i++) {
dp[i][0] = Integer.MAX_VALUE;
}
for (int i = 0; i < nums.length; i++) {
for (int j = i + 2; j <= m; j++) {
dp[i][j] = Integer.MAX_VALUE;
}
}
// 2. 处理边界状态
for (int i = 0; i < nums.length && i + 1 <= m; i++) {
dp[i][i + 1] = intervalMax(nums, 0, i);
}
for (int i = 0; i < nums.length; i++) {
dp[i][1] = intervalSum(cumsum, 0, i);
}
// 3. 正常动态规划的状态转移
for (int j = 2; j <= m; j++) {
for (int i = j; i < nums.length; i++) {
// 求取dp[i][j], k的取值范围从(j-2)到(i-1)
dp[i][j] = Integer.MAX_VALUE;
for (int k = j - 2; k < i; k++) {
int candidate = Math.max(dp[k][j - 1], intervalSum(cumsum, k + 1, i))
dp[i][j] = Math.min(dp[i][j], candidate);
}
}
}
// 返回dp[nums.length - 1][m]
return dp[nums.length - 1][m];
}
private int intervalSum(int[] nums, int start, int end) {
// 求数组nums从第start位到第end位的区间和
}
private int intervalMax(int[] nums, int start, int end) {
// 求数组nums从第start位到第end位的区间最大值
}
public static void main(String[] args) {
new Solution().splitArray(new int[]{7, 2, 5, 10, 8}, 2);
}
}
Java代码
在实际编程中要注意的是,为了简化求取区间和的intervalSum
函数,我们先对sum
数组进行积分得到cumsum
数组,通过在积分数组上做差得到数组区间和:
// 积分数组的计算
int[] cumsum = new int[nums.length];
cumsum[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
cumsum[i] = cumsum[i - 1] + nums[i];
}
private int intervalSum(int[] cumsum, int start, int end) {
if (start == 0)
return cumsum[end];
return cumsum[end] - cumsum[start - 1];
}
通过测试的Java代码如下:
class Solution {
public int splitArray(int[] nums, int m) {
// 计算积分数组cumsum,以简化数组区间和的计算
int[] cumsum = new int[nums.length];
cumsum[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
cumsum[i] = cumsum[i - 1] + nums[i];
}
// dp[i][j]表示将从nums[0]到nums[i]这(i+1)个数分为j组的情况下,得到的符合题意的最小组和
int[][] dp = new int[nums.length][m + 1];
// 1. 处理不可能到达的值
for (int i = 0; i < nums.length; i++) {
dp[i][0] = Integer.MAX_VALUE;
}
for (int i = 0; i < nums.length; i++) {
for (int j = i + 2; j <= m; j++) {
dp[i][j] = Integer.MAX_VALUE;
}
}
// 2. 处理边界值
for (int i = 0; i < nums.length && i + 1 <= m; i++) {
dp[i][i + 1] = intervalMax(nums, 0, i);
}
for (int i = 0; i < nums.length; i++) {
dp[i][1] = intervalSum(cumsum, 0, i);
}
// 3. 正常dp
for (int j = 2; j <= m; j++) {
for (int i = j; i < nums.length; i++) {
// 求取dp[i][j]
dp[i][j] = Integer.MAX_VALUE;
for (int k = j - 2; k < i; k++) {
dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j - 1], intervalSum(cumsum, k + 1, i)));
}
}
}
return dp[nums.length - 1][m];
}
private int intervalSum(int[] cumsum, int start, int end) {
if (start == 0)
return cumsum[end];
return cumsum[end] - cumsum[start - 1];
}
private int intervalMax(int[] nums, int start, int end) {
int maxValue = Integer.MIN_VALUE;
for (int i = start; i <= end; i++) {
maxValue = Math.max(maxValue, nums[i]);
}
return maxValue;
}
public static void main(String[] args) {
new Solution().splitArray(new int[]{7, 2, 5, 10, 8}, 2);
}
}