贪心算法(2~3节课)教学逐字稿
适合阶段:CSP-J / 信息学奥赛入门
学生基础:
- 已学习循环
- 数组
- 排序
- 前缀和
- 差分
- 二分
课程目标:
- 理解什么是贪心
- 学会分析“当前最优”
- 知道什么时候可以贪心
- 掌握经典贪心模型
第一节课:认识贪心算法
一、课程开场(5分钟)
同学们,我们前面学过很多算法。
比如:
- 前缀和
- 差分
- 二分查找
这些算法都有一个特点:
它们都有固定套路。
但是今天开始,我们要学习一种:
更像“脑筋急转弯”的算法思想。
它叫:
贪心算法。
什么叫贪心?
大家先别害怕。
其实你们生活里每天都在“贪心”。
比如:
妈妈给你10块钱买零食。你会怎么选?
很多同学都会:
- 先买自己最喜欢的
- 先拿大的
- 先拿最赚的
这就是:
每一步都想当前最优,这就是贪心。
所以:
贪心算法 = 每一步都选择当前看起来最优的方案。
同学们记一句话:
“只看现在怎么最赚。”
但是问题来了:
当前最优,最后真的会全局最优吗?
这就是今天要研究的问题。
二、第一题:排队接水(40分钟)
有n个人接水。每个人接水时间不同,只有一个水龙头。
问:怎么排队,才能让总等待时间最短?
例如:
3个人:
A:3分钟
B:1分钟
C:2分钟
问怎么排?先让学生猜。
大部分学生会说:
1 2 3
然后开始模拟。
三、带学生手算
如果顺序:
3 1 2
等待时间:
第1个人:0
第2个人:3
第3个人:4
总等待:
0+3+4=7
如果顺序:
1 2 3
等待时间:
第1个人:0
第2个人:1
第3个人:3
总等待:
0+1+3=4
明显更优。
四、引导学生发现规律
提问:
为什么小的放前面更好?
因为:
前面的人会影响后面所有人。
如果大的放前面:
后面所有人都要等很久。
如果小的放前面:
影响更小。
所以:
接水时间短的人应该排前面。
得到贪心策略:
从小到大排序。
五、讲“贪心”的核心
这里一定要停下来讲:
我们为什么敢这样做?
因为:
我们认为当前最优,会导致最终最优。
也就是:
局部最优 → 全局最优
这就是贪心。
六、简单反证法(小学生版)
假设:前面站了一个很慢的人。后面站了一个很快的人。
比如:
5 和 1
现在顺序:
5 1
等待:0+5=5
交换:
1 5
等待:0+1=1
明显更优。
所以:大的在前面不划算。
因此:小的应该放前面。
这就是一种简单的“交换思想”。
七、代码讲解
核心步骤:
- 输入
- 排序
- 计算等待时间
重点讲:
为什么是前缀累加。
板书:
第i个人等待时间 = 前面所有人的时间和
#include<bits/stdc++.h>
using namespace std;
const int maxn=10001;
struct stu{
int id;
int time;
};
stu a[maxn];
bool cmp(stu x, stu y){
if(x.time!=y.time){
return x.id<y.id;
}
else return x.time<y.time;
}
int main(){
int n; scanf("%d",&n);
for(int i=1;i<=n;i++) {
scanf("%d",&a[i].time);
a[i].id = i;
}
double sum=0;
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++){
printf("%d ",a[i].id);
sum+=(n-i)*a[i].time;
}
printf("\n%.2lf",sum/n);
return 0;
}
八、课堂总结
今天记住:
贪心算法:每一步都选择当前最优。
本题贪心策略:小的先排。
因为:前面的人会影响后面所有人。
第二节课:排序贪心与双指针
一、复习(5分钟)
上节课我们学习了:
贪心 = 每一步都选择当前最优。
并且学会了:
排队接水的核心思想:小的放前面。
因为:前面的人会影响后面所有人。
今天继续学习第二种贪心。
二、纪念品分组(35分钟)
题目
有n个纪念品。每个纪念品有重量。一辆车最多装两件。
并且: 两件重量和不能超过w。
问:最少需要多少辆车?
三、先让学生暴力思考
先问学生:“如果是你,你会怎么分?”
很多学生会说:
- 随便找两个
- 一个个试
- 暴力枚举
这时开始引导:
谁最难安排?
学生会发现:
最重的人最难安排。
因为:
他最容易超重。
于是开始引导:我们先考虑最重的人。
四、核心分析(重点讲)
假设:最重的是10。限制是15。
现在:如果10连最轻的5都带不了。
那么:他一定只能自己坐一辆车。
因为:
别人只会更重。
这是本题最关键的贪心思想。
五、继续引导
如果:
最重的人能和最轻的人一起。
那应该怎么办?
当然:
直接一起。
因为:
最重的人已经被解决掉了。
同时还顺便解决了一个最轻的人。
这是最赚的。
六、得到贪心策略
排序后:
最轻在左边。
最重在右边。
于是:
双指针。
左指针:i
右指针:j
如果:
a[i]+a[j]<=w
说明能一起。
那么:i++ j--
否则: 最重的人单独一组。
只需要:j--
七、带学生模拟
例如:
2 3 4 5 9
限制:10
现在:2 和 9 超了。
所以:9 单独一组。
然后:2 和 5 可以一起。
然后:3 和 4 可以一起。
最后答案:3组。
八、核心代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,w;
cin>>n>>w;
int a[n+1];
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+n+1);
int i=1,j=n;
int ans=0;
while(i<=j){
if(a[i]+a[j]<=w){
i++;
j--;
}
else{
j--;
}
ans++;
}
cout<<ans;
}
九、第一节课总结
今天我们学会了:排序后贪心。
以及:双指针。
重点记住:“最难处理的人优先考虑。”
第三节课:动态贪心
一、最大整数(35分钟)
题目
给你一些数字。重新排列顺序。让拼接后的整数最大。
例如:
9 34 5
可以拼成:
9345
二、先让学生猜
问: 是不是从大到小排序就行?
很多学生会说:是。
然后举反例: 3 和 34
如果按数字大小:34 在前 3在后。
得到:343
但:334 更小吗?
开始比较:
343
334
发现:
343更大。
所以:
34 应该在前。
三、真正核心(重点)
到底谁放前面?
不是比较数字大小。
而是:比较拼接后谁更大。
即:a+b 和 b+a
例如:
9 和 34
934
349
934更大。
所以:
9 在前。
四、讲排序比较器
以前排序:
默认从小到大。
今天第一次接触:
自定义排序规则。
这在信息学奥赛里非常重要。
五、核心代码
#include<bits/stdc++.h>
using namespace std;
bool cmp(string a,string b){
return a+b>b+a;
}
int main(){
int n;
cin>>n;
string a[n+1];
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++){
cout<<a[i];
}
}
六、课堂强调
本题最重要的不是代码。
而是:贪心标准可以自己定义。
七、合并果子(40分钟)
题目
每次可以合并两堆果子。
代价:消耗两堆重量之和的体力。
问:最小总代价。
八、让学生先乱试
例如:1 2 9
问:先合并谁?
很多学生会:随便合。
这时开始对比。
九、带学生计算
方案1:
1+2=3 代价:3
现在:3 9
继续:3+9=12 代价:12
总代价:3+12=15
方案2:
2+9=11 代价:11
现在:1 11
继续:1+11=12 代价:12
总代价:11+12=23
明显方案2更差。
十、引导发现规律
为什么?
因为:合并后的数字还会继续参与计算。
如果大的数字太早出现。
后面会被重复计算很多次。
代价会越来越大。
所以:每次先合并最小的两个。
十一、关键问题
那是不是排序一次就行?
不是。
因为:每次合并后。会产生新数字。
所以:每一步都要重新找最小。
十二、引出优先队列
于是:我们需要一种东西:
能快速找到最小值。
这就是: 优先队列。
十三、核心代码
#include<bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>,greater<int>> q;
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
int x;
cin>>x;
q.push(x);
}
int ans=0;
while(q.size()>1){
int a=q.top();
q.pop();
int b=q.top();
q.pop();
int s=a+b;
ans+=s;
q.push(s);
}
cout<<ans;
}
十四、课堂总结
今天学会了:动态贪心。
也就是:每一步都重新选择当前最优。
金银岛(提升题)
一、题目
船容量有限。有很多金银珠宝。
每个宝物:
- 有重量
- 有价值
问:
怎么拿价值最大?
二、学生容易犯的错误
很多学生会:直接拿价值最大的。
例如:
价值100,重量99
价值90,重量10
其实:
第二个明显更赚。
三、核心思想
所以:不能只看价值。
而是:单位价值。
即:价值 ÷ 重量
谁更赚。谁优先。
四、课堂强调
这是贪心里非常重要的一点:
贪心标准不是固定的。
有时:
- 小的优先
- 大的优先
- 最早结束优先
- 性价比最高优先
五、核心代码
#include<bits/stdc++.h>
using namespace std;
struct node{
double w,v;
};
bool cmp(node a,node b){
return a.v/a.w>b.v/b.w;
}
int main(){
int n,t;
cin>>n>>t;
node a[n+1];
for(int i=1;i<=n;i++){
cin>>a[i].w>>a[i].v;
}
sort(a+1,a+n+1,cmp);
double ans=0;
for(int i=1;i<=n;i++){
if(t>=a[i].w){
t-=a[i].w;
ans+=a[i].v;
}
else{
ans+=a[i].v/a[i].w*t;
break;
}
}
printf("%.2lf",ans);
}
教学注意事项(老师一定要看)
一、不要把贪心讲成“套路”
很多学生学完贪心后:
- 看见排序就想贪
- 看见最大就想贪
- 看见最小就想贪
但:真正重要的是:
“为什么这样贪是对的?”
所以:每道题一定都要反复强调:
当前最优为什么能推出最终最优。
例如:
排队接水:前面的人会影响后面所有人。
合并果子:合并后的数字还会重复参与计算。
金银岛:真正重要的是单位价值。
二、每道题一定要设计“错误思路”
不要直接公布正确答案。
而是:先让学生乱猜。
然后:让错误思路自己暴露问题。
例如:纪念品分组:
故意问:“先考虑最轻的人行不行?”
然后学生会发现:真正难处理的是最重的人。
合并果子:
故意让学生:
- 大的先合
- 随便合
然后比较总代价。
学生会瞬间理解。
三、不要太早讲严格证明
小学生阶段不要一开始就:
- 严格反证法
- 数学证明
- 长篇推导
学生容易断线。
更适合:
交换思想。
例如:5 在 1 前面。
交换后更优。
于是:小的应该在前。
这种方式孩子最容易理解。
四、一定让学生“猜规律”
贪心最怕:老师直接公布答案。
正确流程:
- 学生乱猜
- 学生犯错
- 老师引导
- 学生自己发现规律
这样:
“学生会感觉:规律是我发现的。”
记忆会非常深。
五、排序是贪心的灵魂
一定反复强调:
排序是在帮我们制造规律。
很多贪心,本质其实都是:
排序后更容易做决策。
例如:
- 排队接水:排序后:小的都在前面。
- 纪念品分组:排序后:最轻最重容易同时考虑。
- 最大整数:排序后:比较规则统一。
- 金银岛:排序后:性价比从高到低。
六、合并果子一定慢讲
这是整套里最难的一题。
因为:
第一次出现:
动态贪心。
学生会疑惑:
- 为什么不是排序一次?
- 为什么每次重新选?
- 为什么小的先合?
所以一定要:多模拟。
甚至:
可以让学生上台演。
效果会特别好。
七、不要急着刷题
这一阶段:
重点不是:做了多少题。
而是:学生会不会主动找“当前最优”。
如果学生开始会问:
“老师,这题是不是应该先处理最难的?”
说明已经开始理解贪心了。
八、多用生活例子
- 排队接水:“一个打饭特别慢的人站第一。”
- 纪念品分组:“最胖的人最难安排座位。”
- 金银岛:“去自助餐优先拿贵的肉。”
这些生活化比算法术语更容易记。
九、老师上课时的节奏建议
推荐课堂流程:
- 题目
- 让学生猜
- 故意出现错误思路
- 举反例
- 找规律
- 得到贪心策略
- 模拟过程
- 写代码
- 总结“为什么能贪”
这是最适合小学生的信息学课堂节奏。
十、老师一定反复强调的话
- “前面的选择会影响后面。”
- “当前最优,不一定真的最优,所以要验证。”
- “贪心不是乱贪,而是有规律地贪。”
- “排序很多时候是在帮贪心。”
- “局部最优,才能推出全局最优。”
全部课程最终总结
贪心算法:
每一步都选择当前最优。
常见套路:
- 排序
- 找规律
- 制定贪心标准
- 证明为什么正确
经典模型:
- 排队接水
- 纪念品分组
- 最大整数
- 合并果子
- 金银岛
贪心算法:每一步都选择当前最优。
常见套路:
- 排序
- 找规律
- 制定贪心标准
- 证明为什么正确
经典模型:
- 排队接水
- 纪念品分组
- 最大整数
- 合并果子
- 金银岛
适合反复强调的话
- “前面的选择会影响后面。”
- “当前最优,不一定真的最优,所以要证明。”
- “贪心不是乱贪,而是有规律地贪。”
- “排序很多时候是在帮贪心。”
- “局部最优,才能推出全局最优。”