[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(如果存在一个或多个子数组的和

所以这个解法的思路概括一下就是:

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 };