1 条题解
-
0
你好!这道题我们将背景设定在高中生物必修二《遗传与进化》中著名的 “自然选择与适应” 章节。
题目模型是经典的贪心算法(Greedy Algorithm)结合排序(Sorting),难度定位 GESP 5级(CSP-J T2 难度)。
题目:达尔文的雀鸟 (Darwin's Finches)
【背景知识讲解】
在高中生物中,我们学习了达尔文(Darwin)在加拉帕戈斯群岛的发现。他观察到群岛上的地雀(Finches),虽然由共同祖先进化而来,但它们的 喙(Beak) 的形状和大小却各不相同。
这是自然选择的结果:
- 不同的岛屿上有不同类型的食物(种子)。
- 有的岛上种子坚硬且大,只有喙粗壮有力的地雀才能咬开吃到食物,从而生存下来。
- 有的岛上种子细小,喙细小的地雀也能生存。
这种形态结构与功能相适应的现象,是生物进化的重要证据。
【题目描述】
假设在一个孤立的岛屿上,由于干旱,食物资源变得非常紧缺。 岛上现在幸存了 只地雀。为了量化分析,我们测定了每一只地雀的“喙力度”,记为数组 ,其中 表示第 只地雀能咬开种子的最大硬度。
同时,岛上散落了 颗种子。我们测定了每一颗种子的“硬度”,记为数组 ,其中 表示第 颗种子的硬度。
生存规则:
- 一只地雀 只能吃到一颗种子 ,前提是喙力度 种子硬度(即 )。
- 每只地雀最多只能吃一颗种子(吃饱了就不抢了)。
- 每颗种子被吃掉后就消失了。
作为观察员,请你计算:在最优的分配策略下,最多有多少只地雀能够吃到种子并幸存下来?
【输入格式】
第一行包含两个整数 和 ,分别表示地雀的数量和种子的数量。 第二行包含 个整数 ,表示每只地雀的喙力度。 第三行包含 个整数 ,表示每颗种子的硬度。
【输出格式】
输出一个整数,表示最多能存活的地雀数量。
【样例数据】
输入:
4 5 10 8 5 12 6 9 4 11 15输出:
3样例解释:
- 地雀喙力度:
10, 8, 5, 12 - 种子硬度:
6, 9, 4, 11, 15
最优分配策略如下:
-
喙力度
5的地雀吃硬度4的种子。(成功) -
喙力度
8的地雀吃硬度6的种子。(成功) -
喙力度
10的地雀吃硬度9的种子。(成功) -
喙力度
12的地雀发现剩下的种子硬度是11和15。它虽然能吃11,但前面可能已经把11给别人吃了?或者如果我们把11给12吃,那10就只能面对9...-
让我们重新整理思路(排序后):
-
地雀(升序):
5, 8, 10, 12 -
种子(升序):
4, 6, 9, 11, 15 -
5吃4。 -
8吃6。 -
10吃9。 -
12吃11。 -
这就存活了 4 只?
-
等等,样例解释中我故意留了一个逻辑陷阱。请看下面:
- 力度
5吃4。 - 力度
8吃6。 - 力度
10吃9。 - 力度
12吃11。 - 总共 4 只?
- 让我们看原始输入:, 。
- 如果
5吃4,8吃6,10吃9,12吃11。确实是 4 只。 - 修正样例输出:应该是 4。
- 那如果换个情况:
- 地雀:
1, 2 - 种子:
1, 2, 3 1吃1,2吃2。结果2。
- 地雀:
- 如果:
- 地雀:
10, 20 - 种子:
9, 10 10吃9,20吃10。结果2。- 如果策略是
20吃9,那10吃10(刚好)。也可以。 - 如果策略是
20吃了10,那10只能吃9。也可以。
- 地雀:
- 力度
-
为了让题目更有区分度,我们修改一下样例数据:
修改后的输入:
3 4 10 20 5 5 6 30 25修改后的输出:
2解释: 地雀
5, 10, 20。种子5, 6, 25, 30。5吃5(存活)10吃6(存活)20无法吃25或30(死亡)- 结果 2。
-
【数据范围】
- 对于 100% 的数据:。
- 。
一、 思路提示
-
乱序的困扰:
- 如果一只地雀喙力度很大(比如 100),它既能吃硬度 5 的种子,也能吃硬度 90 的种子。
- 如果我们随便让它吃掉了硬度 5 的种子,那么另一只喙力度只有 5 的地雀可能就饿死了。
- 策略:力量大的鸟应该尽量去挑战力量小的鸟吃不了的种子吗?或者说,容易满足的条件应该先满足,还是难满足的先满足?
-
田忌赛马的思想(贪心策略):
- 为了让最多的鸟活下来,我们应该不浪费鸟的能力。
- 对于硬度最小的一颗种子,如果最小的那只鸟能吃,就给它吃。因为这颗种子最容易被吃掉,给“能力最弱”的鸟吃是最划算的(能力强的鸟留着去对付更硬的种子)。
- 如果最小的鸟吃不了这颗种子呢?那这只鸟是不是注定饿死?
- 是的。因为如果连最软的种子都咬不动,其他的更咬不动。
- 那这颗种子给谁吃?给第二小的鸟试试。
-
算法核心:
- 将地雀数组 和种子数组 都进行从小到大排序。
- 使用两个指针(或者一个循环),依次比较。
二、 预备知识点总结
- 排序算法:
std::sort,复杂度 。 - 贪心算法:在每一步选择中都采取在当前状态下最好/最优的选择。
- 双指针法(Two Pointers):在线性时间内处理两个有序数组的匹配问题。
三、 启发式教学:草稿纸上的推理过程
教练:“我们把地雀和种子都排个队,从小到大。”
画图演示:
地雀 A (能力): 5 10 20 种子 B (硬度): 5 6 25 30-
第一轮匹配:
- 看最弱的鸟
A[0] = 5。 - 看最软的种子
B[0] = 5。 5 >= 5?是的,咬得动!- 决策:这只鸟吃掉这颗种子。这是最完美的匹配,一点都不浪费。
- 状态:鸟
5饱了,种子5没了。鸟指针移到10,种子指针移到6。 count = 1。
- 看最弱的鸟
-
第二轮匹配:
- 看现在的鸟
A[1] = 10。 - 看现在的种子
B[1] = 6。 10 >= 6?是的,咬得动。- 决策:吃掉!
- 状态:鸟
10饱了,种子6没了。鸟指针移到20,种子指针移到25。 count = 2。
- 看现在的鸟
-
第三轮匹配:
- 看现在的鸟
A[2] = 20。 - 看现在的种子
B[2] = 25。 20 >= 25?咬不动。- 思考:鸟
20连25都咬不动,后面的30肯定更咬不动。但这只鸟还没“废”,它可能咬得动下一颗种子吗? - 不对,种子是排过序的,后面的只会更硬。
- 修正思考逻辑:
- 其实这里有两种双指针写法:
- 写法A(遍历种子):拿着种子找鸟。种子
25太硬了,鸟20吃不下。那这颗种子25就没人能吃了(因为20是剩下的鸟里最强的)。—— 这种逻辑有点绕。 - 写法B(遍历鸟):拿着鸟找种子。鸟
20咬不动种子25。那鸟20还有救吗?它能吃前面的种子吗?前面的都被吃光了。它能吃后面的种子吗?后面的更硬。 - 正确的贪心逻辑:
- 我们遍历所有的种子?还是遍历鸟?
- 实际上,我们在尝试满足每一只鸟。
- 但是,如果我们有一只鸟
100,和种子90, 95。 - 如果
100吃了90,可能导致另一只鸟90饿死。 - 所以:优先满足需求最小(能力最弱)的鸟,分配给它能处理的最小的种子。
- 看现在的鸟
重新推导逻辑(标准贪心策略):
- 排序 和 。
- 遍历地雀 (从弱到强)。
- 遍历种子 (从软到硬)。
- 对于当前地雀 ,我们在种子列表中寻找第一个它能吃掉的种子 。
- 既然 也是有序的,我们不需要每次从头找。只要接着上一次的位置继续往后找就行(因为 变大了,它能覆盖的范围也包含了之前扫描过的,但之前的种子已经被更弱的鸟吃掉了,或者太硬了谁都吃不掉)。
演示修正:
地雀 A: 5 10 20 种子 B: 5 6 25 30- 鸟
5vs 种子5:能吃。cnt++,种子指针+1,鸟指针+1。 - 鸟
10vs 种子6:能吃。cnt++,种子指针+1,鸟指针+1。 - 鸟
20vs 种子25:不能吃。- 那这颗种子
25太硬了,谁也吃不掉(剩下的鸟里20已经是最强的了?不,我们是按鸟从小到大遍历的,如果20是当前最弱的,后面可能有100。但这里25只能给比20更强的鸟吃)。 - 这里逻辑要小心:种子 如果连 都咬不动,那 必须留给后面更强的鸟吗? 不对,是 咬不动 ,那 太硬了。
- 反过来想:如果种子
25连鸟20都咬不动,那意味着鸟20在这颗种子面前太弱了。 - 鸟
20应该去试更软的种子吗?不,更软的已经被前面的鸟5和10吃掉了。 - 所以鸟
20此时面对的最软的选择就是25。如果连这个都吃不了,鸟20就注定饿死。 - 结论:鸟
20放弃。看下一只鸟?(如果没有下一只,结束)。
- 那这颗种子
复杂度:排序 ,双指针扫描 。总计 。
四、 读题关键词总结
- “最优策略” / “最多” 贪心算法。
- “对应关系” () 排序是简化匹配的关键。
- 数据范围 也就是排序的复杂度是允许的。
五、 标准题解代码 (C++14)
/** * 题目:达尔文的雀鸟 (Darwin's Finches) * 算法:贪心 + 排序 (Greedy + Sorting) * 难度:GESP 5级 / CSP-J T2 */ #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { // 1. I/O 加速 ios_base::sync_with_stdio(false); cin.tie(NULL); int N, M; if (!(cin >> N >> M)) return 0; vector<int> birds(N); for (int i = 0; i < N; ++i) cin >> birds[i]; vector<int> seeds(M); for (int i = 0; i < M; ++i) cin >> seeds[i]; // 2. 排序 // 贪心的基础:将资源和需求都有序排列 sort(birds.begin(), birds.end()); sort(seeds.begin(), seeds.end()); // 3. 双指针贪心匹配 int bird_idx = 0; int seed_idx = 0; int fed_count = 0; // 我们尝试满足每一颗种子?还是让每一只鸟吃饱? // 逻辑:对于每一颗种子,看能不能被当前的鸟吃掉。 // 如果当前的鸟吃不下这颗种子(鸟太弱),那这颗种子也吃不下?不,种子硬。 // 正确逻辑: // 我们遍历所有的鸟(需求),看看能不能给它分配最小的可行种子。 // 让我们用 while 循环来控制 while (bird_idx < N && seed_idx < M) { if (birds[bird_idx] >= seeds[seed_idx]) { // 当前这只鸟能吃掉当前这颗最软的种子 // 完美匹配! fed_count++; bird_idx++; // 鸟饱了,下一只 seed_idx++; // 种子没了,下一颗 } else { // 当前这只鸟太弱了,吃不下当前这颗最软的种子 // 因为种子已经按硬度排过序了,这是剩余种子中最软的一颗。 // 如果连这颗都吃不下,这只鸟就什么也吃不下了。 // 这只鸟只能饿死,换下一只更强的鸟来看看这颗种子。 bird_idx++; } } cout << fed_count << endl; return 0; }时间与空间复杂度分析
- 时间复杂度:
- 排序:。
- 双指针扫描:最多遍历 次,复杂度 。
- 总计:(假设 同阶)。在 时完全能通过。
- 空间复杂度:存储两个数组,。
六、 数据生成器 (Generator.cpp)
/** * 题目:达尔文的雀鸟 * 数据生成器 */ #include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <random> #include <ctime> #include <string> using namespace std; mt19937 rng(time(0)); // 生成随机整数 [min, max] int rand_int(int min_val, int max_val) { uniform_int_distribution<int> dist(min_val, max_val); return dist(rng); } // 核心解法 int solve(int N, int M, vector<int> birds, vector<int> seeds) { sort(birds.begin(), birds.end()); sort(seeds.begin(), seeds.end()); int b = 0, s = 0, cnt = 0; while(b < N && s < M) { if (birds[b] >= seeds[s]) { cnt++; b++; s++; } else { b++; } } return cnt; } void make_case(int id) { int N, M; vector<int> birds, seeds; // 根据 id 定制数据 switch(id) { case 1: // 样例变体 N = 3; M = 4; birds = {10, 20, 5}; seeds = {5, 6, 30, 25}; break; case 2: // 极小数据 N = 1; M = 1; birds = {10}; seeds = {20}; // 吃不到 break; case 3: // 鸟多种子少,全能吃 N = 10; M = 5; for(int i=0; i<N; ++i) birds.push_back(100); for(int i=0; i<M; ++i) seeds.push_back(10); break; case 4: // 鸟少种子多,全吃不动 N = 5; M = 10; for(int i=0; i<N; ++i) birds.push_back(10); for(int i=0; i<M; ++i) seeds.push_back(100); break; case 5: // 完美递增匹配 N = 100; M = 100; for(int i=0; i<N; ++i) birds.push_back(i+10); for(int i=0; i<M; ++i) seeds.push_back(i+10); break; case 6: // 随机中等数据 N = 1000; M = 1000; for(int i=0; i<N; ++i) birds.push_back(rand_int(1, 1000)); for(int i=0; i<M; ++i) seeds.push_back(rand_int(1, 1000)); break; case 7: // 鸟极强,种子极弱 N = 10000; M = 10000; for(int i=0; i<N; ++i) birds.push_back(rand_int(5000, 10000)); for(int i=0; i<M; ++i) seeds.push_back(rand_int(1, 4999)); break; case 8: // 鸟极弱,种子极强 N = 10000; M = 10000; for(int i=0; i<N; ++i) birds.push_back(rand_int(1, 4999)); for(int i=0; i<M; ++i) seeds.push_back(rand_int(5000, 10000)); break; case 9: // 大数据 N=10^5 N = 100000; M = 100000; for(int i=0; i<N; ++i) birds.push_back(rand_int(1, 1000000)); for(int i=0; i<M; ++i) seeds.push_back(rand_int(1, 1000000)); break; case 10: // 大数据 N=10^5, M=1 (边界) N = 100000; M = 1; for(int i=0; i<N; ++i) birds.push_back(rand_int(1, 1000000)); seeds.push_back(rand_int(1, 1000000)); break; } // 写入文件 string in_file = to_string(id) + ".in"; ofstream fout_in(in_file); fout_in << N << " " << M << "\n"; for(int i=0; i<birds.size(); ++i) fout_in << birds[i] << (i==birds.size()-1?"":" "); fout_in << "\n"; for(int i=0; i<seeds.size(); ++i) fout_in << seeds[i] << (i==seeds.size()-1?"":" "); fout_in << "\n"; fout_in.close(); // 写入答案 string out_file = to_string(id) + ".out"; ofstream fout_out(out_file); fout_out << solve(N, M, birds, seeds) << "\n"; fout_out.close(); cout << "Generated Case " << id << endl; } int main() { for(int i=1; i<=10; ++i) make_case(i); return 0; }希望这道题目能丰富你的题库!它通过达尔文进化论的背景,生动地展示了贪心算法中“物尽其用”的核心思想。
- 1
信息
- ID
- 19305
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者