代码随想录训练营 Day28打卡 贪心算法 part02
一、 力扣122. 买卖股票的最佳时机 II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 :
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7 。
假如第 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])。如图:
从图中可以发现,其实我们需要收集每天的正利润就可以,收集正利润的区间,就是股票买卖的区间,而我们只需要关注最终利润,不需要记录区间。
代码实现
该贪心算法的核心思想是,只要今天的价格高于昨天,就在这两天之间完成一次买卖,从而获得利润。
累加所有正利润的交易,即可得到最大总利润。
这种方法有效地利用了每一个可以获利的机会,而不考虑具体买卖的时机,只要能获利就加上。
时间复杂度为O(n),其中n是价格数组的长度,空间复杂度为O(1)。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
result = 0 # 初始化总利润为0
for i in range(1, len(prices)):
# 计算今天与昨天的价格差,如果是正数,说明可以获得利润,否则不计入利润
result += max(prices[i] - prices[i - 1], 0)
return result # 返回累计的总利润
二、 力扣55. 跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
示例 :
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。
这个范围内,别管是怎么跳的,反正一定可以跳过来。
那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
如果 cover 大于等于了终点下标,直接 return true 就可以了。
代码实现
实现思路:
该算法通过维护一个变量cover来记录当前能够覆盖的最远范围。
初始时cover为0,即只能站在起点。
通过遍历数组,逐步更新cover,每次更新为当前位置i加上能够跳跃的最大步数nums[i]。
如果在某一步的更新中,cover已经大于或等于数组的末尾位置,则返回True。
如果在遍历过程中,cover无法继续向前推进,说明无法到达终点,返回False。
class Solution:
def canJump(self, nums: List[int]) -> bool:
cover = 0 # 初始化覆盖范围,最远可以到达的位置
if len(nums) == 1:
return True # 如果数组长度为1,直接返回True,因为起点就是终点
i = 0 # 初始化下标
# 使用while循环遍历数组,确保下标i在当前覆盖范围内
while i <= cover:
cover = max(i + nums[i], cover) # 更新当前覆盖范围,取最大可达范围
if cover >= len(nums) - 1:
return True # 如果覆盖范围已经可以到达或超过终点,返回True
i += 1 # 移动下标到下一个位置
return False # 如果遍历结束后无法到达终点,返回False
三、 力扣45. 跳跃游戏 II
给定一个长度为 n 的 0 索引 整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
示例 :
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
本题要计算最少步数,那么就要想清楚什么时候步数才一定要加一呢?
真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!
这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。
版本一 贪心法(一)
实现思路:
该算法使用贪心策略,每次选择能够跳到的最远位置。
维护两个变量cur_distance和next_distance,分别表示当前跳跃能够到达的最远位置和下一次跳跃能够达到的最远位置。
每当遍历到当前的最远位置时,更新cur_distance为next_distance并增加跳跃次数。
如果在某一步的跳跃中,next_distance已经可以到达或超过终点,则结束跳跃并返回跳跃次数。
class Solution:
def jump(self, nums):
if len(nums) == 1:
return 0 # 如果数组长度为1,不需要跳跃,返回0
cur_distance = 0 # 当前可以覆盖的最远距离下标
ans = 0 # 记录跳跃的次数
next_distance = 0 # 下一步可以覆盖的最远距离下标
for i in range(len(nums)):
next_distance = max(nums[i] + i, next_distance) # 更新下一步可以覆盖的最远距离
if i == cur_distance: # 当遍历到当前最远可达位置时
ans += 1 # 需要进行一次跳跃
cur_distance = next_distance # 更新当前最远可达位置
if next_distance >= len(nums) - 1: # 如果下一步的覆盖范围已经达到或超过终点
break # 跳出循环,不再需要进一步跳跃
return ans # 返回跳跃的次数
版本二 贪心法(二)
实现思路:
这个版本与第一个版本类似,但在遍历数组时,只遍历到len(nums) - 1,因为最后一步跳跃到达终点后不再需要进一步跳跃。
通过每次更新next_distance来确定当前的最优跳跃目标,并在达到cur_distance时更新跳跃次数。
最终返回的ans就是完成从起点到终点所需的最小跳跃次数。
class Solution:
def jump(self, nums):
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次取反后最大化的数组和
给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
示例:
输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。
贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
局部最优可以推出全局最优。
那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。
代码实现
class Solution:
def largestSumAfterKNegations(self, A: List[int], K: int) -> int:
# 第一步:按照绝对值降序排序数组A
# 这样可以优先处理绝对值大的负数,保证每次取反操作能使得数组的和最大化
A.sort(key=lambda x: abs(x), reverse=True)
# 第二步:执行K次取反操作
for i in range(len(A)):
# 如果当前元素为负数,并且还有剩余的K次取反操作
if A[i] < 0 and K > 0:
A[i] *= -1 # 将负数取反,使其变为正数
K -= 1 # 每次取反后,减少一次K的次数
# 第三步:如果K还有剩余次数
# 由于排序后的最后一个元素绝对值最小,如果K是奇数,还需要再取反一次
if K % 2 == 1:
A[-1] *= -1 # 将绝对值最小的元素取反
# 第四步:计算数组A的元素和
result = sum(A)
return result # 返回最终的和