目录
455. 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i
,都有一个胃口值 g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j
,都有一个尺寸 s[j]
。如果 s[j] >= g[i]
,我们可以将这个饼干 j
分配给孩子 i
,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。
⚠️ 倒序遍历的 range 为
(len(g) - 1, -1, -1) # 正序 (len(g))
⚠️ 遍历胃口值,不可以遍历饼干:饼干的索引是在满足条件下再移动的,遍历的胃口值是固定移动的
⚠️ 需要判断 j >= 0(当饼干遍历结束 即 j < 0 时,遍历g直至结束, 不再判断 if / cnt 不再增加)。否则g = [1,2,3], s = [] 时报错。单纯的设定 j > 0 在s只有一个元素时报错
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort()
s.sort()
#饼干索引
j, cnt = len(s)-1, 0
#遍历胃口值,不可以遍历饼干:饼干的索引是在满足条件下再移动的,遍历的胃口值是固定移动的
for i in range(len(g)-1, -1, -1): #左闭右开的,所以-1,倒序遍历,所以-1
if j>=0 and g[i]<=s[j]: #先执行i_s >= 0,防止异常
j -= 1
cnt += 1
return cnt
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
-
例如,
[1, 7, 4, 9, 2, 5]
是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3)
是正负交替出现的。 - 相反,
[1, 4, 7, 2, 5]
和[1, 7, 4, 5, 5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums
,返回 nums
中作为 摆动序列 的 最长子序列的长度 。
算法思想:
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
判断条件:prediff(num[i]-num[i-1]); curdiff(num[i+1]-num[i])。要求prediff>=0 and curdiff<0 或prediff<=0 and curdiff>0
- 上下有平坡(删左边):prediff = 0 and curdiff<0/prediff = 0 and curdiff>0
- 首尾元素:以上判断需要元素数量>=3,在2个的情况下不成立,但在2个元素不相同的情况下也计为2个摆动 加虚拟头节点:prediff=0-->有一个摆动 默认数组最右有个摆动:答案初始为1-->有一个摆动。
- 单调坡度有平坡 只需要在 这个坡度 摆动变化的时候,更新prediff就行,这样prediff在 单调区间有平坡的时候 就不会发生变化,造成我们的误判:将prediff = curdiff的更新写在if语句里
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
prediff, curdiff = 0, 0
ans = 1
if len(nums)<2: ans =len(nums)
#边界处理
for i in range(0,len(nums)-1):
#最后一个节点已经算是一个摆动,计算到倒数第二个就可以
curdiff = nums[i+1] - nums[i]
if (prediff>=0 and curdiff <0) or (prediff<=0 and curdiff>0):
ans+=1
prediff = curdiff#
return ans
53. 最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
暴力
class Solution:
def maxSubArray(self, nums):
result = float('-inf') # 初始化结果为负无穷大
count = 0
for i in range(len(nums)): # 设置起始位置
count = 0
for j in range(i, len(nums)): # 从起始位置i开始遍历寻找最大值
count += nums[j]
result = max(count, result) # 更新最大值
return result
贪心
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
全局最优:选取最大“连续和”
⚠️ 两个变量
- cnt做加法记录子数组和,当为负时置0
- ans和cnt比较后,记录最大值,防止加一个数后反而变小
两个变量初始值:cnt = 0,做加法;cnt = -inf 初始化为无穷小,防止数组元素都为负
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
ans = -inf
cnt = 0
for i in range(len(nums)):
cnt += nums[i]
if cnt>ans:
ans = cnt
if cnt < 0:
cnt = 0
#若当前连续和小于0,及时抛弃 防止拖累下面的和
return ans
121. 买卖股票的最佳时机
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
取最左最小值,取最右最大值,那么得到的差值就是最大利润。
一直往后遍历,而minprice停在左边的最小值上,price在取到右边最大值时,得最大利润。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
minprice = inf
maxprofit = 0
for price in prices:
minprice = min(minprice, price) #取左边最小值
maxprofit = max(maxprofit, price-minprice) #取最大区间利润
return maxprofit
122. 买卖股票的最佳时机 II(可以买卖多次)
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
局部最优:收集每天的为正的利润,全局最优:求得最大利润。
假如第 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:
ans = 0
for i in range(1,len(prices)):
# up = prices[i]-prices[i-1]
#if up > 0:
# ans+=up
ans += max(prices[i]-prices[i-1],0)
return ans
55. 跳跃游戏(是否能到)
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
贪心算法
局部最优解:每次取最大跳跃步数(取最大覆盖范围)
整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
当前位置元素如果是 3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢?
跳几步无所谓,关键在于可跳的覆盖范围
i 每次移动只能在 cover 的范围内移动
![]()
class Solution:
def canJump(self, nums: List[int]) -> bool:
n = len(nums)
can_reach = 0
if len(nums) == 1:
return True
for i in range(n):
if i <= can_reach:
# i 只能 <= 当前可到达的范围
#如果当前下标已经大于可到达的最远处,false(可以用while写,for不可以加条件)
can_reach = max(nums[i] + i, can_reach)
if can_reach >= n-1:
return True
return False
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 (最少几步)
给定一个长度为 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]
。
从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最小步数!
需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。
⚠️ 到达当前最远范围时,需要判断是否到终点了,决定是否步数加一
- 如果当前覆盖最远距离下标不是是集合终点,步数就加一,还需要继续走。
- 如果当前覆盖最远距离下标就是是集合终点,这一步刚好到,直接输出。
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums)==1:return 0
cur_cover = 0
nxt_cover = 0
cnt = 0
for i in range(len(nums)):
nxt_cover = max(nums[i]+i,nxt_cover) # 下一步可到的范围
if i == cur_cover: # 到达这一步最远范围
if i !=len(nums)-1: # 还没到终点时才会更新ans和可到范围
cnt += 1
cur_cover = nxt_cover
if nxt_cover>=len(nums)-1: return cnt
else: # 到终点直接输出步数
return cnt
1005. K 次取反后最大化的数组和
给你一个整数数组 nums
和一个整数 k
,按以下方法修改该数组:
- 选择某个下标
i
并将nums[i]
替换为-nums[i]
。
重复这个过程恰好 k
次。可以多次选择同一个下标 i
。
以这种方式修改数组后,返回数组 可能的最大和 。
局部最优:让绝对值大的负数变为正数,当前数值达到最大。
整体最优:整个数组和达到最大。
如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了)。
全局最优:整个 数组和 达到最大。
❌我本来的思路:排序,而不是按照绝对值大小降序排序
导致的问题:当负数都反转完毕,序列无序,无法找最小的正数继续反转,绝对值逆序排序能保证最后一个数永远是正数序列里的最小值。
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
nums.sort(key=abs, reverse=True) # 按绝对值倒序排列数组
"""
原理:先反转负数才能得最大值,且先反转负数中绝对值大的(即最小负数)
情况一:没反转完负数k就为0,直接求和
情况二:负数反转完毕,都为正数,k不为0:
k为偶数,无需再反转(可将一个数反转偶数次,结果不变),直接求和
k为基数,还需反转一次,此时都为正数,反转最小数,即nums[-1]
"""
for i in range(len(nums)):
if nums[i] < 0 and k > 0 : # 当元素为负且还需要反转时,反转并将k-1
nums[i] = -nums[i]
k-=1
if k % 2 == 1: # 对k模二有余数时,说明还需反转
nums[-1] = -nums[-1]
return sum(nums)
134. 加油站
在一条环路上有 n
个加油站,其中第 i
个加油站有汽油 gas[i]
升。
你有一辆油箱容量无限的的汽车,从第 i
个加油站开往第 i+1
个加油站需要消耗汽油 cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas
和 cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1
。如果存在解,则 保证 它是 唯一 的。
暴力法
i 遍历找到满足gas[i] > cost[i]的起点
rest 记录剩余油量
index 记录下一个加油站的索引,因为是环形,所以需要不能光是i+1,要做取余操作
while循环模拟从起点跑一圈的过程(环形适合用while)
当剩余油量大于0 且 还没跑完一圈(加油站的索引不等于起点)时,循环,继续向下一个加油站判断油量够不够。
超时
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
for i in range(len(cost)):
rest = gas[i] - cost[i] # 记录剩余油量
index = (i + 1) % len(cost) # 下一个加油站的索引
while rest > 0 and index != i: # 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了)
rest += gas[index] - cost[index] # 更新剩余油量
index = (index + 1) % len(cost) # 更新下一个加油站的索引
if rest >= 0 and index == i: # 如果以i为起点跑一圈,剩余油量>=0,并且回到起始位置
return i # 返回起始位置i
return -1 # 所有起始位置都无法环绕一圈,返回-1
贪心
总油量减去总消耗大于等于零则一定可以跑一圈,主要是求起点。
每个加油站的剩余量rest[i]为gas[i] - cost[i]。
i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
curSum = 0 # 当前累计的剩余油量
totalSum = 0 # 总剩余油量
start = 0 # 起始位置
for i in range(len(gas)):
curSum += gas[i] - cost[i]
totalSum += gas[i] - cost[i]
if curSum < 0: # 当前累计剩余油量curSum小于0
start = i + 1 # 起始位置更新为i+1
curSum = 0 # curSum重新从0开始累计
if totalSum < 0:
return -1 # 总剩余油量totalSum小于0,说明无法环绕一圈
return start
763. 划分字母区间
给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
- 统计每一个字符最后出现的位置
- 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
class Solution:
def partitionLabels(self, s: str) -> List[int]:
hash = [0]*26
for i in range(len(s)):
hash[ord(s[i])-ord('a')]= i
#记录每一个字母的最远位置,ord()返回字符对应的 Unicode 码点(整数值)
left ,right = 0, 0
ans = []
for i in range(len(s)):
right = max(right, hash[ord(s[i])-ord('a')])
#找右边界
if i == right:
#索引=右边界,遍历到右边界,找到当前段内最远的重复字母
ans.append(right-left+1)
left = i+1
#第二段左边界,从当前位置+1
return ans