L2-4.贪心算法(CSP初级算法)

39 阅读12分钟

贪心算法(2~3节课)教学逐字稿

适合阶段:CSP-J / 信息学奥赛入门

学生基础:

  • 已学习循环
  • 数组
  • 排序
  • 前缀和
  • 差分
  • 二分

课程目标:

  • 理解什么是贪心
  • 学会分析“当前最优”
  • 知道什么时候可以贪心
  • 掌握经典贪心模型

第一节课:认识贪心算法


一、课程开场(5分钟)

同学们,我们前面学过很多算法。

比如:

  • 前缀和
  • 差分
  • 二分查找

这些算法都有一个特点:

它们都有固定套路。

但是今天开始,我们要学习一种:

更像“脑筋急转弯”的算法思想。

它叫:

贪心算法。

什么叫贪心?

大家先别害怕。

其实你们生活里每天都在“贪心”。

比如:

妈妈给你10块钱买零食。你会怎么选?

很多同学都会:

  • 先买自己最喜欢的
  • 先拿大的
  • 先拿最赚的

这就是:

每一步都想当前最优,这就是贪心。

所以:

贪心算法 = 每一步都选择当前看起来最优的方案。

同学们记一句话:

“只看现在怎么最赚。”

但是问题来了:

当前最优,最后真的会全局最优吗?

这就是今天要研究的问题。


二、第一题:排队接水(40分钟)


有n个人接水。每个人接水时间不同,只有一个水龙头。
问:怎么排队,才能让总等待时间最短?

image.png

例如:

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

明显更优。

所以:大的在前面不划算。

因此:小的应该放前面。

这就是一种简单的“交换思想”。

七、代码讲解


核心步骤:

  1. 输入
  2. 排序
  3. 计算等待时间

重点讲:

为什么是前缀累加。

板书:

第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。
问:最少需要多少辆车?

image.png

image.png

三、先让学生暴力思考


先问学生:“如果是你,你会怎么分?”
很多学生会说:
  • 随便找两个
  • 一个个试
  • 暴力枚举

这时开始引导:

谁最难安排?

学生会发现:

最重的人最难安排。

因为:

他最容易超重。

于是开始引导:我们先考虑最重的人。

四、核心分析(重点讲)


假设:最重的是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 前面。

交换后更优。

于是:小的应该在前。

这种方式孩子最容易理解。

四、一定让学生“猜规律”

贪心最怕:老师直接公布答案。

正确流程:

  1. 学生乱猜
  2. 学生犯错
  3. 老师引导
  4. 学生自己发现规律

这样:

“学生会感觉:规律是我发现的。”

记忆会非常深。


五、排序是贪心的灵魂

一定反复强调:

排序是在帮我们制造规律。

很多贪心,本质其实都是:

排序后更容易做决策。

例如:

  1. 排队接水:排序后:小的都在前面。
  2. 纪念品分组:排序后:最轻最重容易同时考虑。
  3. 最大整数:排序后:比较规则统一。
  4. 金银岛:排序后:性价比从高到低。

六、合并果子一定慢讲

这是整套里最难的一题。

因为:

第一次出现:

动态贪心。

学生会疑惑:

  • 为什么不是排序一次?
  • 为什么每次重新选?
  • 为什么小的先合?

所以一定要:多模拟。

甚至:

可以让学生上台演。

效果会特别好。


七、不要急着刷题


这一阶段:

重点不是:做了多少题。
而是:学生会不会主动找“当前最优”。

如果学生开始会问:

“老师,这题是不是应该先处理最难的?”

说明已经开始理解贪心了。

八、多用生活例子

  • 排队接水:“一个打饭特别慢的人站第一。”
  • 纪念品分组:“最胖的人最难安排座位。”
  • 金银岛:“去自助餐优先拿贵的肉。”

这些生活化比算法术语更容易记。

九、老师上课时的节奏建议

推荐课堂流程:

  1. 题目
  2. 让学生猜
  3. 故意出现错误思路
  4. 举反例
  5. 找规律
  6. 得到贪心策略
  7. 模拟过程
  8. 写代码
  9. 总结“为什么能贪”

这是最适合小学生的信息学课堂节奏。


十、老师一定反复强调的话


  1. “前面的选择会影响后面。”
  2. “当前最优,不一定真的最优,所以要验证。”
  3. “贪心不是乱贪,而是有规律地贪。”
  4. “排序很多时候是在帮贪心。”
  5. “局部最优,才能推出全局最优。”

全部课程最终总结


贪心算法:

每一步都选择当前最优。

常见套路:

  1. 排序
  2. 找规律
  3. 制定贪心标准
  4. 证明为什么正确

经典模型:

  • 排队接水
  • 纪念品分组
  • 最大整数
  • 合并果子
  • 金银岛

贪心算法:每一步都选择当前最优。

常见套路:

  1. 排序
  2. 找规律
  3. 制定贪心标准
  4. 证明为什么正确

经典模型:

  • 排队接水
  • 纪念品分组
  • 最大整数
  • 合并果子
  • 金银岛

适合反复强调的话

  1. “前面的选择会影响后面。”
  2. “当前最优,不一定真的最优,所以要证明。”
  3. “贪心不是乱贪,而是有规律地贪。”
  4. “排序很多时候是在帮贪心。”
  5. “局部最优,才能推出全局最优。”