内容为武汉大学国家网络安全学院2022级大一第三学期“996”实训课程中所做的笔记,仅供个人复习使用,如有侵权请联系本人,将与15个工作日内将博客设置为仅粉丝可见。
贪心算法回顾
我们先来看一个简单的例子:
你现在有面值为 20 元、10 元、5 元、1 元的纸币足够张,现在想用最少张数的纸币支付 58 元。
最差的方法是支付 58 张 1 元的纸币,但我相信你肯定不会这么做。
你会按下面的步骤想:
-
20 元纸币用 2 张,还需凑 18 元。
-
10 元纸币用 1 张,还需凑 8 元。
-
5 元纸币用 1 张,还需凑 3 元。
-
1 元纸币用 3 张,刚好凑出了 58 元,一共用了 7 张纸币。
即优先用大面值的纸币,能用多少就用多少,然后再一步步考虑小面值,这其实就是一种贪心的思想。像这种贪心是显而易见的,也不难证明其正确性,而有些贪心结论却隐藏的很深。
贪心选择性质
求解最优化问题的算法通常需要经过一系列的步骤,在每个步骤都面临多种选择。这类问题我们可以采用 贪心算法 来解决,我们在每一步都做出当前看起来最正确的决定,这些决定组合起来,就可以得到整个问题的最优解,即通过做出局部最优选择来构造全局最优解。
下面,我们通过几个例子,由浅入深地带领大家学习贪心算法。
采购奖品
马上又到了一年一度的新年联欢,小明作为班里的班长,负责组织策划新年联欢活动,他决定采购一些奖品奖励积极参与每个项目活动的同学。为了激励更多的人参与活动,需要采购的奖品数目越多越好。班费中可支出的钱数为 m 元,现给定商店中 n 种可作为奖品的物品的价格和库存数量,怎样才能购得最多的物品数?
输入格式
输入一共 n+1 行:
第一行包含两个正整数 m (1<m≤10000) 和 n (1≤n≤100),表示可支出的费用为 m 元和可供购买的物品有 n 种。
接下来的 n 行,每行包含两个数(用一个空格分隔),分别表示一种物品的单价 a_i 和库存数量 b_i。a_i 和 b_i 均不会超过 1000。
输出格式
一个整数,表示最多可以购买的物品数量。
样例输入
500 6
100 3
20 15
50 10
35 5
5 6
60 2
样例输出
25
样例解释
价格为 5 的可以买 6 个,价格为 20 的可以买 15 个,价格为 35 的可以买 4 个,总共买 25 个奖品。
解析
本题的贪心结论很简单:我们只需按单价从小到大排序,每次尽可能地买单价低的物品。因为你每购买一件物品,你都希望剩余的钱更多。
从这里可以看出,贪心就是人的一种本质思想,在现实中你也会选择优先买便宜的。部分参考代码如下:
struct Node {
int x, y; // x 表示价格,y 表示个数
} a[105];
bool cmp(struct Node p1, struct Node p2) {
return p1.x < p2.x;
} // 结构体排序函数
sort(a, a + n, cmp);
int ans = 0;
for (int i = 0; i < n; i++) {
int t = min(m / a[i].x, a[i].y); // 剩余的钱最多可以买多少个 a[i]
m -= a[i].x * t;
ans += t;
}
奥利凡德
哈利波特在与伏地魔的战斗中毁坏了自己的魔杖,于是他决定去奥利凡德的魔杖店买个新的。他在店里看到 n 个魔杖和 n 个盒子,每个魔杖的长度为 X_1,X_2,…X_n,每个盒子的长度为 Y_1,Y_2,…,Y_n。一个长度为 X 的魔杖能放进长度为 Y 的盒子里只有满足 X≤Y。
哈利想知道他能否把所有魔杖都放进盒子里,并且每个盒子只能放一根魔杖。请你帮他解决这个问题。
输入格式
第一行一个整数 n (1≤n≤100),表示魔杖的数量。
第二行 n 个整数 X_i (1≤X_i≤10^9) 表示每根魔杖的长度。
第三行 n 个整数 Y_i (1≤Y_i≤10^9) 表示每个盒子的长度。
输出格式
如果哈利能把所有魔杖放进盒子里,输出"DA"
,否则输出"NE"
。(克罗地亚语的“yes”和“no”)
样例输入
3
7 9 5
6 13 10
样例输出
DA
解析
可以这么想:
首先尝试把最长的魔杖的放进最长的盒子里,然后尝试把第二长的魔杖放进第二长的盒子里…………最后尝试把最短的魔杖放进最短的盒子里。
显然这样就一定是最优配对了,如果这种方法都无法实现,更别说其他的放法了。
参考代码:
sort(a, n, sizeof(a[0]), cmp_int); // 给魔杖排序
sort(b, n, sizeof(b[0]), cmp_int); // 给盒子排序
int ok = 1;
for (int i = 0; i < n; i++) {
if (a[i] > b[i]) {
ok = 0;
break;
}
}
if (ok) {
cout << "DA";
} else {
cout << "NE";
}
贪心算法证明
糖果
在一次班级活动上。同学们被奖励每人吃一些糖果。吃太多糖果对牙齿不好,为了不让同学们吃得太多,决定两人一组,使得吃得最多的那组吃得尽量少。
输入格式
第一行一个偶数 n (n≤10^4)。
第二行有 n 个正整数,为给定的一列数字,表示每个同学能吃多少糖果。(数字均小于 10^9)
输出格式
一个正整数,吃的最多的一组同学吃的个数的最小值。
样例输入
4
1 5 2 8
样例输出
9
解析
遇事不决先排序,我们首先会把所有整数从小到大排序,排序后的结果为 a_1,a_2,⋯,a_n。
当 n=2 时,只有一种分配方案,答案为 a_1+a_2。
当 n=4 时,有三种分配方案:
- a_1+a_2 和 a_3+a_4,显然后者更大。
- a_1+a_3 和 a_2+a_4,显然后者更大。
- a_1+a_4 和 a_2+a_3。
容易发现,前两种方案都比较浪费,可以通过适当的调整更优。而第三种方案,无论 a_1+a_4 还是 a_2+a_3 更大,都比前两种方案小。
那么就能想到贪心策略是每次用最小的和最大的来配对,即 (a_1,a_n),(a_2,a_(n−1)),⋯,(a_2n,a_(2n+1)),然后答案就为其中的最大值。
不相交的线段
在坐标轴上有 n 条线段,每条线段的左端点为 x_i,右端点为 y_i。现在你需要删去部分线段,使得剩下的线段除端点外无公共部分。请你计算最多能保留的线段数目。
输入格式
第一行一个整数 n (1≤n≤10^6),表示线段的条数。
接下来 n 行,每行两个整数 x_i,y_i (0≤x_i<y_i≤10^6)
输出格式
一个整数,表示最多能保留的线段数。
样例输入
3
0 2
2 4
1 3
样例输出
2
解析
首先我们会想到一种贪心策略:优先选择长度较短的区间。
容易举出反例:
如果刚开始选了绿色线段就没法选其他线段了,而更优的方法是选择红色线段。所以这种贪心策略是错误的。
我们又想到第二种贪心策略:把左端点从小到大排序,从左到右一条条覆盖,记录一下当前已经覆盖到的最右边位置,然后每次插入左端点大于该位置且左端点最小的线段。
依然能举出反例:
红色线段的左端点最小,但是实在太长了,选了它就没法选别的线段了,不如选两条绿色线段。所以这种贪心策略也是错误的。
那么我们就想到第三种贪心策略:把右端点从小到大排序,从左到右一条条覆盖,记录一下当前已经覆盖到的最右边位置,然后每次插入左端点大于该位置且右端点最小的线段。
这种贪心策略是正确的,因为我们的目的就是让所选的线段尽可能早的结束,这样能给予更多的空间来选择剩余的线段。
如上图所示,我们会选择三条绿色线段。
事实上,我们也可以按左端点排序,然后从右到左一条条覆盖,每次选择左端点最大的线段。
混合牛奶
- 时间限制:1000ms
- 内存限制:131072K
- 语言限制:C语言
由于乳制品产业利润很低,所以降低原材料(牛奶)价格就变得十分重要。帮助 Marry 乳业找到最优的牛奶采购方案。
Marry 乳业从一些奶农手中采购牛奶,并且每一位奶农为乳制品加工企业提供的价格是不同的。此外,就像每头奶牛每天只能挤出固定数量的奶,每位奶农每天能提供的牛奶数量是一定的。每天 Marry 乳业可以从奶农手中采购到小于或者等于奶农最大产量的整数数量的牛奶。
给出 Marry 乳业每天对牛奶的需求量,还有每位奶农提供的牛奶单价和产量。计算采购足够数量的牛奶所需的最小花费。
注:每天所有奶农的总产量大于 Marry 乳业的需求量。
输入格式
第一行共二个数值:N (0≤N≤2,000,000)是需要牛奶的总数;M (0≤M≤5,000)是提供牛奶的农民个数,两数之间以一个空格分隔。
接下来 M 行,每行二个整数:p_i 和 x_i,两数之间以一个空格分隔。
p_i (0≤p_i≤1,000) 是农民 i 的牛奶的单价。
x_i (0≤x_i≤2,000,000) 是农民 i 一天能卖给 Marry 的牛奶制造公司的牛奶数量。
输出格式
单独的一行包含单独的一个整数,表示 Marry 的牛奶制造公司拿到所需的牛奶所要的最小费用。
格式说明
输出时每行末尾的多余空格,不影响答案正确性
样例输入
100 5
5 20
9 40
3 10
8 80
6 30
样例输出
630
题解:按单价从小到大排序,贪心地取单价最小的供应商来买。
我的答案
#include <stdio.h>
#include <stdlib.h>
typedef struct Farmer {
int price;
int amount;
} Farmer;
int cmp(const void *a, const void *b) {
return ((Farmer *)a)->price - ((Farmer *)b)->price;
}
int main() {
int n, m;
scanf("%d %d", &n, &m);
Farmer farmers[m];
for (int i = 0; i < m; i++) {
scanf("%d %d", &farmers[i].price, &farmers[i].amount);
}
qsort(farmers, m, sizeof(Farmer), cmp);
int cost = 0;
for (int i = 0; i < m; i++) {
if (n > farmers[i].amount) {
cost += farmers[i].price * farmers[i].amount;
n -= farmers[i].amount;
} else {
cost += farmers[i].price * n;
break;
}
}
printf("%d\n", cost);
return 0;
}
蘑菇森林
- 时间限制:1000ms
- 内存限制:131072K
- 语言限制:C语言
蒜头君来到蘑菇森林,这里有 n 只僵尸蘑菇,每只僵尸蘑菇的闪避值为 x_i,血量为 y_i。只有蒜头君的命中值大于等于怪物的闪避值,才能对怪物造成伤害。蒜头君一共有 m 点能量值,他每次攻击会消耗一点能量,然后造成一点伤害(单体攻击,某个怪物血量减少 1)。
现在已知蒜头君的基础命中值为 h,身上装备增加的命中值为 b。现在蒜头君他想知道一共能杀死多少个僵尸蘑菇。
输入格式
第一行四个整数 n,m,h,b,分别表示僵尸蘑菇的数量,能量值,基础命中值,装备的命中值加成,相邻两数之间以一个空格分隔。
接下来 n 行,每行两个整数 x_i,y_i,表示每个僵尸蘑菇的闪避值和血量,两数之间以一个空格分隔。
输出格式
一个整数,表示能杀死的僵尸蘑菇数量。
数据范围
1≤n≤5000,1≤m≤1000,1≤h,b≤200,1≤x_i≤300,1≤y_i≤50。
格式说明
输出时每行末尾的多余空格,不影响答案正确性
样例输入
5 10 50 50
120 1
110 2
100 4
80 7
90 6
样例输出
2
题解:把闪避值小于等于 h+b 的怪物血量放进数组里排序,然后贪心的从小到大取。
我的答案
#include <stdio.h>
#include <stdlib.h>
typedef struct Zombie {
int dodge;
int health;
} Zombie;
int cmp(const void *a, const void *b) {
return ((Zombie *)a)->health - ((Zombie *)b)->health;
}
int main() {
int n, m, h, b;
scanf("%d %d %d %d", &n, &m, &h, &b);
Zombie zombies[n];
int count = 0;
for (int i = 0; i < n; i++) {
scanf("%d %d", &zombies[i].dodge, &zombies[i].health);
if (h + b >= zombies[i].dodge) {
count++;
}
}
qsort(zombies, n, sizeof(Zombie), cmp);
int killed = 0;
for (int i = 0; i < n; i++) {
if (h + b >= zombies[i].dodge) {
if (m >= zombies[i].health) {
killed++;
m -= zombies[i].health;
} else {
break;
}
}
}
printf("%d\n", killed);
return 0;
}
线段覆盖
- 时间限制:1000ms
- 内存限制:131072K
- 语言限制:C语言
在坐标轴上有 n 条线段,每条线段的左端点为 a_i,右端点为 b_i。现在你需要删去部分线段,使得剩下的线段除端点外无公共部分。请你计算最多能保留的线段数目。
输入格式
第一行一个整数 n (1≤n≤10^6),表示线段的条数。
接下来 n 行,每行两个整数 a_i,b_i (0≤a_i<b_i≤10^6)
输出格式
一个整数,表示最多能保留的线段数。
格式说明
输出时每行末尾的多余空格,不影响答案正确性
样例输入
3
0 2
2 4
1 3
样例输出
2
题解:对右端点从小到大排序,记录一下当前已经覆盖到的位置,每次加入左端点大于等于该位置且右端点最小的线段。
我的答案
#include <stdio.h>
#include <stdlib.h>
typedef struct Segment {
int left;
int right;
} Segment;
int cmp(const void *a, const void *b) {
Segment *seg1 = (Segment *)a;
Segment *seg2 = (Segment *)b;
if (seg1->right != seg2->right) {
return seg1->right - seg2->right;
} else {
return seg2->left - seg1->left;
}
}
int main() {
int n;
scanf("%d", &n);
Segment segments[n];
for (int i = 0; i < n; i++) {
scanf("%d %d", &segments[i].left, &segments[i].right);
}
qsort(segments, n, sizeof(Segment), cmp);
int count = 1;
int right = segments[0].right;
for (int i = 1; i < n; i++) {
if (segments[i].left >= right) {
count++;
right = segments[i].right;
}
}
printf("%d\n", count);
return 0;
}
Even More Odd Photos
- 时间限制:2000ms
- 空间限制:262144K
- 语言限制:C语言
Farmer John 正再一次尝试给他的 N 头奶牛拍照(2≤N≤1000 )。
每头奶牛有一个范围在 1…100 之内的整数的「品种编号」。Farmer John 对他的照片有一个十分古怪的构思:他希望将所有的奶牛分为不相交的若干组(换句话说,将每头奶牛分到恰好一组中)并将这些组排成一行,使得第一组的奶牛的品种编号之和为偶数,第二组的编号之和为奇数,以此类推,奇偶交替。
Farmer John 可以分成的最大组数是多少?
输入格式
输入的第一行包含 N 。下一行包含 N 个空格分隔的整数,为 N 头奶牛的品种编号。
输出格式
输出 Farmer John 的照片中的最大组数。可以证明,至少存在一种符合要求的分组方案。
格式说明
输出时每行末尾的多余空格,不影响答案正确性
样例输入1
7
1 3 5 7 9 11 13
样例输出1
3
样例解释1
在这个样例中,以下是一种分成最大组数三组的方案。将 1 和 3 分在第一组,5、7 和 9 分在第二组,11 和 13 分在第三组。
样例输入2
7
11 2 17 13 1 15 3
样例输出2
5
样例解释2
在这个样例中,以下是一种分成最大组数五组的方案。将 2 分在第一组,11 分在第二组,13 和 1 分在第三组,15 分在第四组,17 和 3 分在第五组。
我的答案
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
scanf("%d", &n);
int even = 0, odd = 0;
for (int i = 0; i < n; i++) {
int x;
scanf("%d", &x);
if (x % 2 == 0) {
even++;
} else {
odd++;
}
}
int ans = 0;
while (even > 0 || odd > 0) {
if (ans % 2 == 0) {
if (even > 0) {
even--;
ans++;
} else if (odd > 1) {
odd -= 2;
ans++;
} else {
break;
}
} else {
if ((odd == 2) && (even == 0)) {
break;
} else if (odd > 0) {
odd--;
ans++;
} else {
break;
}
}
}
printf("%d\n", ans);
return 0;
}