1. 程式人生 > >[LeetCode] Split Array Largest Sum 分割陣列的最大值

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

上面的解法相對來說比較難想,在熱心網友的提醒下,我們再來看一種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];
    }
};

參考資料: