欢迎阅读、点赞、转发、订阅,你的举手之间,我的动力源泉。
写本题,源自看到的官方的一句描述"「将数组分割为 m 段,求……」是动态规划题目常见的问法。",让我想起了之前写的1043. 分隔数组以得到最大和,早期写的题解,风格更像一种草稿,但还是有很多扣友鼓励似的阅读点赞,感谢感谢,笔芯~
定义状态
d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个数即 n u m s [ 0... i − 1 ] nums[0...i-1] nums[0...i−1]之间的数被分成 j j j段,所组成的 j j j个子数组各自和的最大值中的最小值
转移方程
思考动态规划的类的问题,有点像道家的道生一,一生二,二生三,三生万物,从特殊情况到一般情况,推演归纳
回到本题也是如此,目标是求 d p [ i ] [ j ] dp[i][j] dp[i][j],再读一遍此定义:表示前 i i i个数即 n u m s [ 0... i − 1 ] nums[0...i-1] nums[0...i−1]之间的数被分成 j j j段,所组成的 j j j个子数组各自和的最大值中的最小值,我们能不能求其在这 n u m s [ 0... i − 1 ] nums[0...i-1] nums[0...i−1]中试着用剪刀,减出来两端,分成两部分来考虑?
-
第二段:可以想象成第 j j j段,如图上的 n u m s [ k , k + 1 , . . . . i − 1 ] nums[k,k+1,....i-1] nums[k,k+1,....i−1],因为我们取的是前 i i i个数,数组下标从 0 0 0开始的,这部分的和很显然就是 s u m ( n u m s [ k , k + 1 , . . . i − 1 ] ) sum(nums[k,k+1,...i-1]) sum(nums[k,k+1,...i−1])
-
第一段:可以想象成前 j − 1 j-1 j−1段,那这一段呢?既然是 j − 1 j-1 j−1段,其实和 j j j段没什么大的区别,可以转化成 d p dp dp,也就是 d p [ k ] [ j − 1 ] dp[k][j-1] dp[k][j−1],表示前 k k k个数即 n u m s [ 0... k − 1 ] nums[0...k-1] nums[0...k−1]之间的数被分成 j − 1 j-1 j−1段,所组成的 j − 1 j-1 j−1个子数组各自和的最大值的最小值
-
上面两端的最小值便是 d p [ i ] [ j ] dp[i][j] dp[i][j]的结果,即 d p [ i ] [ j ] dp[i][j] dp[i][j]= m a x max max( d p [ k ] [ j − 1 ] dp[k][j-1] dp[k][j−1], s u m [ k . . . i − 1 ] sum[k...i-1] sum[k...i−1]),然后这只是其中随便剪的一刀,也就是 k k k有很多种可能, k k k的范围可以从 0 0 0取到 i − 1 i-1 i−1,超过 i − 1 i-1 i−1没有意义,如果 k k k> i − 1 i-1 i−1,表示前 k k k个数,但是目标只考虑到前 i i i个数
-
最终的转移方程:
d p [ i ] [ j ] dp[i][j] dp[i][j]= m i n min min[ m a x max max( d p [ k ] [ j − 1 ] , s u m [ k . . . i − 1 ] dp[k][j-1],sum[k...i-1] dp[k][j−1],sum[k...i−1])] ,其中 0=< k k k<= i − 1 i-1 i−1
边界
- d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]表示前0个数被分成0段,这在逻辑上是讲不通的,如何出现这种场景呢,也就是整个 n u m s [ 0... i − 1 ] nums[0...i-1] nums[0...i−1]被分成 j = 1 j=1 j=1段,那么 d p [ k ] [ j − 1 ] dp[k][j-1] dp[k][j−1]就变成了 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0],取其与 s u m [ k . . . i − 1 ] sum[k...i-1] sum[k...i−1]的最大值,只要让 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]=0就不会影响最后的结果
- j j j> i i i,没有意义,因为,前 i i i个数字不可能被分成超过 i i i段,因为一个数字只能最多被分成一组,每个数组单独一组的话,也就是 i i i组,分不出大于 i i i组的情况,在变遍历的过程中, j j j的取值应该是 m i n ( m , i ) min(m,i) min(m,i), m m m是题目中给出的分割数的上限
- 最外层的 m i n min min如果初始值为0,会影响到结果,设置一个 M A X MAX MAX值,每次的
前缀和辅助数组
定义 p r e f i x [ i ] prefix[i] prefix[i]表示 n u m s nums nums数组中前 i i i个数的和,即 n u m s [ 0... i − 1 ] nums[0...i-1] nums[0...i−1]之前的和,此前缀和数组初始化 n + 1 n+1 n+1, p r e f i x [ 0 ] prefix[0] prefix[0]冗余,最为辅助数组
以图中的例子举例,打印的 d p dp dp如下
[[0,MAX,MAX],
[MAX,7,MAX],
[MAX,9,7],
[MAX,14,7],
[MAX,24,14],
[MAX,32,18]]
一个小函数
- 填充二维数组,遍历行,对每个列进行填充
for (int i = 0; i <= n; i++) {
Arrays.fill(dp[i], Integer.MAX_VALUE);
}
完整代码
public int splitArray(int[] nums, int m) {
int n = nums.length;
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) {
Arrays.fill(dp[i], Integer.MAX_VALUE);
}
int[] prefix = new int[n + 1];
for (int i = 0; i < n; i++) {
prefix[i + 1] = nums[i] + prefix[i];
}
dp[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= Math.min(m, i); j++) {
for (int k = 0; k < i; k++) {
dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j - 1], prefix[i] - prefix[k]));
}
}
}
// System.out.println(JSON.toJSONString(dp));
return dp[n][m];
}
复杂度分析:
- 时间复杂度: O ( M ∗ N ∗ N ) O(M*N*N) O(M∗N∗N) ,其中 N N N是数组 n u m s nums nums的长度, M M M是要分割的段数,三层 f o r for for l o o p loop loop , k k k最大到 N N N
- 空间复杂度: O ( M ∗ N ) O(M*N) O(M∗N) d p dp dp的空间