题目要求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Given an array which consists of non-negative integers and an integer m, you can split the array into m non-empty continuous subarrays. Write an algorithm to minimize the largest sum among these m subarrays. Note: If n is the length of array, assume the following constraints are satisfied: 1 ≤ n ≤ 1000 1 ≤ m ≤ min(50, n) Examples: Input: nums = [7,2,5,10,8] m = 2 Output: 18 Explanation: There are four ways to split nums into two subarrays. The best way is to split it into [7,2,5] and [10,8], where the largest sum among the two subarrays is only 18.
将一个长度为n的正整数数组分割为m个非空的连续子数组,并分别计算每个子数组中所有元素的和。求一种分割方式,使得该分割方式生成的最大子数组和为所有分割方式中最小的。
比如题目中的例子nums = [7,2,5,10,8],m = 2 一共有四种分割方式:
[7], [2,5,10,8]
[7,2], [5,8,10]
[7,2,5], [8,10]
[7,2,5,8], [10]
其中第三种分割得到的最大子数组的和 是所有分割中最小的
思路一:动态规划 首先,我们可以通过递归的方式来遍历所有的分割方式,从而找到所有分割方式中最符合要求的那一种结果。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public int splitArray (int [] nums, int m) { int [] sums = new int [nums.length+1 ]; for (int i = 1 ; i<=nums.length ; i++) { sums[i] = nums[i-1 ] + sums[i-1 ]; } return splitArray(nums, m, 0 , sums); } public int splitArray (int [] nums, int m, int cur, int [] sums) { if (m == 1 ) { return sums[nums.length] - sums[cur]; } int min = Integer.MAX_VALUE; int diff = Integer.MAX_VALUE; for (int i = cur+1 ; i<=nums.length-m+1 ; i++) { int left = sums[i]-sums[cur]; int right = splitArray(nums, m-1 , i, sums); if (diff < Math.abs(left - right)) { break ; } diff = Math.abs(left - right); min = Math.min(min, Math.max(left, right)); } return min; }
这种方法在大数据量的场景下会出现超时的问题,本质在于我们没有足够的复用中间的所有场景,如对于[i-j]这个子数组的k次分割的最优结果。如果我们记录从i到数组结尾进行k次分割的最优结果,该结果记录为dp[i][k],则从j到数组结尾进行k+1次分割的最优结果为min(max(num(j), dp[j+1][k]), max(nums(j)+num(j+1), dp[j+2][k])... ) 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public int splitArray (int [] nums, int m) { int L = nums.length; int [] S = new int [L+1 ]; S[0 ]=0 ; for (int i=0 ; i<L; i++) S[i+1 ] = S[i]+nums[i]; int [] dp = new int [L]; for (int i=0 ; i<L; i++) dp[i] = S[L]-S[i]; for (int s=1 ; s<m; s++) { for (int i=0 ; i<L-s; i++) { dp[i]=Integer.MAX_VALUE; for (int j=i+1 ; j<=L-s; j++) { int t = Math.max(dp[j], S[j]-S[i]); if (t<=dp[i]) dp[i]=t; else break ; } } } return dp[0 ]; }
思路二:二分法 这是一个非常难想到的方法。二分法的难点一直在于如何划分初始边界,以及如何逐渐缩小边界并且确保左右指针可以相遇。在这里,边界被设置为该数组中可以得到的子数组元素和的最小值和最大值。 根据基本常识可知,数组的最大元素决定了该数组分割出的子数组的元素和的下界,而数组的元素和上界一定不会超过数组所有元素的和。 在确定了数组元素和的上界和下界之后, 就需要找出一种方法,来不断压缩区间直到最后一种。
可以使用中间位置作为数组元素和的边界,即假设所有的连续数组的和都不会超过mid值。假如按照这种方式得到的分割结果大于了规定的m个,则说明mid值作为最大元素和上界并不能够做到只分割出m个子数组,因此最大元素和上界一定在mid和有界中间。同理,假如按照这种方式得到的分割结果小于等于规定的m个,则说明mid值作为最大元素和上界能够满足分割出m个子数组,但是可能还存在更优解。通过这种二分法思路得到的最后结果就是所需要的最小分割结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public int splitArray2 (int [] nums, int m) { long sum = 0 ; int max = Integer.MIN_VALUE; for (int i = 0 ; i<nums.length ; i++) { max = Math.max(max, nums[i]); sum += nums[i]; } if (m == 1 ) { return (int )sum; } long lft = max; long rgt = sum; while (lft <= rgt) { long mid = (lft + rgt) / 2 ; if (valid(nums, m, mid)) { rgt = mid - 1 ; }else { lft = mid + 1 ; } } return (int ) lft; } public boolean valid (int [] nums, int m, long target) { int count = 1 ; long sum = 0 ; for (int i = 0 ; i<nums.length ; i++) { sum += nums[i]; if (sum > target) { sum = nums[i]; count++; if (count > m) { return false ; } } } return true ; }