文章目录
前言
英雄算法联盟 - 七月集训 已经开始 13 天,八月算法集训 将于 08月01日 正式开始,目前已经提前开始报名,报名方式参见(八月算法集训报名),想要参加的同学,建议提早报名,因为对于算法零基础的同学,会有一些提前的准备工作,比如需要 1 - 5 天的时间完成预训练 和 九日集训 提前养成刷题的习惯,再参加算法集训会更加有成效。
零、贪心思想
贪心我相信绝大多数人都不明白是怎么回事,因为没有固定的范式去套,有一类贪心,是依附于枚举算法的,比如:不枚举所有情况,只对一小部分的解进行枚举,判定可行性,然后找到最优解,就一定是全局最优解。
一般贪心会配合 二分查找、双指针、前缀和 等等这些基础算法进行实现,就像求最大的最小值,最小的最大值,有经验的同学,第一步想到的肯定是二分枚举答案,这道题就是这样。
给定一个非负整数数组和一个整数 m,求将这个数组分割成 m 个,非空连续子数组,并且让子数组各自和的最大值最小,求这个值。抖音纯享视频版本👉🏻 https://v.douyin.com/iPVghYW/
一、O(10^9 n) 错误版本
实现一个 check 函数,用于检测将数组 nums 分割成 m 份,每份的和不大于 val 是否可行。如果可行返回 True;否则返回 False。
定义子数组和 sum 初始为 0,遍历数组,将数组每个元素累加到 sum 上,如果 sum 已经大于 val ,则当前的 v 作为新的子数组的第一个元素,m 减一,当 m 小于 1,代表已经超过了 m 个子数组,直接返回 False;如果遍历完数组还没有返回,则返回 True,代表可以分割成满足条件的 m 个子数组。
然后从 0 枚举到 10 的 9 次,依次调用 check 函数,一旦返回 True,那么 i 就是我们要求的答案,直接返回即可。
运行,没什么问题,提交!
class Solution:
def check(self, nums, m, val):
sum = 0
for v in nums:
sum += v
if sum > val:
sum = 0
sum += v
m -= 1
if m < 1:
return False
return True
def splitArray(self, nums, m):
for i in range(0, 1000000000):
if self.check(nums, m, i):
return i
错辣!嘶……
二、O(10^9 n) 超时版本
哦哦哦哦哦~ 如果某个数已经大于 val,则直接返回 False,运行!没什么问题,提交!
class Solution:
def check(self, nums, m, val):
sum = 0
for v in nums:
if v > val:
return False
sum += v
if sum > val:
sum = 0
sum += v
m -= 1
if m < 1:
return False
return True
def splitArray(self, nums, m):
for i in range(0, 1000000000):
if self.check(nums, m, i):
return i
超时啦!嘶
三、O(nlogm) 错误边界
题目要求的是最小值,也就是大于等于这个最小值的情况下,都是可以满足条件的。所以答案满足单调性,就可以用二分来枚举答案啦。 定义左边界为 0,右边界为 10 的 9 次,把结果存储在 ans 中,然后迭代枚举二分的区间。取一个二分中点 mid 如果 mid 值,那么 mid 必然是一个可行解,赋值给 ans。 最优解定位在区间 [l, mid-1] 中,所以更新右区间 r 为 mid - 1,否则最优解定位在区间 [mid+1, r] 中,所以更新左区间 l 为 mid + 1,最后返回 ans 就是我们要求的解了。运行!没什么问题,提交!
class Solution:
def check(self, nums, m, val):
sum = 0
for v in nums:
if v > val:
return False
sum += v
if sum > val:
sum = 0
sum += v
m -= 1
if m < 1:
return False
return True
def splitArray(self, nums, m):
l = 0
r = 1000000000
ans = -1
while l < r:
mid = (l + r) // 2
if self.check(nums, m, mid):
ans = mid
r = mid - 1
else:
l = mid + 1
return ans
又错辣!嘶……看看哪组数据错了???
四、O(nlogm) 过啦
哦哦哦哦哦 ~ 边界问题,l 需要小于等于 r,并不是小于 r,这是二分枚举的常见错误。
class Solution:
def check(self, nums, m, val):
sum = 0
for v in nums:
if v > val:
return False
sum += v
if sum > val:
sum = 0
sum += v
m -= 1
if m < 1:
return False
return True
def splitArray(self, nums, m):
l = 0
r = 1000000000
ans = -1
while l <= r:
mid = (l + r) // 2
if self.check(nums, m, mid):
r = mid - 1
ans = mid
else:
l = mid + 1
return ans
不测啦直接提交!过啦!
五、O(nlogm) 切换为C语言
没有击败 100% 的人?不甘心!算算了,用C语言来敲一遍,毕竟C语言是世界上最好的语言,想必一定不会让我失望。
同样实现一个 check 函数,定义 sum 初始化为 0,遍历数组,将数组每个元素累加到 sum 上。
当数组元素超过 v 时,直接返回 false,否则如果 sum 比 v 大,则将数组进行分割,m 减一,判断 m 等于 0,则直接返回 false;如果遍历完数组都没有返回,则直接返回 true。
然后就是二分的过程了,定义左右边界,定义最终的答案,while 循环进行迭代,不断找到最优解,最终的结果存储在 ans 中。
迭代结束,喝一口水,返回 ans 就可以了,运行没什么问题,提交!
bool check(int *a, int n, int m, int v) {
int sum = 0;
for(int i = 0; i < n; ++i) {
sum += a[i];
if (a[i] > v) {
return false;
}
if (sum > v) {
m--;
sum = a[i];
if(!m) {
return false;
}
}
}
return true;
}
int splitArray(int* a, int n, int m){
int l = 0, r = 1000000000;
int ans = -1;
while(l <= r) {
int mid = (l + r) >> 1;
if (check(a, n, m, mid)) {
ans = mid;
r = mid - 1;
}else {
l = mid + 1;
}
}
return ans;
}
过啦!4ms
六、O(nlogm) 打败了100%的人
只打败了 57.14% 的人,内存也十分的拉胯,不甘心啊,怎么回事,怎么样才能打败他们呢?好像也没有什么优化空间了呀,难道就这么放弃了吗?真的要放弃了吗? 要在这里放弃了吗?
我一定还遗漏了什么?是什么呢?是什么呢?
等等,左右区间是可以预先算出来的呀,左区间一定是数组元素的最大值,右区间一定是数组元素的和,运行,没什么问题,提交!
bool check(int *a, int n, int m, int v) {
int sum = 0;
for(int i = 0; i < n; ++i) {
sum += a[i];
if (a[i] > v) {
return false;
}
if (sum > v) {
m--;
sum = a[i];
if(!m) {
return false;
}
}
}
return true;
}
int splitArray(int* a, int n, int m){
int l = 0, r = 0;
int ans = -1;
for(int i = 0; i < n; ++i) {
if (a[i] > l) l = a[i];
r += a[i];
}
while(l <= r) {
int mid = (l + r) >> 1;
if (check(a, n, m, mid)) {
ans = mid;
r = mid - 1;
}else {
l = mid + 1;
}
}
return ans;
}
过啦!
打败了全世界的人!!!