【leetcode】贪心法刷题tricks

什么是贪心

贪心的本质是选择每一阶段的局部最优,从而达到全局最优。例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?指定每次拿最大的,最终结果就是拿走最大数额的钱。每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。

什么时候用贪心

贪心算法唯一的难点就是如何通过局部最优,推出整体最优。如果手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。如果不可行,可能需要动态规划。

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

序列问题

LeetCode376题 摆动序列

序列中峰值组成的序列,即为摆动序列的最长子序列,需要注意平坡的情况(包括单调平坡、峰值平坡)

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        if len(nums) <= 1:
            return len(nums)
        preDiff = 0
        result = 1  # 记录峰值的个数,初始为1(默认最右边的元素被视为峰值)
        for i in range(len(nums) - 1):
            curDiff = nums[i + 1] - nums[i]
            # 遇到峰值
            if (preDiff <= 0 and curDiff > 0) or (preDiff >= 0 and curDiff < 0):
                result += 1
                preDiff = curDiff  # 只在摆动变化的时候更新preDiff
        return result

LeetCode738题 单调自增的数字

若出现递减的情况,需要让递减开始位置数字减1,后面所有位置补9;其他情况直接取对应位置数字即可。正序遍历解法:

class Solution:
    def monotoneIncreasingDigits(self, n: int) -> int:
        res = ''
        s = str(n)
        # pre代表最后一个处理完结果的位置
        pre = -1
        for i in range(len(s) - 1):
            # 递增将对应数字直接写入结果;相等直接跳过;递减直接-1补9 跳出循环
            if int(s[i]) < int(s[i + 1]):
                res += s[pre + 1: i + 1]
                pre = i
            elif int(s[i]) > int(s[i + 1]):
                # 如果-1位置是1,则不补0,直接补9,补0会导致数字不合法
                if s[pre + 1] == '1':
                    res += (len(s) - pre - 2) * '9'
                else:
                    res += str(int(s[pre + 1]) - 1) + (len(s) - pre - 2) * '9'
                pre = len(s) - 1
                break
        # 补充处理未处理的数字
        if pre < len(s) - 1:
            res += s[pre + 1:]
        return int(res)

LeetCode122题 买卖股票最佳时机II

只要遇到递增序列,就进行收益计算

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        res = 0
        for i in range(1, len(prices)):
            # 只要遇到递增序列,就进行收益计算
            if prices[i] >= prices[i - 1]:
                res += prices[i] - prices[i - 1]
        return res

LeetCode53题 最大子序和

用一个变量记录连续和,如果和为正,则持续相加;否则直接更新,中间记录最大的连续和

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        res = float('-inf')
        cur_sum = 0
        for i in range(len(nums)):
            if cur_sum > 0:
                cur_sum += nums[i]
            else:
                cur_sum = nums[i]
            res = max(res, cur_sum)
        return res

LeetCode134题 加油站

假设1:如果total_sum>=0,那么一定可以跑完一圈

假设2:当前累加和cur_sum一旦小于0,出发位置至少要是i+1,从i之前开始一定不行

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        res = 0
        total_sum = 0
        cur_sum = 0
        for i in range(len(gas)):
            cur_sum += gas[i] - cost[i]
            total_sum += gas[i] - cost[i]
            if cur_sum < 0:
                res = i + 1
                cur_sum = 0
        if total_sum < 0:
            return -1
        return res

LeetCode1005题 K次取反后最大化的数组和

可对数组排序(也可对数组绝对值排序),然后按顺序处理

class Solution:
    def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
        # 四种情形:都是正;都是负;有正有负,负数量>=k;都是负;有正有负,负数量<k
        nums.sort()
        # 为了方便处理都是负数情况
        nums.append(float('inf'))
        res = 0
        for i in range(len(nums) - 1):
            # 找到正负数分隔点
            if nums[i] < 0 and nums[i + 1] >= 0:
                if k == 0:
                    res += nums[i]
                elif k % 2 == 1:
                    res -= nums[i]
                elif - nums[i] <= nums[i + 1]:
                    res += nums[i]
                elif - nums[i] > nums[i + 1]:
                    res -= nums[i] + 2 * nums[i + 1]
            # 处理负数
            elif nums[i] < 0 and nums[i + 1] < 0:
                if k > 0:
                    k -= 1
                    res -= nums[i]
                else:
                    res += nums[i]
            # 处理都是正数情况
            elif i == 0 and nums[i] >= 0 and k % 2 == 1:
                res -= nums[i]
            # 处理正数
            else:
                res += nums[i]
        return res

两个维度权衡问题

LeetCode135题 分发糖果

从左右两个方向分别计算需要的糖果数,然后取两个方向需要的最大糖果数

class Solution:
    def candy(self, ratings: List[int]) -> int:
        res = 0
        c = [1] * len(ratings)
        for i in range(1, len(ratings)):
            if ratings[i] > ratings[i - 1]:
                c[i] = c[i - 1] + 1
        c_reverse = [1] * len(ratings)
        for i in range(len(ratings) - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                c_reverse[i] = c_reverse[i + 1] + 1
        for i in range(len(ratings)):
            res += max(c[i], c_reverse[i])
        return res

LeetCode406题 根据身高重建队列

按照身高降序排序、按照人数升序排序,这样排序后,后面的元素不会影响前面的元素,就可以通过插入后面元素,来重建队列数组

class Solution:
    def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
        res = []
        people.sort(key=lambda x: (- x[0], x[1]))
        for p in people:
            k = p[1]
            # 插入第k个位置,以满足人数要求
            res = res[:k] + [p] + res[k:]
        return res

区间问题

LeetCode55题 跳跃游戏

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        if len(nums) == 1:
            return True
        # cover代表可以覆盖的最大范围
        cover = 0
        for i in range(len(nums) - 1):
            # 确保可以跳跃到这个位置
            if i <= cover:
                # 更新覆盖范围
                cover = max(cover, i + nums[i])
                # 如果已经覆盖到终点,就返回
                if cover >= len(nums) - 1:
                    return True
        return False

LeetCode45题 跳跃游戏II

class Solution:
    def jump(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return 0
        res = 0
        # 记录当前跳跃的最大范围
        cover = 0
        # 记录下次跳跃最大的范围
        next_cover = 0
        for i in range(len(nums)):
            next_cover = max(next_cover, i + nums[i])
            # 当循环到当前跳跃的最大范围时,还没有跳到终点,需要再进行跳跃
            if i == cover:
                cover = next_cover
                res += 1
                # 判断跳跃后,是否能跳到终点
                if cover >= len(nums) - 1:
                    return res

LeetCode452题 用最少数量的箭引爆气球

class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        res = 1
        # 按左区间排序后,判断相邻区间是否重合只需用 i-1的右区间 vs i的左区间
        points.sort(key=lambda x: x[0])
        for i in range(1, len(points)):
            # 当前区间和之前区间不重合,则需要增加弓箭
            if points[i - 1][1] < points[i][0]:
                res += 1
            # 当前区间和之前区间重合,则需要更新区间右边界,以判断和之后区间是否重合
            else:
                points[i][1] = min(points[i][1], points[i - 1][1])
        return res

 LeetCode56题 合并区间

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort(key=lambda x: x[0])
        res = [intervals[0]]
        # 记录当前重叠区间,最大右边界
        cur_max_right = intervals[0][1]
        for i in range(1, len(intervals)):
            if cur_max_right < intervals[i][0]:
                res.append(intervals[i])
                cur_max_right = intervals[i][1]
            else:
                if cur_max_right < intervals[i][1]:
                    res[-1][1] = intervals[i][1]
                    cur_max_right = intervals[i][1]
        return res

贪心和二叉树的结合

LeetCode968题 监督二叉树

贪心思路:尽量在叶子结点的父节点放摄像头,然后尽量每隔两个节点放个摄像头

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def minCameraCover(self, root: Optional[TreeNode]) -> int:
        self.res = 0
        # 若根节点没被监控,还需要安装一个摄像头
        if self.traversal(root) == 0:
            self.res += 1
        return self.res

    def traversal(self, root):
        # 返回0代表没被监控;1表示被监控;2表示安装摄像头
        if root is None:
            return 1
        left = self.traversal(root.left)
        right = self.traversal(root.right)
        # 后续遍历
        # 若左右孩子被监控,则间隔两个节点放摄像头,节点返回没被监控
        if left == 1 and right == 1:
            return 0
        # 若左右孩子至少一个没被监控,则节点必须安装摄像头,结果+1
        elif left == 0 or right == 0:
            self.res += 1
            return 2
        # 若左右孩子至少一个安装摄像头,则节点已被监控
        elif left == 2 or right == 2:
            return 1
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值