题目
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
示例:
输入: nums = [7,2,5,10,8] m = 2
输出: 18
很好的一道题,对于动态规划和二分查找都是比较经典的一种问法。
<可惜菜菜子一个都没想到,还是要多练习才对啊(T_T)>
方法一:动态规划
「将数组分割为 m 段,求……」是动态规划题目常见的问法。
f
[
i
]
[
j
]
\ f[i][j]
f[i][j]:将数组的前 i 个数分割为 j 段所能得到的最大连续子数组和的最小值。
考虑第 j 段的具体范围:
枚举 k,前 k 个数被分割为 j−1 段,第 k+1 到第 i 个数为第 j 段。
此时,这 j 段子数组中和的最大值,就等于
f
[
k
]
[
j
−
1
]
f[k][j-1]
f[k][j−1] 和
s
u
b
(
k
+
1
,
i
)
sub(k+1,i)
sub(k+1,i) 中的较大值,其中
sub
(
i
,
j
)
\textit{sub}(i,j)
sub(i,j)表示数组
nums
\textit{nums}
nums中下标落在区间
[
i
,
j
]
[i,j]
[i,j] 内的数的和。
注意,对于
sub
(
i
,
j
)
\textit{sub}(i,j)
sub(i,j)的求取,通过前缀和直接得到。
C++最值常量:
有符号基本整型:INT_MIN,INT_MAX
有符号长整型:LONG_MIN,LONG_MAX || LLONG_MAX
双精度实数:DBL_MIN,DBL_MAX
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
int n=nums.size();
vector<vector<long long>> f(n+1,vector<long long>(m+1,LLONG_MAX));
vector<long long> sub(n+1,0);
for(int i=1;i<=n;i++){
sub[i]=sub[i-1]+nums[i-1];
}
f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=min(i,m);j++){
for(int k=0;k<i;k++){
f[i][j]=min(f[i][j],max(f[k][j-1],sub[i]-sub[k]));
}
}
}
return int(f[n][m]);
}
};
方法二:二分查找+贪心
「使……最大值尽可能小」是二分搜索题目常见的问法。
当我们选定一个值 x,我们可以线性地验证是否存在一种分割方案,满足其最大分割子数组和不超过 x。
class Solution {
public:
bool check(vector<int>& nums, int x, int m) {
long long sum = 0;
int cnt = 1;
for (int i = 0; i < nums.size(); i++) {
if (sum + nums[i] > x) {
cnt++;
sum = nums[i];
} else {
sum += nums[i];
}
}
return cnt <= m;
}
int splitArray(vector<int>& nums, int m) {
long long left = 0, right = 0;
for (int i = 0; i < nums.size(); i++) {
right += nums[i];
if (left < nums[i]) {
left = nums[i];
}
}
while (left < right) {
long long mid = (left + right) >> 1;
if (check(nums, mid, m)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
};