贪心篇
1.贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
有没有啥套路呢?不好意思,贪心没套路,就刷题而言,如果感觉好像局部最优可以推出全局最优,然后想不到反例,那就试一试贪心吧!
2.贪心一般和dfs或者dp结合
题目链接
状态定义:dp[i][0] dp[i][1] 代表以i下标结尾的序列此时是波峰/波谷时,最长摆动序列的长度
状态转移: 初始化: 本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序 整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。 实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度) 这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点。 统计峰值的时候,数组最左面和最右面是最不好统计的。 我们可以一开始直接把res = 1,这个1就是最右端的峰值,图中的话就是最后一个 8,因为两端天然就可以加进序列 但是最左端的那个1我们怎么处理呢,我们可以用prec <= 0 / >=0, 因为一开始我们把prec = 0, 此时无论计算出第一对差值是什么 都会进入if 条件,把res += 1,这样其实就计算了最左端的元素波峰 然后我们只统计波峰,也就是两次差值异号的情况,并且只在异号的时候更新一下prec就好了,没必要每次更新 题目链接 这道题的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] 代码: 其实在上面的dp解法中我已经说到了贪心思想的应用的点了, 这道题的贪心写法是脱胎于两个指针遍历的暴力的写法的。我们在遍历的时候,拿一个cnt 去记录当前累加的和,拿res去记录最大的cnt。 那么怎么贪呢,如果此时cnt < 0我们就即时舍掉,下一个遍历位置开始我们从新开始,不让负数加在我们结果上 题目链接 理解利润拆分是关键点 如何分解呢? 假如第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])。 题目链接 其实跳几步无所谓,关键在于可跳的覆盖范围! 不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。 这个范围内,别管是怎么跳的,反正一定可以跳过来。 那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点! 所以要动态维护一个范围cover , 这个范围是 i + nums[i] (i <= cover) 每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。 贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。 局部最优推出全局最优,找不出反例,试试贪心! 如图: 而cover每次只取 max(该元素数值补充后的范围, cover本身范围)。 如果cover大于等于了终点下标,直接return true就可以了。 题目链接 然后45.跳跃游戏2,问的是跳跃的最小步数,还是需要动态更新最大跳跃距离,只不过,这里需要更新两个cover, 我理解的贪心的思想是,如果cur_cover,到不了终点,我们才需要跳下一步,动态更新两个cover 思路如图所示: 这里还是有个特殊情况需要考虑,当移动下标达到了当前覆盖的最远距离下标时 如果当前覆盖最远距离下标不是是集合终点,步数就加一,还需要继续走。 由于我们是先跳,就是先计算ans+1, 因此我们在,已经到了cur_cover,并且如果此时没有走到尽头,我们就需要再跳,这个步骤中。在ans += 1, 同时更新cur_cover之后,我们需要判断next_cur能不能达到终点,如果能我们也跳出循环。通俗理解其实我们此时已经跳了,这个nextcover就是我们这一跳能到的最远距离。 针对于方法一的特殊情况,可以统一处理,即:移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不考虑是不是终点的情况。 想要达到这样的效果,只要让移动下标,最大只能移动到nums.size - 2的地方就可以了。 因为当移动下标指向nums.size - 2时: 如果移动下标等于当前覆盖最大距离下标, 需要再走一步(即ans++),因为最后一步一定是可以到的终点。(题目假设总是可以到达数组的最后一个位置),如图: 题目链接 1.思路:这个题的思路应该是什么呢?我们把负数里绝对值大的,给调成正的,调一次k -= 1;如果k正好把负数全都调成正的或者k不够,就这样不用继续处理;如果负数全调成正数之后,k还剩余怎么样?那么应该看k剩余的是奇数还是偶数,偶数的话对同一个数调正一次调负一次等于白调;奇数的话我们就让绝对值最小的正数调成负数就行了。 2.根据这个思路,我们的实现的时候有技巧的。首先我们要明白,这道题返回的是一个sum,不用模拟,直接在nums上改就行了。
如果说波峰 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.贪心解法
如图所示
其实就是转化成求峰的个数
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。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解法
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
贪心解法
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
贪心解法
如果想到其实最终利润是可以分解的,那么本题就很容易了!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,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢?
i每次移动只能在cover的范围内移动,每移动一个元素,cover得到该元素数值(新的覆盖范围)的补充,让i继续移动下去。贪心解法
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.
从图中可以看出来,就是移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。
如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了。
我们在遍历数组的时候,需要动态更新next_cover, (第一次的cur_cover是0)。如果下标此时已经到了cur_cover,如果此时没有走到尽头,我们就需要再跳了。ans += 1, 同时更新cur_cover;如果此时走到尽头了,跳出循环贪心写法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
总结这种思路,还是先跳,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次取反后最大化的数组和
这个题相比其它题来说算是简单的,首先是思路比较容易想,但是一些具体实现的细节对我来说还是没整好
首先我们按照绝对值,将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)