【贪心算法】leetcode刷题

贪心算法无固定套路。
核心思想:先找局部最优,再扩展到全局最优。

455.分发饼干

在这里插入图片描述
两种思路:
1、从大到小。局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。先遍历的胃口,在遍历的饼干
在这里插入图片描述

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g = sorted(g,reverse=True)
        s = sorted(s,reverse=True)
        count = 0
        si = 0
        for gi in g:
            if si<len(s) and s[si]>=gi:   # 饼干要大于胃的容量,才能喂饱
                count+=1
                si+=1             
        return count

2、从小到大。小饼干先喂饱小胃口。两个循环的顺序改变了,先遍历的饼干,在遍历的胃口,这是因为遍历顺序变了,我们是从小到大遍历。

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g = sorted(g)
        s = sorted(s)
        count = 0
        gi = 0
        for si in s:
            if gi<len(g) and g[gi]<=si:  # 胃的容量要小于等于饼干大小才能喂饱
                count+=1
                gi+=1    
                
        return count

376. 摆动序列

在这里插入图片描述
具体实例:
在这里插入图片描述
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
**整体最优:**整个序列有最多的局部峰值,从而达到最长摆动序列。

考虑几种情况:

  • 情况一:上下坡中有平坡
  • 情况二:数组首尾两端
  • 情况三:单调坡中有平坡

情况一:上下坡中有平坡
在这里插入图片描述
它的摇摆序列长度是多少呢? 其实是长度是 3,也就是我们在删除的时候 要不删除左面的三个 2,要不就删除右边的三个 2。
在这里插入图片描述
在图中,当 i 指向第一个 2 的时候,prediff > 0 && curdiff = 0 ,当 i 指向最后一个 2 的时候 prediff = 0 && curdiff < 0。
如果我们采用,删左面三个 2 的规则,那么 当 prediff = 0 && curdiff < 0 也要记录一个峰值,因为他是把之前相同的元素都删掉留下的峰值。
所以我们记录峰值的条件应该是: (preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0),为什么这里允许 prediff == 0 ,就是为了 上面我说的这种情况。

情况二:数组首尾两端
例如序列[2,5],如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。
因为我们在计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i])的时候,至少需要三个数字才能计算,而数组只有两个数字。
这里我们可以写死,就是 如果只有两个元素,且元素不同,那么结果为 2。
在这里插入图片描述

情况三:单调坡度有平坡
在这里插入图片描述

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        count = 1
        # 情况二,小于等于2的情况,可以写死
        if len(nums)<2:
            return len(nums)
        if len(nums)==2:
            if nums[-1]-nums[0]==0:
                return 1
            return len(nums)
        # 情况一以及情况三:
        prediff = 0
        for i in range(0,len(nums)-1):
            # if i>0:
            #     prediff = nums[i-1]-nums[i]
            curdiff = nums[i]-nums[i+1]
            if (prediff<=0 and curdiff>0) or (prediff>=0 and curdiff<0):
                count+=1
                prediff = curdiff   # 更新prediff
        return count  

122.买卖股票的最佳时机 II

在这里插入图片描述
如果想到其实最终利润是可以分解的,那么本题就很容易了!
如何分解呢?
假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。
相当于==(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])==。
此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑!
那么根据 prices 可以得到每天的利润序列:(prices[i] - prices[i - 1])…(prices[1] - prices[0])。

  • 局部最优:收集每天的正利润,全局最优:求得最大利润。
    在这里插入图片描述
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [0]*(n-1)
        for i in range(1,n):
            dp[i-1] = prices[i]-prices[i-1]
        res = 0
        for i in dp:
            if i>0:
                res+=i
        return res

55. 跳跃游戏

在这里插入图片描述
在这里插入图片描述
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。
而 cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)。
如果 cover 大于等于了终点下标,直接 return true 就可以了

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

45.跳跃游戏 II

在这里插入图片描述
如果移动下标等于当前覆盖最大距离下标, 需要再走一步(即 ans++),因为最后一步一定是可以到的终点。(题目假设总是可以到达数组的最后一个位置),如图:
在这里插入图片描述
如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。如图:
在这里插入图片描述

class Solution:
    def jump(self, nums: List[int]) -> int:
		cur_distance = 0  # 当前覆盖的最远距离下标
        ans = 0  # 记录走的最大步数
        next_distance = 0  # 下一步覆盖的最远距离下标
        
        for i in range(len(nums) - 1):  # 注意这里是小于len(nums) - 1,这是关键所在
            next_distance = max(nums[i] + i, next_distance)  # 更新下一步覆盖的最远距离下标
            if i == cur_distance:  # 遇到当前覆盖的最远距离下标
                cur_distance = next_distance  # 更新当前覆盖的最远距离下标
                ans += 1
        
        return ans

1005.K次取反后最大化的数组和

在这里插入图片描述

class Solution:
    def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
        nums.sort(key=lambda x:abs(x),reverse=True)  # 按照绝对值的个数排序,且从大到小,为了后面先将前k个大负数进行翻转为正数,保证和最大
        for i in range(len(nums)):
            if nums[i]<0 and k>0:   # 将负数都翻转为正数
                k-=1
                nums[i] = -nums[i]
                
        # 现在全部都是正数了
        if k%2==1: # 如果还剩奇数个,则将最小的正数翻转
            nums[-1] = -nums[-1]

        # 如果是偶数,其实不用管,因为随便翻转个正数偶数次就可以了,不影响最后结果
        return sum(nums)

134. 加油站

135.分发糖果

在这里插入图片描述

  • 从左到右:
    如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1
    在这里插入图片描述
  • 从右到左考虑:
    再确定左孩子大于右孩子的情况(从后向前遍历)
    遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢?
    因为 rating[5]与rating[4]的比较 要利用上 rating[5]与rating[6]的比较结果,所以 要从后向前遍历。
    如果从前向后遍历,rating[5]与rating[4]的比较 就不能用上 rating[5]与rating[6]的比较结果了 。如图:
    在这里插入图片描述在这里插入图片描述

如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。
那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。

所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多。

class Solution:
    def candy(self, ratings: List[int]) -> int:
        k = len(ratings)
        dp = [1]*k
        # 从左到右
        for i in range(k):
            if i>0 and ratings[i]>ratings[i-1]:
                dp[i] = dp[i-1]+1
        # 从右到左
        for i in range(k-2,-1,-1):
            if ratings[i]>ratings[i+1]:
                dp[i] = max(dp[i+1]+1,dp[i])  # 关键点 见解析
        return sum(dp)

860. 柠檬水找零

在这里插入图片描述

class Solution:
    def lemonadeChange(self, bills: List[int]) -> bool:
        if bills[0] != 5:
            return False
        n = len(bills)
        counter = {5: 0}
        for i in range(n):
            remain = bills[i]
            # 添加
            if remain in counter.keys():
                counter[remain] += 1
            else:
                counter[remain] = 1

            # 删除 # 若有20的,先找10元的。
            if remain // 10 > 1 and 10 in counter.keys():
                if counter[10] >= (remain // 10) - 1:  # 先找10元的。
                    counter[10] = counter[10] - ((remain // 10) - 1)
                    remain = ((remain // 10) - 1) * 10

            # 删除 若是10/20,则进行删除操作
            if remain // 5 > 1:
                if counter[5] >= (remain // 5) - 1:  # 再找5元的。剩余的5个数,大于需要找回的。
                    counter[5] = counter[5] - ((remain // 5) - 1)
                else:
                    return False
        return True
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值