如何用贪心的思想来解决问题?

前言

英雄算法联盟 - 七月集训 已经开始 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;
}

过啦!
打败了全世界的人!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值