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

理论基础

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

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

455.分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 gi,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 sj 。如果 sj >= gi,我们可以将这个饼干 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 <= gi, sj <= 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,51,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. 分发饼干 = 田忌赛马/以大吃大(最大的饼干给最贪心的孩子,或者最小的饼干给最小胃口的孩子,不要浪费)。

相关推荐
Forever Nore35 分钟前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考2 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX3 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面3 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号4 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽4 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI4 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者6 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者6 小时前
【征程6】校准量化中HistogramObserver解析
算法
OuO-26 小时前
笔试强训 Day 34:ISBN 号码、kotori 和迷宫、矩阵最长递增路径
java·算法·矩阵