理论基础
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
455.分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
- 输入: g = [1,2,3], s = [1,1]
- 输出: 1 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以你应该输出 1。
示例 2:
- 输入: g = [1,2], s = [1,2,3]
- 输出: 2
- 解释:你有两个孩子和三块小饼干,2 个孩子的胃口值分别是 1,2。你拥有的饼干数量和尺寸都足以让所有孩子满足。所以你应该输出 2.
提示:
- 1 <= g.length <= 3 * 10^4
- 0 <= s.length <= 3 * 10^4
- 1 <= g[i], s[j] <= 2^31 - 1
python
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort() # 先排序孩子胃口
s.sort() # 先排序饼干大小
child_i = 0 # 孩子的指针
cookie_j = 0 # 饼干的指针
# 只有当 既有孩子 又有饼干 时才继续
while child_i < len(g) and cookie_j < len(s):
# 如果当前饼干能满足当前孩子
if s[cookie_j] >= g[child_i]:
child_i += 1 # 孩子满足了,看下一个孩子
# 无论是否满足,饼干都要往后移
# (如果满足了,这块饼干被吃了,要看下一块)
# (如果不满足,这块太小了,要看下一块更大的)
cookie_j += 1
return child_i # 满足的孩子数量就是 child_i 的下标
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
- 输入: [1,7,4,9,2,5]
- 输出: 6
- 解释: 整个序列均为摆动序列。
示例 2:
- 输入: [1,17,5,10,13,15,10,5,16,8]
- 输出: 7
- 解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。
示例 3:
- 输入: [1,2,3,4,5,6,7,8,9]
- 输出: 2
贪心算法 - "数峰值"
其实这道题最直观的解法就是:数山峰和山谷。
想象把数组画成折线图,我们只需要统计拐点的数量。
-
如果是单调递增(1, 2, 3, 4),中间的数都没用,只要两头。
-
一旦趋势发生改变(从升变降,或从降变升),序列长度就 +1。
python
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if len(nums) < 2:
return len(nums)
# cur_diff: 当前坡度 (现在的数 - 前一个数)
# pre_diff: 前一个坡度
pre_diff = 0
result = 1 # 默认序列最右边的一个数算作一个长度,所以从1开始
for i in range(len(nums) - 1):
# 计算当前一对数的坡度
cur_diff = nums[i+1] - nums[i]
# 核心判断:出现"拐点"
# 情况1:之前是平的或下的(pre<=0),现在往上了(cur>0) -> 形成山谷
# 情况2:之前是平的或上的(pre>=0),现在往下了(cur<0) -> 形成山峰
if (cur_diff > 0 and pre_diff <= 0) or (cur_diff < 0 and pre_diff >= 0):
result += 1
pre_diff = cur_diff # 更新前一个坡度,准备找下一个拐点
return result
假设输入是 [1, 17, 5, 10, 13, 15, 10]:
-
1 -> 17 :上升(
cur > 0),之前默认pre=0。是一个拐点(起点) 。result变成 2。记录pre为正。 -
17 -> 5 :下降(
cur < 0),之前是正。是拐点(山峰) 。result变成 3。记录pre为负。 -
5 -> 10 :上升(
cur > 0),之前是负。是拐点(山谷) 。result变成 4。记录pre为正。 -
10 -> 13 :上升。之前也是正。不是拐点,只是半山腰,跳过。
-
13 -> 15 :上升。之前也是正。不是拐点,跳过。
-
15 -> 10 :下降。之前是正。是拐点(山峰) 。
result变成 5。
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
- 输入: [-2,1,-3,4,-1,2,1,-5,4]
- 输出: 6
- 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
res = nums[0]
total = 0
for n in nums:
if total < 0:
total = 0
total += n
res = max(res,total)
return res
为什么要 if total < 0: total = 0?为什么一旦当前和小于 0 就要扔掉?
记忆故事:【赌徒的口袋】
想象你在赌场走一条路,路上全是筹码。
-
有些是正数(捡到钱)。
-
有些是负数(遇到抢劫,要倒扣钱)。
你的任务是:找一段路,让你手里的钱最多。
-
逻辑推演:
-
你捡到了一个
-2(当前 total = -2)。 -
这时候前面有个
4等着你。 -
如果你带着之前的
-2去捡这个4,你手里的总数是2。 -
如果你扔掉 之前的
-2,空手去捡这个4,你手里的总数是4。 -
显然,之前的
-2是个累赘!
-
核心口诀:
"只要之前的收益是负债(Total < 0),就立刻破产重开(Total = 0),不要拖累下一个数。"
因为任何数加上一个负数,都会变小。所以只要当前的"累积和"变成了负数,它对未来就没有任何贡献了,直接丢弃,从现在重新开始算。
那些 up 和 down 到底在算什么?为什么要看坡度?
记忆故事:【炒股只做波段】
把你给的数组想象成股票的K线图。
-
[1, 17, 5, 10, 13, 15, 10] -
我们要找最长的摆动序列,其实就是问你:这只股票一共发生了几次趋势逆转?
逻辑推演:
-
1 -> 17(涨了) :好,现在是上涨趋势。中间如果变成
1 -> 5 -> 17这种微小波动不管,反正只要最后比前面高就行。这时候我们处于山峰寻找中。 -
17 -> 5(跌了) :趋势变了!从涨变跌,这说明刚才的
17是个山顶(拐点)。记录下来,现在开始找山谷。 -
5 -> 10(涨了) :趋势又变了!说明刚才的
5是个谷底(拐点)。记录下来。 -
10 -> 13 -> 15(一直涨) :这叫单边行情 。你在
10买入,在15卖出是一样的,中间的13是废话,不算摆动。直到它下次跌了,才算一个新的摆动。
核心口诀:
"只抓拐点,过滤单边。" 只有当坡度反向(从上变下,或从下变上)时,长度才 +1。单调递增或递减的一段路,只算作一步。
不要背代码,要背物理模型:
-
最大子序和 = 带负债就扔(负数对于以此开头的子序列是累赘)。
-
摆动序列 = 数山峰山谷(只在趋势改变时计数)。
-
分发饼干 = 田忌赛马/以大吃大(最大的饼干给最贪心的孩子,或者最小的饼干给最小胃口的孩子,不要浪费)。