算法训练营 Day27 - 贪心算法part01

理论基础

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

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. 1 -> 17 :上升(cur > 0),之前默认 pre=0是一个拐点(起点)result 变成 2。记录 pre 为正。

  2. 17 -> 5 :下降(cur < 0),之前是正。是拐点(山峰)result 变成 3。记录 pre 为负。

  3. 5 -> 10 :上升(cur > 0),之前是负。是拐点(山谷)result 变成 4。记录 pre 为正。

  4. 10 -> 13 :上升。之前也是正。不是拐点,只是半山腰,跳过。

  5. 13 -> 15 :上升。之前也是正。不是拐点,跳过。

  6. 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),不要拖累下一个数。"

因为任何数加上一个负数,都会变小。所以只要当前的"累积和"变成了负数,它对未来就没有任何贡献了,直接丢弃,从现在重新开始算。

那些 updown 到底在算什么?为什么要看坡度?

记忆故事:【炒股只做波段】

把你给的数组想象成股票的K线图

  • [1, 17, 5, 10, 13, 15, 10]

  • 我们要找最长的摆动序列,其实就是问你:这只股票一共发生了几次趋势逆转?

逻辑推演:

  1. 1 -> 17(涨了) :好,现在是上涨趋势。中间如果变成 1 -> 5 -> 17 这种微小波动不管,反正只要最后比前面高就行。这时候我们处于山峰寻找中。

  2. 17 -> 5(跌了) :趋势变了!从涨变跌,这说明刚才的 17 是个山顶(拐点)。记录下来,现在开始找山谷。

  3. 5 -> 10(涨了) :趋势又变了!说明刚才的 5 是个谷底(拐点)。记录下来。

  4. 10 -> 13 -> 15(一直涨) :这叫单边行情 。你在 10 买入,在 15 卖出是一样的,中间的 13 是废话,不算摆动。直到它下次跌了,才算一个新的摆动。

核心口诀:

"只抓拐点,过滤单边。" 只有当坡度反向(从上变下,或从下变上)时,长度才 +1。单调递增或递减的一段路,只算作一步。

不要背代码,要背物理模型

  1. 最大子序和 = 带负债就扔(负数对于以此开头的子序列是累赘)。

  2. 摆动序列 = 数山峰山谷(只在趋势改变时计数)。

  3. 分发饼干 = 田忌赛马/以大吃大(最大的饼干给最贪心的孩子,或者最小的饼干给最小胃口的孩子,不要浪费)。

相关推荐
码农三叔1 小时前
(11-4-02)完整人形机器人的设计与实现案例:机器人跳跃
人工智能·算法·机器人·人机交互·人形机器人
xiaoye-duck2 小时前
深入解析 STL 优先级队列:从原理到实战
c++·算法·stl
蜕变的小白2 小时前
数据结构:排序算法与哈希表
数据结构·算法·哈希算法
_OP_CHEN2 小时前
【算法基础篇】(六十一)SG 函数通关指南:博弈论通用解法,从原理到实战秒杀各类 ICG 游戏
算法·蓝桥杯·c/c++·博弈论·acm/icpc·sg函数·有向图游戏
We་ct2 小时前
LeetCode 2. 两数相加:链表经典应用题详解
前端·算法·leetcode·链表·typescript
If using 10 days2 小时前
multiprocessing:创建并管理多个进程
python·算法
wu_asia2 小时前
每日一练壹
算法
程序员酥皮蛋2 小时前
hot 100 第二十二题 22.相交链表
数据结构·算法·leetcode·链表
一只小小的芙厨2 小时前
寒假集训·子集枚举2
c++·笔记·算法·动态规划