题目描述
给定一个非负整数数组 nums 和一个整数 m ,你需要将这个数组分成 m 个非空的连续子数组。
设计一个算法使得这 m 个子数组各自和的最大值最小。
题目难度:困难
示例1
输入:nums = [7,2,5,10,8], m = 2
输出:18
解释:
一共有四种方法将 nums 分割为 2 个子数组。
其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
示例2
输入:nums = [1,2,3,4,5], m = 2
输出:9
示例3
输入:nums = [1,4,4], m = 3
输出:4
解法1(动态规划)
总体思路
动态规划与分治法的思想类似,都是将大问题划分成若干个小问题,但是与分治法不同的是动态规划往往会保存已解决子问题的结果,在需要这些结果时不需要重复计算,可以直接取出,更加地节省时间。就本题而言,可以用dp[i][j]表示将子数组nums[0…i]分成j份的最大值的最小值,根据这个表示方法我们很容易能够找出一些i和j无意义的取值和一些能够直接得到结果的取值。
无意义的取值如下:
- i任意,j=0(将某个数组分成0份这显然无意义)
- i+1<j(不可能把2个数分成3份或更多份)
可直接计算的取值如下:
- i任意,j=1(将i+1个数字分成1份,就是累加和,直接计算子数组和即可)
- i+1 ==j(将m个数分成m份,只有一种分法,直接求出子数组最大值即可)
当以上两种特殊情况处理好之后就需要考虑一般情况,当i>=2,j>=2时有
dp[i][j] = min{max{dp[k][j-1],sum{nums[k+1…i]}}}
(其中k从j-2到i-1)
代码实现
public static int splitArray(int[] nums, int m) {
int[][] db = new int[nums.length][m+1];
//初始化数组,对于没有意义的情况将数组值置为无穷大
for (int i=0;i<nums.length;i++){
db[i][0] = Integer.MAX_VALUE;
}
for (int i=0;i<nums.length;i++){
for (int j=i+2;j<m+1;j++){
db[i][j] = Integer.MAX_VALUE;
}
}
//对于可以直接计算的情况直接进行计算
for (int i=0;i<nums.length;i++){
db[i][1] = getSum(nums,0,i);
}
for (int i=1;i<nums.length;i++){
for (int j=i;j<m+1;j++){
if (i+1 == j){
db[i][j] = getMax(nums,i);
}
}
}
//对于正常情况根据递推公式进行计算
for (int i=2;i<nums.length;i++){
for (int j=2;j<=Math.min(i,m);j++){
int minMax = Integer.MAX_VALUE;
for (int k=j-2;k<i;k++){
int tempMax = Math.max(db[k][j-1],getSum(nums,k+1,i));
if (tempMax < minMax){
minMax = tempMax;
}
}
db[i][j] = minMax;
}
}
printDB(db,nums,m);
return db[nums.length-1][m];
}
//从nums数组种在一定范围内获取和
public static int getSum(int[] nums,int begin,int end){
int sum = 0;
for (int i=begin;i<=end;i++){
sum+=nums[i];
}
return sum;
}
//获取[0,end]范围内的最大值
public static int getMax(int[] nums,int end){
int max=nums[0];
for (int i=1;i<=end;i++){
if (nums[i]>max){
max = nums[i];
}
}
return max;
}
运行结果
示例1
示例2
示例3
结果分析
由以上运行结果可知该算法对于所给示例能够得到正确结果,但是很显然,该算法时间复杂度为O(n³),估计很难满足要求,提交到leetcode平台后发现果然如此。
这leetcode是真的猛啊,还有这么长的测试用例。
解法2(二分法)
总体思路
二分法在前面的题目中已经多次使用,但是真的是没想到本题还是使用二分法,经过”参考“别人的解题思路,也使得我有了一些想法。使用二分法需要有一个单调的一种趋势,在本题中这个趋势很容易理解,但是却很难想到,因为这似乎是一种逆向思维,即
- 如果数组和的最大值越大,则所要的划分数应该越少
- 反之,如果数组和的最大值越小,则所要的划分数应该越多
这成了我们解题的关键,我们可以使用二分法不断试探所需的划分数,直到满足题目要求为止,当然,需要注意的是题目要求的是”最小的最大值“,所以当找到符合条件的最大值后一定要将最大值最小化后才能符合条件。
代码实现
public static int splitArray(int[] nums, int m) {
int max=nums[0],sum=nums[0];
//计算得到数组的最大值和数组各元素之和
for (int i=1;i<nums.length;i++){
if (nums[i]>max){
max = nums[i];
}
sum += nums[i];
}
//定义数组子数组之和最大值的最小值的范围
int left = max,right = sum;
while (left < right){
int mid = (left + right)/2;
//计算最大值不大于mid需要划分的最少的数组个数
int splits = 1,tempSum=0;
for (int num : nums){
if (tempSum + num > mid){//如果遍历和加上当前遍历的值大于mid,则”从头再来“
tempSum = 0;
splits++;
}
tempSum += num;
}
if (splits > m){//划分次数太多,说明当前所选最大值偏小
left = mid + 1;
}else{//划分次数太少,说明当前所选最大值偏大
right = mid;
}
}
return left;
}
运行结果
1.示例1运行结果
2.示例2运行结果
3.示例3运行结果
结果分析
经过一番“折腾”,使用二分法也得出了正确的结果,由于二分法的效率是比较高的,所以一般都能够通过测试,提交到leetcode平台后也确实如此。
就本题而言,我是确实没有想到使用二分法(可能是自己经验不足,也有可能是自己太菜),但是二分法好像都和“非负整数数组”、“连续”等特点有关,而且实现起来好像比解法1的动态规划简单一点,没有那么多细节需要考虑,之后还要认真总结,看来二分法确实是很重要的一种思想,远没有想象中的那么简单。
总结
本题的难度为困难,开始我甚至不敢做,心想有必要在这中秋佳节找虐嘛,但是我还是不相信,最后还是选择了这道题目,结果表明我还是过于相信自己了,做的还是挺吃力的,当然,我也从中学习到了很多,也算是过了一个充实的节日吧。最后,祝大家节日快乐。