题目:
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/split-array-largest-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
这个题目很难,不过是典型的动态规划问题。
首先定义:
dp[i][j]:前i个连续数划分为j段时满足题目要求----最大数组和的最小值。
subSum[i,j]:从下标 i 到 j 的子数组的和。
很明显,可以使用dp数组来记忆化,使得将前i个数分割为j段时的状态,可以由将前i个数分割为(j-1)段时的状态转移而来:
在前i个数中,我们遍历0-i之间,找到一个下标k,使得原先分割为j-1段的状态,转换为分割为j段的状态,即
分割为0-k(分割为j-1段),以及k-i(单独一段),而由于前i个数分割为j-1段时的状态以及保存记忆下来,所以,当将前i个数划分为j段时,其状态转移方程为:
dp[i][j] = min(dp[i][j],max(dp[k][j-1],subSum[k+1,i])。
外面一个min用来保持:总体划分为 j 段时的段和保持最小状态。
里面一个max用来保持:使用下标k来将前 i 个数划分为 j 段时段和最大。
实现:
JAVA版:
package leetcode;
import java.util.Arrays;
/*
USER:LQY
DATE:2020/7/25
TIME:9:07
*/
public class leetcode_410_important {
public static void main(String []args){
}
public int splitArray_DP(int[] nums, int m) {
int lens = nums.length;
// if(lens == m){
// return Math.max(nums);
// }
int [][]dp = new int[lens+1][m+1];
for(int i = 0;i <= lens;i++)
for(int j = 0;j <= m;j++)
dp[i][j] = Integer.MAX_VALUE;
dp[0][0] = 0;
// int []subSum = new int[lens+1];
// subSum[1] = nums[0];
// for(int i = 2;i <= lens;i++)
// subSum[i] = subSum[i-1] + nums[i-1];
for(int i = 1;i < lens;i++)
nums[i] += nums[i-1];
for(int i = 1;i <= lens;i++){
for(int j = 1;j<=m && j<=i;j++){ //j<=m保持前i个连续数最多划分为m段。j<=i保证没有空的段
for(int k = 0;k < i;k++){ //在前i个连续数已经被符合题意的划分为j-1段时,再在其基础上,找到一个k下标,使得前i个连续数划分为符合题意的j段。这里利用记忆化,从而会产生叠加效应,这也是动态规划的特点。
if(k == 0){
//此时,直接是将前i个连续数划分为一段,即完整的子数组。
dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j-1], nums[i-1]));
}else
dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j-1], nums[i-1] - nums[k-1]));
}
}
}
return dp[lens][m];
}
//二分查找。
// 根据题意可以知道,每一段的和的范围为[maxNums, sumNums]。
// 即不小于nums数组中的最大值,不大于nums数组的和。
// 前者为划分为单个元素的情况,后者为不划分的情况。
// 统计sunNums时如果使用的是int类型可能会越界,最好使用long类型,在返回时强制转换即可。
public int splitArray_SEA(int[] nums, int m) {
int maxNums = 0;
int sumNums = 0;
for(int i : nums){
maxNums = maxNums>i? maxNums : i;
sumNums += i;
}
int lowMax = maxNums;
int highMax = sumNums;
while(lowMax < highMax){
int midMax = (lowMax+highMax) >> 1;
int temp = 0;
int div = 1; //初始划分组数为1.
for(int i : nums){
temp += i;
if(temp > midMax){ //再加上第i个数时就超过了
//此时前i-1个数可以划分为一组
div++;
temp = i; //再初始化,相当于开始新的一组的划分。
}
}
//再重新定位low以及high
if(div > m){
//划分多了。说明此时的midMax还是太小了。
lowMax = midMax + 1;
}else{
//划分组数太少了,说明midMax太大了。
highMax = midMax;
}
}
return lowMax;
}
}
C++版:
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
int n = nums.size();
long dp[n+1][m+1];
for(int i = 0;i <= n;i++)
for(int j = 0;j <= m;j++)
dp[i][j] = INT_MAX;
dp[0][0] = 0;
long subSum[n+1];
subSum[0] = 0;
subSum[1] = nums[0];
for(int i = 2;i <= n;i++){
subSum[i] = subSum[i-1] + nums[i-1];
}
for(int i = 1;i <= n;i++){
for(int j = 1;j<=m && j<=i;j++){
for(int k = 0;k < i;k++){
dp[i][j] = min(dp[i][j], max(dp[k][j-1], subSum[i]-subSum[k]));
}
}
}
return (int)dp[n][m];
}
};