題目要求
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:
If n is the length of array, assume the following constraints are satisfied:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
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.
將一個長度爲n的正整數數組分割爲m個非空的連續子數組,並分別計算每個子數組中所有元素的和。求一種分割方式,使得該分割方式生成的最大子數組和爲所有分割方式中最小的。
比如題目中的例子nums = [7,2,5,10,8],m = 2
一共有四種分割方式:
- [7], [2,5,10,8]
- [7,2], [5,8,10]
- [7,2,5], [8,10]
- [7,2,5,8], [10]
其中第三種分割得到的最大子數組的和 是所有分割中最小的
思路一:動態規劃
首先,我們可以通過遞歸的方式來遍歷所有的分割方式,從而找到所有分割方式中最符合要求的那一種結果。代碼如下:
public int splitArray(int[] nums, int m) {
//計算[0...i]中所有元素的和
int[] sums = new int[nums.length+1];
for(int i = 1 ; i<=nums.length ; i++) {
sums[i] = nums[i-1] + sums[i-1];
}
return splitArray(nums, m, 0, sums);
}
//計算從cur位置開始,將其分割爲m個子數組的最小分割場景
public int splitArray(int[] nums, int m, int cur, int[] sums) {
if(m == 1) {
return sums[nums.length] - sums[cur];
}
int min = Integer.MAX_VALUE;
int diff = Integer.MAX_VALUE;
for(int i = cur+1 ; i<=nums.length-m+1 ; i++) {
//當前元素爲止,左邊的子數組的元素和
int left = sums[i]-sums[cur];
//對右邊的剩餘元素遞歸的調用splitArray方法
int right = splitArray(nums, m-1, i, sums);
//如果出現二者之間的差遞增的情況,則說明距離最優分割越來越遠,則停止繼續嘗試
if(diff < Math.abs(left - right)) {
break;
}
diff = Math.abs(left - right);
min = Math.min(min, Math.max(left, right));
}
return min;
}
這種方法在大數據量的場景下會出現超時的問題,本質在於我們沒有足夠的複用中間的所有場景,如對於[i-j]這個子數組的k次分割的最優結果。如果我們記錄從i到數組結尾進行k次分割的最優結果,該結果記錄爲dp[i][k]
,則從j到數組結尾進行k+1次分割的最優結果爲min(max(num(j), dp[j+1][k]), max(nums(j)+num(j+1), dp[j+2][k])... )
代碼如下:
public int splitArray(int[] nums, int m)
{
int L = nums.length;
//記錄0-i的元素和
int[] S = new int[L+1];
S[0]=0;
for(int i=0; i<L; i++)
S[i+1] = S[i]+nums[i];
//如果m=1,則最小分割結果對應的是整個數組中所有元素的和
int[] dp = new int[L];
for(int i=0; i<L; i++)
dp[i] = S[L]-S[i];
for(int s=1; s<m; s++)
{
for(int i=0; i<L-s; i++)
{
dp[i]=Integer.MAX_VALUE;
for(int j=i+1; j<=L-s; j++)
{
int t = Math.max(dp[j], S[j]-S[i]);
if(t<=dp[i])
dp[i]=t;
else
break;
}
}
}
return dp[0];
}
思路二:二分法
這是一個非常難想到的方法。二分法的難點一直在於如何劃分初始邊界,以及如何逐漸縮小邊界並且確保左右指針可以相遇。在這裏,邊界被設置爲該數組中可以得到的子數組元素和的最小值和最大值。
根據基本常識可知,數組的最大元素決定了該數組分割出的子數組的元素和的下界,而數組的元素和上界一定不會超過數組所有元素的和。
在確定了數組元素和的上界和下界之後, 就需要找出一種方法,來不斷壓縮區間直到最後一種。
可以使用中間位置作爲數組元素和的邊界,即假設所有的連續數組的和都不會超過mid值。假如按照這種方式得到的分割結果大於了規定的m個,則說明mid值作爲最大元素和上界並不能夠做到只分割出m個子數組,因此最大元素和上界一定在mid和有界中間。同理,假如按照這種方式得到的分割結果小於等於規定的m個,則說明mid值作爲最大元素和上界能夠滿足分割出m個子數組,但是可能還存在更優解。通過這種二分法思路得到的最後結果就是所需要的最小分割結果。
public int splitArray2(int[] nums, int m) {
long sum = 0;
int max = Integer.MIN_VALUE;
for(int i = 0 ; i<nums.length ; i++) {
max = Math.max(max, nums[i]);
sum += nums[i];
}
if(m == 1) {
return (int)sum;
}
long lft = max;
long rgt = sum;
while(lft <= rgt) {
long mid = (lft + rgt) / 2;
if(valid(nums, m, mid)) {
rgt = mid - 1;
}else {
lft = mid + 1;
}
}
return (int) lft;
}
public boolean valid(int[] nums, int m, long target) {
int count = 1;
long sum = 0;
for(int i = 0 ; i<nums.length ; i++) {
sum += nums[i];
if(sum > target) {
sum = nums[i];
count++;
if(count > m) {
return false;
}
}
}
return true;
}