代码随想录算法训练营第31、32天(贪心篇) | 376.摆动序列 53.最大子数组和 122.买卖股票的最佳时机II 55.跳跃游戏 45.跳跃游戏II 1005.K次取反后最大化的数组和

代码随想录系列文章目录

贪心篇


文章目录

  • 代码随想录系列文章目录
  • 贪心算法的基础思路
  • 376.摆动序列
    • 1. dp
    • 2.贪心解法
  • 53.最大子数组和
    • 1.dp解法
    • 贪心解法
  • 122.买卖股票的最佳时机II
    • 贪心解法
  • 55.跳跃游戏
    • 贪心解法
  • 45.跳跃游戏II
    • 贪心写法1
    • 贪心写法2
  • 1005.K次取反后最大化的数组和
    • 贪心解法


贪心算法的基础思路

1.贪心的本质是选择每一阶段的局部最优,从而达到全局最优

有没有啥套路呢?不好意思,贪心没套路,就刷题而言,如果感觉好像局部最优可以推出全局最优,然后想不到反例,那就试一试贪心吧!

2.贪心一般和dfs或者dp结合

376.摆动序列

题目链接

1. dp

状态定义:dp[i][0] dp[i][1] 代表以i下标结尾的序列此时是波峰/波谷时,最长摆动序列的长度

状态转移:
如果说波峰 nums[i]>nums[j]: dp[i][0] = max(dp[i][0], dp[j][1]+1) j 如果说波谷 nums[i]

初始化:
dp[0][1] 和 dp[0][0] 都应该等于1 , dp数组是一个[[1,1],[,]…]这样的数组,其实这道题里应该直接把dp数组全初始化为[1,1], 这样的话如果摆动数组全是一样的值的话,返回的结果一直是1,不用特判这种情况

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        dp = [[1,1] for _ in range(len(nums))]

        for i in range(1,len(nums)):
            for j in range(i):
                if nums[i] > nums[j]: dp[i][0] = max(dp[i][0], dp[j][1]+1)
                if nums[i] < nums[j]: dp[i][1] = max(dp[i][1], dp[j][0]+1)
        
        return max(dp[len(nums)-1][0], dp[(len(nums)-1)][1])

2.贪心解法

本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序
如图所示
其实就是转化成求峰的个数
代码随想录算法训练营第31、32天(贪心篇) | 376.摆动序列 53.最大子数组和 122.买卖股票的最佳时机II 55.跳跃游戏 45.跳跃游戏II 1005.K次取反后最大化的数组和_第1张图片
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。

整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。

实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)

这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点。

统计峰值的时候,数组最左面和最右面是最不好统计的。

我们可以一开始直接把res = 1,这个1就是最右端的峰值,图中的话就是最后一个 8,因为两端天然就可以加进序列

但是最左端的那个1我们怎么处理呢,我们可以用prec <= 0 / >=0, 因为一开始我们把prec = 0, 此时无论计算出第一对差值是什么 都会进入if 条件,把res += 1,这样其实就计算了最左端的元素波峰

然后我们只统计波峰,也就是两次差值异号的情况,并且只在异号的时候更新一下prec就好了,没必要每次更新

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        prec, curc, res = 0, 0, 1
        for i in range(len(nums)-1):
            curc = nums[i+1] - nums[i]
            if (prec <= 0 and curc > 0) or (prec >=0 and curc < 0):
                res += 1
                prec = curc
        return res

53.最大子数组和

题目链接

1.dp解法

这道题的dp解法很好想,是一个一维的dp,和所给的数据有很大的关系。我们可以看出,最大子数组,它要求连续。而且数组中既有正数,也有负数。

状态定义:我们把dp[i],定义成以第i个元素为结尾的子数组的最大和

状态转移:我们可以想到,如果遍历到i了,前面的连续子数组和如果是正数,加上这个nums[i],肯定会变大是吧;但如果前面的连续子数组和是负数呢?加上nums[i]肯定会变小吧。那么我们dp[i]= max(dp[i-1] + nums[i], nums[i]), 不就好了。我们肯定不亏是吧,不让它变小,这是不是也是一种贪心呢?

初始化:我们的dp[0] = nums[0]。 再一个,这道题的解,并不是在dp[n-1]的。我们需要拿一个变量记录一下每一个dp[i], 保留里面最大的那个。这个res 我们也要初始化成nums[0]

代码:

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [0 for _ in range(n)]
        dp[0] = nums[0]
        res = dp[0]
        for i in range(1,n):
            dp[i] = max(dp[i-1]+nums[i], nums[i])
            if dp[i] > res: res = dp[i]
        return res

贪心解法

其实在上面的dp解法中我已经说到了贪心思想的应用的点了,

这道题的贪心写法是脱胎于两个指针遍历的暴力的写法的。我们在遍历的时候,拿一个cnt 去记录当前累加的和,拿res去记录最大的cnt。

那么怎么贪呢,如果此时cnt < 0我们就即时舍掉,下一个遍历位置开始我们从新开始,不让负数加在我们结果上

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        cnt = 0
        res = nums[0]
        for i in range(len(nums)):
            cnt += nums[i]
            if cnt > res: res = cnt
            if cnt < 0: cnt = 0
        return res

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])。

如图:
代码随想录算法训练营第31、32天(贪心篇) | 376.摆动序列 53.最大子数组和 122.买卖股票的最佳时机II 55.跳跃游戏 45.跳跃游戏II 1005.K次取反后最大化的数组和_第2张图片

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        res = 0
        for i in range(1, len(prices)):
            res += max(prices[i] - prices[i-1], 0)
        return res 

55.跳跃游戏

题目链接
刚看到本题一开始可能想:当前位置元素如果是3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢?

其实跳几步无所谓,关键在于可跳的覆盖范围

不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。

这个范围内,别管是怎么跳的,反正一定可以跳过来。

那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!

所以要动态维护一个范围cover , 这个范围是 i + nums[i] (i <= cover)

每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围

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

局部最优推出全局最优,找不出反例,试试贪心!

如图:
代码随想录算法训练营第31、32天(贪心篇) | 376.摆动序列 53.最大子数组和 122.买卖股票的最佳时机II 55.跳跃游戏 45.跳跃游戏II 1005.K次取反后最大化的数组和_第3张图片
i每次移动只能在cover的范围内移动,每移动一个元素,cover得到该元素数值(新的覆盖范围)的补充,让i继续移动下去。

而cover每次只取 max(该元素数值补充后的范围, cover本身范围)。

如果cover大于等于了终点下标,直接return true就可以了。

贪心解法

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        cover = 0
        if len(nums) == 1: return True
        i = 0
        # python不支持动态修改for循环中变量,使用while循环代替
        while i <= cover:
            cover = max(i + nums[i], cover)
            if cover >= len(nums) - 1: return True
            i += 1
        return False

45.跳跃游戏II

题目链接
55.跳跃游戏,一直在动态更新一个变量cover, 也就是现在可跳的最大范围,如果说它>= 数组最后一个下标,就可以。由于这个cover是动态更新的,所以遍历数组最好用while.

然后45.跳跃游戏2,问的是跳跃的最小步数,还是需要动态更新最大跳跃距离,只不过,这里需要更新两个cover, 我理解的贪心的思想是,如果cur_cover,到不了终点,我们才需要跳下一步,动态更新两个cover

思路如图所示:
从图中可以看出来,就是移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。

这里还是有个特殊情况需要考虑,当移动下标达到了当前覆盖的最远距离下标时

如果当前覆盖最远距离下标不是是集合终点,步数就加一,还需要继续走。
如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了。
代码随想录算法训练营第31、32天(贪心篇) | 376.摆动序列 53.最大子数组和 122.买卖股票的最佳时机II 55.跳跃游戏 45.跳跃游戏II 1005.K次取反后最大化的数组和_第4张图片
我们在遍历数组的时候,需要动态更新next_cover, (第一次的cur_cover是0)。如果下标此时已经到了cur_cover,如果此时没有走到尽头,我们就需要再跳了。ans += 1, 同时更新cur_cover;如果此时走到尽头了,跳出循环

由于我们是先跳,就是先计算ans+1, 因此我们在,已经到了cur_cover,并且如果此时没有走到尽头,我们就需要再跳,这个步骤中。在ans += 1, 同时更新cur_cover之后,我们需要判断next_cur能不能达到终点,如果能我们也跳出循环。通俗理解其实我们此时已经跳了,这个nextcover就是我们这一跳能到的最远距离。

贪心写法1

class Solution:
    def jump(self, nums: List[int]) -> int:
        cur_cover = 0
        next_cover = 0
        ans = 0
        for i in range(len(nums)):
            next_cover = max(next_cover, i+nums[i])
            if i == cur_cover:
                if cur_cover != len(nums)-1:
                    cur_cover = next_cover
                    ans += 1
                    if next_cover >= len(nums)-1: break
                else: break
        return ans

针对于方法一的特殊情况,可以统一处理,即:移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不考虑是不是终点的情况

想要达到这样的效果,只要让移动下标,最大只能移动到nums.size - 2的地方就可以了。

因为当移动下标指向nums.size - 2时:

如果移动下标等于当前覆盖最大距离下标, 需要再走一步(即ans++),因为最后一步一定是可以到的终点。(题目假设总是可以到达数组的最后一个位置),如图:
代码随想录算法训练营第31、32天(贪心篇) | 376.摆动序列 53.最大子数组和 122.买卖股票的最佳时机II 55.跳跃游戏 45.跳跃游戏II 1005.K次取反后最大化的数组和_第5张图片
代码随想录算法训练营第31、32天(贪心篇) | 376.摆动序列 53.最大子数组和 122.买卖股票的最佳时机II 55.跳跃游戏 45.跳跃游戏II 1005.K次取反后最大化的数组和_第6张图片
总结这种思路,还是先跳,i到cur_cover就跳,计数+1;i最后就遍历到 len(nums)-2, 如果cur_cover到终点了,i就不会到最后一次cur_cover,计数就不会+1

贪心写法2

def jump(self, nums: List[int]) -> int:
        cur_cover = 0
        next_cover = 0
        ans = 0
        for i in range(len(nums)-1):
            next_cover = max(next_cover, i+nums[i])
            if i == cur_cover:
                cur_cover = next_cover
                ans += 1
       return ans

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

题目链接
这个题相比其它题来说算是简单的,首先是思路比较容易想,但是一些具体实现的细节对我来说还是没整好

1.思路:这个题的思路应该是什么呢?我们把负数里绝对值大的,给调成正的,调一次k -= 1;如果k正好把负数全都调成正的或者k不够,就这样不用继续处理;如果负数全调成正数之后,k还剩余怎么样?那么应该看k剩余的是奇数还是偶数,偶数的话对同一个数调正一次调负一次等于白调;奇数的话我们就让绝对值最小的正数调成负数就行了。

2.根据这个思路,我们的实现的时候有技巧的。首先我们要明白,这道题返回的是一个sum,不用模拟,直接在nums上改就行了。
首先我们按照绝对值,将nums从大到小排序一下;
我们正常从左到右遍历,如果k>0 遇到负数我们调成正数,同时k – ;
如果 k还有剩余 看k是奇数还是偶数,再调;

贪心解法

class Solution:
    def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
        nums.sort(key = abs, reverse=True)
        for i in range(len(nums)):
            if k>0 and nums[i] < 0:
                nums[i] = -nums[i]
                k -= 1
        if k % 2 == 1:
            nums[-1] = -nums[-1]
        return sum(nums)

你可能感兴趣的:(代码随想录算法训练营打卡,算法,游戏,leetcode)