python 贪心算法总结


贪心算法理论基础

选择每一阶段的局部最优,从而达到全局最优。选择贪心算法的关键在于:可以从局部最优推出整体最优。
验证能不能用贪心:

  • 举反例 (想不到反例的时候可以试一下)
  • 数学归纳法

贪心算法步骤:

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

分发饼干

局部最优:大饼干胃给胃口最大的
全局最优:喂饱尽可能多的小孩

# 先喂饱胃口小的
# 从小到大遍历饼干,如果最小的饼干不能喂饱最小的孩子,就找第二小的饼干,以此类推。不能反过来,因为找第二小的孩子的还是不能用最小的饼干满足。
class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        res = 0
        for i in range(len(s)):
            if res < len(g) and s[i] >= g[res]:
                res += 1
        return res
# 先喂饱胃口大的
# 从大到小遍历孩子,先找最大的孩子看最大的饼干是否满足,如果不满足,第二大的孩子再匹配最大的饼干,以此类推,不能反过来,因为第二大的饼干还是不能满足最大胃口的孩子。
class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        res = 0
        start = len(s) - 1
        for i in range(len(g) - 1, -1, -1):
            if start >= 0 and g[i] <= s[start]:
                res += 1
                start -= 1
        return res

摆动序列

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
    	# 题目里nums长度大于等于1,当长度为1时,其实到不了for循环里去,所以不用考虑nums长度
        for i in range(len(nums) - 1):
        prediff, curdiff, res = 0, 0, 1
        for i in range(len(nums) - 1):
            curdiff = nums[i + 1] - nums[i]
            # 差值为0时,不算摆动
            if curdiff * prediff <= 0 and curdiff != 0:
                res += 1
                # 如果当前差值和上一个差值为一正一负时,才需要用当前差值替代上一个差值
                prediff = curdiff
        return res

最大子序列和

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        res = -float('inf')
        count = 0
        # 一旦和的值小于0,重新开始计算和
        for i in range(len(nums)):
            count += nums[i]
            if count > res:
                res = count
            if count < 0:
                count = 0
        return res

买卖股票的最佳时机ii

最终利润可以分解为每天的利润,收集真利润的区间,就是股票买卖的区间。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 每天的利润都可以拆解出来
        profit = []
        sum_profit = 0
        for i in range(len(prices) - 1):
            # 买入的钱减去卖出的钱
            day_profit = prices[i + 1] - prices[i]
            profit.append(day_profit)
        print(profit)
        for day_profit in profit:
            if day_profit > 0:
                sum_profit += day_profit
        return sum_profit

跳跃游戏

局部最优解:每次取最大跳跃步数
整体最优:最后得到整体最大覆盖范围,看是否能到终点

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        cover = 0
        i = 0
        if len(nums) == 1: return True
        while i <= cover:
            cover = max(cover, i + nums[i])
            if cover >= len(nums) - 1: return True
            i += 1
        return False

跳跃游戏ii

局部最优:当前可移动的距离尽可能的多
整体最优:一步尽可能的多,从而达到最小步数
题解:从覆盖范围出发,以最小的步数增加覆盖范围,一旦覆盖范围覆盖到了终点 ,得到的就是最小步数。需统计两个覆盖范围,一个是当前的最大覆盖和下一步的最大覆盖。

class Solution:
    def jump(self, nums: List[int]) -> int:
        if len(nums) == 1: return 0
        jump = 0
        next_jump = 0
        cur_jump = 0
        for i in range(len(nums)):
            next_jump = max(i + nums[i], next_jump)
            # 这里一开始jump就加了个1,刚好补上最后的一跳,当next_jump >= len(nums) - 1 时,需要再跳一次到最后一个点
            if i == cur_jump:
                if cur_jump != len(nums) - 1:
                    jump += 1
                    cur_jump = next_jump
                    if next_jump >= len(nums) - 1: break
        return jump

K次取反后最大化数组和

局部最优: 让绝对值大的负数变为正数,当前数值达到最大
整体最优:整个数组和达到最大

class Solution:
    def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
        nums.sort()
        i = 0
        # 找绝对值最大的负数变为正数,多出来的K用来转换最小的数字
        while k > 0 and i < len(nums) - 1 and nums[i] < 0:
            nums[i] = -nums[i]
            k -= 1
            i += 1
        # 这里是比较转换后的nums[i-1] 和nums[i]的大小,即正负交界处的最小值
        if nums[i] > nums[i - 1]:
            i -= 1
        while k != 0:
            nums[i] = -nums[i]
            k -= 1
        return sum(nums)

加油站

  • 情况一:如果gas的总和小于cost的总和,那么无论从哪里出发,一定是跑不了一圈的
  • 情况二:rest[i] = gas[i] - cost[i] 为一天剩下的油, i 从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么起点就是0
  • 情况三:如果累加的最小值是负数,骑车就要从非0节点出发,从后向前,看哪个节点能将这个负数填平,能把这个负数填平的节点就是出发节点。

局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要j+1,因为从j开始一定不行
全局最优:找到可以跑一圈的起始位置

# 解法一:
class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        if sum(gas) < sum(cost): return -1
        min_sum = float('inf')
        cur_sum = 0

        # 求出累计过程中的最小值
        for i in range(len(gas)):
            cur_sum += gas[i] - cost[i]
            min_sum = min(cur_sum, min_sum)
        
        # 如果累计过程中的最小值都大于0,说明直接从0开始就行
        if min_sum >= 0: return 0
        
        # 往前找能填满累计最小值的累计值,直到大于等于0
        for i in range(len(gas) - 1, 0 , -1):
            min_sum += gas[i] - cost[i]
            if min_sum >= 0:
                return i
        return -1
# 解法二
class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        if sum(gas) < sum(cost): return -1
        rest = []
        cur_sum = 0
        start = 0
        # 相当于遍历,如果走到i变成负的了,就从i+1开始跑
        for i in range(len(gas) - 1):
           cur_sum += gas[i] - cost[i]
           if cur_sum < 0:
               cur_sum = 0
               start = i + 1
        return start
        

分发糖果

确定一边之后再确定另一边,例如比较每个孩子的左边,再比较右边,如果两边一起考虑会顾此失彼。
先确定右边评分大于左边的情况(从前向后遍历)
局部最优:只要右边评分比左边大,右边的孩子就多一颗糖果
全局最优:评分最高的右孩子获得比左边孩子更多的糖果
再确定左孩子大于右孩子的情况(从后向前遍历)
确定左孩子大于右孩子一定要从后向前遍历!

贪心策略:

  • 从做到右遍历,只比较右边孩子评分比左边大的情况
  • 做右到左遍历,只比较左边孩子评分比右边大的情况
# 核心:先比较孩子的左边,再比较孩子的右边
class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        candy = [1 for _ in range(n)]
        # 从前向后,右孩子和做孩子比较
        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                candy[i] = candy[i - 1] + 1
        # 从后向前,左孩子和右孩子比较
        for i in range(n - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                # 可能第一轮比较下来左孩子的分数已经很高了
                candy[i] = max(candy[i], candy[i + 1] + 1)
        return sum(candy)

柠檬水找零

局部最优:遇到账单20,优先消耗10美元,完成本次找零
全局最优:完成全部账单的找零

class Solution:
    def lemonadeChange(self, bills: List[int]) -> bool:
        bill_dict = {5: 0, 10: 0, 20: 0}
        for bill in bills:
            if bill == 5:
                bill_dict[5] += 1
            elif bill == 10:
                bill_dict[10] += 1
                bill_dict[5] -= 1
            else :
                bill_dict[20] += 1
                if bill_dict[10] > 0:
                    bill_dict[10] -= 1
                    bill_dict[5] -= 1
                else:
                    bill_dict[5] -= 3
            if bill_dict[5] < 0:
                return False
        return True

根据身高重建队列

先根据身高排序,然后根据K插入
**局部最优:**优先按身高高的people的k来插入,插入后的people满足队列属性
全局最优: 最后都做完插入操作,整个队列满足题目队列属性

class Solution:
    def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
        # 先按照h维度的身高顺序从高到低排序。确定第一个维度
        # lambda返回的是一个元组:当-x[0](维度h)相同时,再根据x[1](维度k)从小到大排序
        people.sort(key = lambda x:(-x[0], x[1]))
        que = []

        # 根据每个元素的第二个维度k,贪心算法,进行插入
        # people已经排序过了:同一高度时k值小的排前面。
        for p in people:
            que.insert(p[1], p)
        
        return que

用最少数量的箭引爆气球

局部最优:一箭能射最多
全局最优:弓箭最少

class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        if len(points) == 0: return 0
        points.sort(key=lambda x: x[0])
        min_arrow = 1
        for i in range(1, len(points)):
            if points[i - 1][1] < points[i][0]:  # 气球i和气球i-1不挨着,注意这里不是>=
                min_arrow += 1
            else:
                # 更新重叠气球最小右边界,上面的右边界有可能比下面的大,会错过下面的气球
                points[i][1] = min(points[i][1], points[i - 1][1]) 
        return min_arrow

无重叠区间

  • 按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的
  • 按照左边界排序,就要从右向左遍历,因为左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。
class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        intervals.sort(key = lambda x: x[1])
        count = 0  # 记录重叠的个数
        end = intervals[0][1]
        for i in range(len(intervals)):
        	# 注意这里的 <= 数组的最后和end一样一定是也是重叠的
            if i != 0 and intervals[i][0] < end <= intervals[i][1]:
                count += 1
                continue
            end = intervals[i][1]
        return count

划分字母区间

题解:本题相当于找之前遍历过的所有字母的最远边界,说明这个边界就是分割点了

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        hash_map = [0] * 26
        # 用hash_map记录每个字符的最远距离
        for i in range(len(s)):
            hash_map[ord(s[i]) - ord('a')] = i
        res = []
        left = 0
        right = 0
        for i in range(len(s)):
            right = max(right, hash_map[ord(s[i]) - ord('a')])
            if i == right:
                res.append(right - left + 1)
                left = right + 1
        return res

合并区间

题解:
局部最优:先按左边界排序,每次合并都取最大的右边界,这样就可以合并更多的区间了
全局最优:合并所有重叠的区间

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        if not intervals: return []
        intervals.sort(key = lambda x: x[0])
        res = []
        left = intervals[0][0]
        right = intervals[0][1]
        for i in range(len(intervals)):
            # 只要比左边大就能合并
            if right >= intervals[i][0]:
                right = max(right, intervals[i][1])
            else:
                res.append([left, right])
                left = intervals[i][0]
                right = intervals[i][1]
        # 把最后一组合并进去
        res.append([left, right])
        return res

单调递增的数字

局部最优:遇到strNum[i -1] > strNum[i]的情况, 让strNum[i -1] --, 然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。
全局最优:得到小于等于N的最大单调递增的整数。

class Solution:
    def monotoneIncreasingDigits(self, n: int) -> int:
        a = list(str(n))
        for i in range(len(a)- 1, 0, -1):
            if int(a[i]) < int(a[i - 1]):
                a[i - 1] = str(int(a[i - 1]) - 1)
                a[i:] = '9' * (len(a) - i)
        return int(''.join(a))

买卖股票的最佳时机含手续费

  • 买入日期:其实很好想,遇到更低点就记录一下。
  • 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。

三种情况

  • 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润
  • 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
  • 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        res = 0
        min_price = prices[0]
        for i in range(1, len(prices)):
            if prices[i] < min_price:
                min_price = prices[i]
            elif prices[i] >= min_price and prices[i] <= min_price + fee:
                continue
            else:
                res += prices[i] - min_price - fee
                min_price = prices[i] - fee
        return res

监控二叉树

局部最优:让子节点的父节点按摄像头,所用摄像头最少
整体最优:全部摄像头数量最少

三种状态:

  • 0:改节点无覆盖
  • 1: 本节点有摄像头
  • 2:本节点有覆盖

单层逻辑:

  • 情况1:左右节点都有覆盖
  • 情况2:左右节点至少有一个无覆盖的情况
  • 情况3:左右节点至少有一个有摄像头
  • 情况4:头结点没有覆盖
class Solution:
    def minCameraCover(self, root: TreeNode) -> int:
        # 从下往上安装摄像头:跳过leaves这样安装数量最少,局部最优 -> 全局最优
        # 先给leaves的父节点安装,然后每隔两层节点安装一个摄像头,直到Head
        # 0: 该节点未覆盖
        # 1: 该节点有摄像头
        # 2: 该节点有覆盖
        self.result = 0
        if self.travelsal(root) == 0:
            self.result += 1
        return self.result
    
    def travelsal(self, cur):
        if not cur: return 2

        # 从下往上遍历:后序(左右中)
        left = self.travelsal(cur.left)
        right = self.travelsal(cur.right)

        # Case 1:
        # 左右节点都有覆盖  
        if left == 2 and right == 2:
            return 0

        # Case 2:
        # left == 0 && right == 0 左右节点无覆盖
        # left == 1 && right == 0 左节点有摄像头,右节点无覆盖
        # left == 0 && right == 1 左节点有无覆盖,右节点摄像头
        # left == 0 && right == 2 左节点无覆盖,右节点覆盖
        # left == 2 && right == 0 左节点覆盖,右节点无覆盖
        elif left == 0 or right == 0:
            self.result += 1
            return 1

        # Case 3:
        # left == 1 && right == 2 左节点有摄像头,右节点有覆盖
        # left == 2 && right == 1 左节点有覆盖,右节点有摄像头
        # left == 1 && right == 1 左右节点都有摄像头
        elif left == 1 or right == 1:
            return 2
  • 7
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值