[LeetCode] Split Array Largest Sum 分割数组的最大值

 

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:
Given m satisfies the following constraint: 1 ≤ m ≤ length(nums) ≤ 14,000.

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.

 

这道题给了我们一个非负数的数组nums和一个整数m,让我们把数组分割成m个非空的连续子数组,让我们最小化m个子数组中的最大值。开始以为要用博弈论中的最小最大化算法,可是想了半天发现并不会做,于是后面决定采用无脑暴力破解,在nums中取出所有的m个子数组的情况都找一遍最大值,为了加快求子数组和的运算,还建立了累计和数组,可以还是TLE了,所以博主就没有办法了,只能上网参考大神们的解法,发现大家普遍使用了二分搜索法来做,感觉特别巧妙,原来二分搜索法还能这么用,厉害了我的哥。我们首先来分析,如果m和数组nums的个数相等,那么每个数组都是一个子数组,所以返回nums中最大的数字即可,如果m为1,那么整个nums数组就是一个子数组,返回nums所有数字之和,所以对于其他有效的m值,返回的值必定在上面两个值之间,所以我们可以用二分搜索法来做。我们用一个例子来分析,nums = [1, 2, 3, 4, 5], m = 3,我们将left设为数组中的最大值5,right设为数字之和15,然后我们算出中间数为10,我们接下来要做的是找出和最大且小于等于10的子数组的个数,[1, 2, 3, 4], [5],可以看到我们无法分为3组,说明mid偏大,所以我们让right=mid,然后我们再次进行二分查找哦啊,算出mid=7,再次找出和最大且小于等于7的子数组的个数,[1,2,3], [4], [5],我们成功的找出了三组,说明mid还可以进一步降低,我们让right=mid,然后我们再次进行二分查找哦啊,算出mid=6,再次找出和最大且小于等于6的子数组的个数,[1,2,3], [4], [5],我们成功的找出了三组,我们尝试着继续降低mid,我们让right=mid,然后我们再次进行二分查找哦啊,算出mid=5,再次找出和最大且小于等于5的子数组的个数,[1,2], [3], [4], [5],发现有4组,此时我们的mid太小了,应该增大mid,我们让left=mid+1,此时left=6,right=5,循环退出了,我们返回left即可,参见代码如下:

 

解法一:

class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
        long long left = 0, right = 0;
        for (int i = 0; i < nums.size(); ++i) {
            left = max((int)left, nums[i]);
            right += nums[i];
        }
        while (left < right) {
            long long mid = left + (right - left) / 2;
            if (can_split(nums, m, mid)) right = mid;
            else left = mid + 1;
        }
        return left;
    }
    bool can_split(vector<int>& nums, int m, int sum) {
        int cnt = 1, curSum = 0;
        for (int i = 0; i < nums.size(); ++i) {
            curSum += nums[i];
            if (curSum > sum) {
                curSum = nums[i];
                ++cnt;
                if (cnt > m) return false;
            }
        }
        return true;
    }
};

 

上面的解法相对来说比较难想,在热心网友perthblank的提醒下,我们再来看一种DP的解法,相对来说,这种方法应该更容易理解一些。我们建立一个二维数组dp,其中dp[i][j]表示将数组中前j个数字分成i组所能得到的最小的各个子数组中最大值,初始化为整型最大值,如果无法分为i组,那么还是保持为整型最大值。为了能快速的算出子数组之和,我们还是要建立累计和数组,难点就是在于要求递推公式了。我们来分析,如果前j个数字要分成i组,那么i的范围是什么,由于只有j个数字,如果每个数字都是单独的一组,那么最多有j组;如果将整个数组看为一个整体,那么最少有1组,所以i的范围是[1, j],所以我们要遍历这中间所有的情况,假如中间任意一个位置k,dp[i-1][k]表示数组中前k个数字分成i-1组所能得到的最小的各个子数组中最大值,而sums[j]-sums[k]就是后面的数字之和,我们取二者之间的较大值,然后和dp[i][j]原有值进行对比,更新dp[i][j]为二者之中的较小值,这样k在[1, j]的范围内扫过一遍,dp[i][j]就能更新到最小值,我们最终返回dp[m][n]即可,博主认为这道题所用的思想应该是之前那道题Reverse Pairs中解法二中总结的分割重现关系(Partition Recurrence Relation),由此看来很多问题的本质都是一样,但是披上华丽的外衣,难免会让人有些眼花缭乱了,参见代码如下:

 

解法二:

class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
        int n = nums.size();
        vector<int> sums(n + 1, 0);
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
        dp[0][0] = 0;
        for (int i = 1; i <= n; ++i) {
            sums[i] = sums[i - 1] + nums[i - 1];
        }
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                for (int k = i - 1; k < j; ++k) {
                    int val = max(dp[i - 1][k], sums[j] - sums[k]);
                    dp[i][j] = min(dp[i][j], val);
                }
            }
        }
        return dp[m][n];
    }
};

 

参考资料:

https://discuss.leetcode.com/topic/61405/dp-java/2

https://discuss.leetcode.com/topic/61314/binary-search-c-solution

https://discuss.leetcode.com/topic/61324/clear-explanation-8ms-binary-search-java

 

    原文作者:Grandyang
    原文地址: http://www.cnblogs.com/grandyang/p/5933787.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞