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=6,循環退出了,返回 right 即可,參見代碼如下:
解法一:
class Solution { public: int splitArray(vector<int>& nums, int m) { long left = 0, right = 0; for (int i = 0; i < nums.size(); ++i) { left = max(left, (long)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 right; } bool can_split(vector<int>& nums, long m, long sum) { long 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<long> sums(n + 1); vector<vector<long>> dp(m + 1, vector<long>(n + 1, LONG_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) { long val = max(dp[i - 1][k], sums[j] - sums[k]); dp[i][j] = min(dp[i][j], val); } } } return dp[m][n]; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/410
類似題目:
參考資料:
https://leetcode.com/problems/split-array-largest-sum/
https://leetcode.com/problems/split-array-largest-sum/discuss/89816/DP-Java
https://leetcode.com/problems/split-array-largest-sum/discuss/89873/binary-search-c-solution
