[LeetCode]410. Split Array Largest Sum三种不同解法(JavaScript)
题目描述
LeetCode原题链接:410. Split Array Largest Sum
Given an array nums
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.
Example 1:
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.
Example 2:
Input: nums = [1,2,3,4,5], m = 2 Output: 9
Example 3:
Input: nums = [1,4,4], m = 3 Output: 4
Constraints:
1 <= nums.length <= 1000
0 <= nums[i] <= 106
1 <= m <= min(50, nums.length)
解法一:不知道该叫什么那就叫摸着石头过河吧!
要想求一个每个子数组中元素和的最小值,那肯定是要确保这些子数组的和之间差值较小,也就是说我们要尽可能的平分。假设数组中所有元素之和为sum,需要分割成m个子数组,那么每一个子数组的和至少要是 minSum = sum/m 才能保证尽可能的平分。当然,这这只是理想情况,因为sum不一定能整除m(有余数的话分组数就多于m了),而且题目要求是连续的元素组成的子数组,合法的子数组的和不一定刚好能凑成minSum(如果存在一个或多个子数组的和 <minSum,那么最终分出来的组数同样也会多于m)。如果出现上述情况,那么minSum就要扩大了,我们需要用扩大后的minSum再去校验。我们不知道要扩大多少,所以只能每次自增1去判断(所以我叫这个解法为摸着石头过河!),直到找到一个满足条件的minSum为止。
所以这个解法的思路概括一下就是:
1. 根据nums的和还给定的m求出一个下限值minSum;
2. 遍历nums数组进行分割,分割条件是当前subArray的和<=minSum;
3. 如果nums数组还没遍历结束已经分出的子数组的个数大于m,说明当前minSum过小,minSum++再重复2.;如果nums数组遍历完成且分出的子数组数等于m,说明找到了结果,直接返回。
1 var splitArray = function(nums, m) { 2 let sum = 0; 3 nums.forEach((num, i) => { 4 sum += num; 5 }); 6 let minSum = Math.floor(sum / m); // 求下限值 7 while(!canSplit(nums, m, minSum)) { 8 minSum++; 9 } 10 return minSum; 11 }; 12 13 // 校验分组合法性 14 var canSplit = function(nums, m, minSum) { 15 let cnt = 0; 16 let i = 0; 17 while(i < nums.length) { 18 if(cnt == m) return false; 19 let j = i; 20 let temp = 0; 21 while(j < nums.length && temp + nums[j] <= minSum) 22 { 23 temp += nums[j++]; 24 } 25 cnt++; 26 i = j; 27 } 28 return true; 29 }
解法二:Binary Search
上述的解法效率有点低,因为minSum每次只自增1去重新判断。那么有没有什么办法可以加速这个过程呢?当然,我们可以用二分法!二分法需要知道查找的左右边界,我们已经得出下限了,那上限是什么呢?如果m=1,也就是说只分割出一个子数组且这个子数组是其自身,这个时候子数组的和就是整个数组的和,肯定是最大的。很好,下面我们来看如何移动中点。和前面分析的差不多,我们用中点值去分割数组时,根据分割出的子数组个数来判断当面的分割标准是偏大还是偏小:如果分割出的组数大于m,则需要扩大minSum,即将左指针右移;如果恰好可以分割成m组,就缩小当前的minSum,即将右指针左移,去找更优解。
1 var splitArray = function(nums, m) { 2 let right = 0; 3 let left = 0; 4 nums.forEach((num) => { 5 right += num; 6 }); 7 left = Math.floor(right / m); 8 while(left < right) { 9 let minSum = Math.floor((left + right) / 2); 10 if(!canSplit(nums, m, minSum)) { 11 left = minSum + 1; 12 } 13 else { 14 right = minSum; 15 } 16 } 17 return right; 18 }; 19 20 var canSplit = function(nums, m, minSum) { 21 let cnt = 0; 22 let i = 0; 23 while(i < nums.length) { 24 if(cnt == m) return false; 25 let j = i; 26 let temp = 0; 27 while(j < nums.length && temp + nums[j] <= minSum) 28 { 29 temp += nums[j++]; 30 } 31 cnt++; 32 i = j; 33 } 34 return true; 35 }
解法三:动态规划
换个思路,我们建立一个二维数组,dp[i][j]表示前j个数字如果分成i组各子数组和的最小值。下面我们来推导状态转移方程。对于前j各数组,我们分组的情况是分成1组、2组、3组......j组;即最小分成1组,最多分成j组。那么我们在尝试从1组到j组的分组,即遍历中间态k时,可以知道dp[i-1][k]表示数组中前k个数字分成i-1组时各子数组和的最小值,我们用这个最小值和[k,j]位置的这个子数组的和比较,取两者中较大的那个和之前的dp[i][j]比较,再取较小值来更新dp[i][j](局部最大值,全局最小值)。为了快速求[k,j]范围内所有元素和,我们可以用一个前缀和数组来处理:即一次遍历并保存[0,i]范围的元素和,那么要求[i,j]范围内的元素和只需用sum[0, j] - sum[0, i]即可。
1 var splitArray = function(nums, m) { 2 let len = nums.length; 3 let prefixSum = new Array(len + 1); 4 prefixSum.fill(0); 5 let dp = new Array(m + 1); 6 for(let i = 0; i <= m; i++) { 7 dp[i] = new Array(len + 1); 8 for(let j = 0; j <= len; j++) { 9 dp[i][j] = Number.MAX_VALUE; 10 if(j > 0) { 11 prefixSum[j] = prefixSum[j - 1] + nums[j - 1]; 12 } 13 } 14 } 15 dp[0][0] = 0; 16 for(let i = 1; i <= m; i++) { 17 for(let j = 1; j <= len; j++) { 18 for(let k = i - 1; k < j; k++) { 19 dp[i][j] = Math.min(dp[i][j], Math.max(prefixSum[j] - prefixSum[k], dp[i-1][k])); 20 } 21 } 22 } 23 return dp[m][len]; 24 };