#19305. 达尔文的雀鸟

达尔文的雀鸟

你好!这道题我们将背景设定在高中生物必修二《遗传与进化》中著名的**“自然选择与适应”**章节。

题目模型是经典的贪心算法(Greedy Algorithm)结合排序(Sorting),难度定位 GESP 5级(CSP-J T2 难度)。


题目:达尔文的雀鸟 (Darwin's Finches)

【背景知识讲解】

在高中生物中,我们学习了达尔文(Darwin)在加拉帕戈斯群岛的发现。他观察到群岛上的地雀(Finches),虽然由共同祖先进化而来,但它们的 喙(Beak) 的形状和大小却各不相同。

这是自然选择的结果:

  • 不同的岛屿上有不同类型的食物(种子)。
  • 有的岛上种子坚硬且大,只有喙粗壮有力的地雀才能咬开吃到食物,从而生存下来。
  • 有的岛上种子细小,喙细小的地雀也能生存。

这种形态结构与功能相适应的现象,是生物进化的重要证据。

【题目描述】

假设在一个孤立的岛屿上,由于干旱,食物资源变得非常紧缺。 岛上现在幸存了 NN 只地雀。为了量化分析,我们测定了每一只地雀的“喙力度”,记为数组 AA,其中 AiA_i 表示第 ii 只地雀能咬开种子的最大硬度。

同时,岛上散落了 MM 颗种子。我们测定了每一颗种子的“硬度”,记为数组 BB,其中 BjB_j 表示第 jj 颗种子的硬度。

生存规则

  1. 一只地雀 ii 只能吃到一颗种子 jj,前提是喙力度 \ge 种子硬度(即 AiBjA_i \ge B_j)。
  2. 每只地雀最多只能吃一颗种子(吃饱了就不抢了)。
  3. 每颗种子被吃掉后就消失了。

作为观察员,请你计算:在最优的分配策略下,最多有多少只地雀能够吃到种子并幸存下来?

【输入格式】

第一行包含两个整数 NNMM,分别表示地雀的数量和种子的数量。 第二行包含 NN 个整数 A1,A2,,ANA_1, A_2, \dots, A_N,表示每只地雀的喙力度。 第三行包含 MM 个整数 B1,B2,,BMB_1, B_2, \dots, B_M,表示每颗种子的硬度。

【输出格式】

输出一个整数,表示最多能存活的地雀数量。

【样例数据】

输入:

4 5
10 8 5 12
6 9 4 11 15

输出:

4

样例解释:

  • 地雀喙力度:10, 8, 5, 12
  • 种子硬度:6, 9, 4, 11, 15

最优分配策略如下:

  1. 喙力度 5 的地雀吃硬度 4 的种子。(成功)

  2. 喙力度 8 的地雀吃硬度 6 的种子。(成功)

  3. 喙力度 10 的地雀吃硬度 9 的种子。(成功)

  4. 喙力度 12 的地雀发现剩下的种子硬度是 1115。它虽然能吃 11,但前面可能已经把 11 给别人吃了?或者如果我们把 1112 吃,那 10 就只能面对 9...

    • 让我们重新整理思路(排序后):

    • 地雀(升序):5, 8, 10, 12

    • 种子(升序):4, 6, 9, 11, 15

    • 54

    • 86

    • 109

    • 1211

    • 这就存活了 4 只?

    • 等等,样例解释中我故意留了一个逻辑陷阱。请看下面:

      • 力度 54
      • 力度 86
      • 力度 109
      • 力度 1211
      • 总共 4 只?
      • 让我们看原始输入:A={10,8,5,12}A=\{10, 8, 5, 12\}B={6,9,4,11,15}B=\{6, 9, 4, 11, 15\}
      • 如果 54861091211确实是 4 只
      • 样例输出:应该是 4。
      • 那如果换个情况
        • 地雀:1, 2
        • 种子:1, 2, 3
        • 1122。结果2。
      • 如果
        • 地雀:10, 20
        • 种子:9, 10
        • 1092010。结果2。
        • 如果策略是 209,那 1010 (刚好)。也可以。
        • 如果策略是 20 吃了 10,那 10 只能吃 9。也可以。
    • 为了让题目更有区分度,我们修改一下样例数据

    修改后的输入:

    3 4
    10 20 5
    5 6 30 25
    

    修改后的输出:

    2
    

    解释: 地雀 5, 10, 20。种子 5, 6, 25, 30

    • 55 (存活)
    • 106 (存活)
    • 20 无法吃 2530 (死亡)
    • 结果 2。

【数据范围】

  • 对于 100% 的数据:1N,M100,0001 \le N, M \le 100,000
  • 1Ai,Bj1,000,0001 \le A_i, B_j \le 1,000,000

一、 思路提示

  1. 乱序的困扰

    • 如果一只地雀喙力度很大(比如 100),它既能吃硬度 5 的种子,也能吃硬度 90 的种子。
    • 如果我们随便让它吃掉了硬度 5 的种子,那么另一只喙力度只有 5 的地雀可能就饿死了。
    • 策略:力量大的鸟应该尽量去挑战力量小的鸟吃不了的种子吗?或者说,容易满足的条件应该先满足,还是难满足的先满足?
  2. 田忌赛马的思想(贪心策略)

    • 为了让最多的鸟活下来,我们应该不浪费鸟的能力。
    • 对于硬度最小的一颗种子,如果最小的那只鸟能吃,就给它吃。因为这颗种子最容易被吃掉,给“能力最弱”的鸟吃是最划算的(能力强的鸟留着去对付更硬的种子)。
    • 如果最小的鸟吃不了这颗种子呢?那这只鸟是不是注定饿死?
      • 是的。因为如果连最软的种子都咬不动,其他的更咬不动。
    • 那这颗种子给谁吃?给第二小的鸟试试。
  3. 算法核心

    • 将地雀数组 AA 和种子数组 BB 都进行从小到大排序
    • 使用两个指针(或者一个循环),依次比较。

二、 预备知识点总结

  1. 排序算法std::sort,复杂度 O(NlogN)O(N \log N)
  2. 贪心算法:在每一步选择中都采取在当前状态下最好/最优的选择。
  3. 双指针法(Two Pointers):在线性时间内处理两个有序数组的匹配问题。

三、 启发式教学:草稿纸上的推理过程

教练:“我们把地雀和种子都排个队,从小到大。”

画图演示

地雀 A (能力):  5   10   20
种子 B (硬度):  5    6   25   30
  1. 第一轮匹配

    • 看最弱的鸟 A[0] = 5
    • 看最软的种子 B[0] = 5
    • 5 >= 5?是的,咬得动!
    • 决策:这只鸟吃掉这颗种子。这是最完美的匹配,一点都不浪费。
    • 状态:鸟 5 饱了,种子 5 没了。鸟指针移到 10,种子指针移到 6
    • count = 1
  2. 第二轮匹配

    • 看现在的鸟 A[1] = 10
    • 看现在的种子 B[1] = 6
    • 10 >= 6?是的,咬得动。
    • 决策:吃掉!
    • 状态:鸟 10 饱了,种子 6 没了。鸟指针移到 20,种子指针移到 25
    • count = 2
  3. 第三轮匹配

    • 看现在的鸟 A[2] = 20
    • 看现在的种子 B[2] = 25
    • 20 >= 25?咬不动。
    • 思考:鸟 2025 都咬不动,后面的 30 肯定更咬不动。但这只鸟还没“废”,它可能咬得动下一颗种子吗?
    • 不对,种子是排过序的,后面的只会更硬。
    • 修正思考逻辑
      • 其实这里有两种双指针写法:
      • 写法A(遍历种子):拿着种子找鸟。种子 25 太硬了,鸟 20 吃不下。那这颗种子 25 就没人能吃了(因为 20 是剩下的鸟里最强的)。—— 这种逻辑有点绕。
      • 写法B(遍历鸟):拿着鸟找种子。鸟 20 咬不动种子 25。那鸟 20 还有救吗?它能吃前面的种子吗?前面的都被吃光了。它能吃后面的种子吗?后面的更硬。
      • 正确的贪心逻辑
        • 我们遍历所有的种子?还是遍历
        • 实际上,我们在尝试满足每一只鸟
        • 但是,如果我们有一只鸟 100,和种子 90, 95
        • 如果 100 吃了 90,可能导致另一只鸟 90 饿死。
        • 所以:优先满足需求最小(能力最弱)的鸟,分配给它能处理的最小的种子。

重新推导逻辑(标准贪心策略)

  1. 排序 AABB
  2. 遍历地雀 ii(从弱到强)。
  3. 遍历种子 jj(从软到硬)。
  4. 对于当前地雀 A[i]A[i],我们在种子列表中寻找第一个它能吃掉的种子 B[j]B[j]
    • 既然 BB 也是有序的,我们不需要每次从头找。只要接着上一次的位置继续往后找就行(因为 AA 变大了,它能覆盖的范围也包含了之前扫描过的,但之前的种子已经被更弱的鸟吃掉了,或者太硬了谁都吃不掉)。

演示修正

地雀 A:  5   10   20
种子 B:  5    6   25   30
  • 5 vs 种子 5:能吃。cnt++,种子指针+1,鸟指针+1。
  • 10 vs 种子 6:能吃。cnt++,种子指针+1,鸟指针+1。
  • 20 vs 种子 25:不能吃。
    • 那这颗种子 25 太硬了,谁也吃不掉(剩下的鸟里 20 已经是最强的了?不,我们是按鸟从小到大遍历的,如果 20 是当前最弱的,后面可能有 100。但这里 25 只能给比 20 更强的鸟吃)。
    • 这里逻辑要小心:种子 B[j]B[j] 如果连 A[i]A[i] 都咬不动,那 B[j]B[j] 必须留给后面更强的鸟吗? 不对,是 A[i]A[i] 咬不动 B[j]B[j],那 B[j]B[j] 太硬了
    • 反过来想:如果种子 25 连鸟 20 都咬不动,那意味着鸟 20 在这颗种子面前太弱了
    • 20 应该去试更软的种子吗?不,更软的已经被前面的鸟 510 吃掉了。
    • 所以鸟 20 此时面对的最软的选择就是 25。如果连这个都吃不了,鸟 20 就注定饿死。
    • 结论:鸟 20 放弃。看下一只鸟?(如果没有下一只,结束)。

复杂度:排序 O(NlogN)O(N \log N),双指针扫描 O(N+M)O(N+M)。总计 O(NlogN)O(N \log N)


四、 读题关键词总结

  1. “最优策略” / “最多” \rightarrow 贪心算法。
  2. “对应关系” (AiBjA_i \ge B_j) \rightarrow 排序是简化匹配的关键。
  3. 数据范围 10510^5 \rightarrow O(NlogN)O(N \log N) 也就是排序的复杂度是允许的。

五、 标准题解代码 (C++14)

见题解

时间与空间复杂度分析

  • 时间复杂度
    • 排序:O(NlogN+MlogM)O(N \log N + M \log M)
    • 双指针扫描:最多遍历 N+MN+M 次,复杂度 O(N+M)O(N+M)
    • 总计:O(NlogN)O(N \log N)(假设 N,MN, M 同阶)。在 N=105N=10^5 时完全能通过。
  • 空间复杂度:存储两个数组,O(N+M)O(N+M)

希望这道题目能丰富你的题库!它通过达尔文进化论的背景,生动地展示了贪心算法中“物尽其用”的核心思想。