第十七章:贪心算法——每次都选最好的¶
你好!欢迎来到第十七章!在生活中,我们经常要做选择:吃自助餐时,是先吃喜欢的菜还是随便拿?去游乐场玩,是先玩排队时间长的项目还是短的?贪心算法就是一种“每次都选当前最好的”的策略。虽然不一定能得到全局最优解,但在很多问题上却非常有效。这一章我们就来学习贪心算法,看看什么时候可以“贪心”。
17.1 贪心算法程序范例¶
先来看一个最简单的贪心问题:找零钱问题。假设我们需要用最少的硬币数量凑出一定的金额,硬币面额有 1元、2元、5元、10元。贪心策略是:每次都尽量用面额最大的硬币。
#include <iostream>
using namespace std;
int main() {
int amount;
cout << "请输入金额(元):";
cin >> amount;
int coins[] = {10, 5, 2, 1}; // 硬币面额,从大到小排序
int count = 0;
for (int i = 0; i < 4; i++) {
int num = amount / coins[i]; // 用多少枚当前面额的硬币
count += num;
amount -= num * coins[i];
if (num > 0) {
cout << "需要" << coins[i] << "元硬币:" << num << "枚" << endl;
}
}
cout << "总共需要" << count << "枚硬币" << endl;
return 0;
}
运行示例(输入17):
需要10元硬币:1枚
需要5元硬币:1枚
需要2元硬币:1枚
总共需要3枚硬币
这里我们每次都用最大面额的硬币,这就是贪心:在当前步,选择当前最好的(面额最大的),最终得到了最少硬币数。注意,这个策略对这套面额有效,但如果面额是 1、3、4 元,要凑 6 元,贪心会先选4,再选1和1,共3枚,但最优是 3+3 只用2枚,说明贪心不一定总是最优。所以我们用贪心时要先确认问题是否适合贪心。
17.2 贪心算法的用法¶
17.2.1 什么是贪心算法?¶
贪心算法(Greedy Algorithm)是指在每一步选择中都采取当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的算法。就像吃自助餐时,每次都拿自己最喜欢的菜,不一定能吃得最健康,但能吃得最开心。
贪心算法通常用于求解最优化问题,比如找最少硬币、安排最多活动、求最短路径等。
17.2.2 贪心算法的特点¶
- 局部最优:每一步都选当前最优的。
- 不可回溯:一旦做出选择,就不再改变。
- 不一定得到全局最优:需要问题具有贪心选择性质(即局部最优能导致全局最优)。
17.2.3 贪心算法的适用条件¶
- 贪心选择性质:可以通过局部最优选择来得到全局最优解。
- 最优子结构性质:问题的最优解包含子问题的最优解。
17.2.4 贪心算法的一般步骤¶
- 建立数学模型来描述问题。
- 把问题分解成若干个子问题。
- 对每个子问题,按照某种规则(贪心策略)做出当前最优选择。
- 把所有子问题的局部最优解合成原问题的一个解。
17.3 编程实例讲解¶
实例1:活动安排问题¶
题目:有若干个活动,每个活动有开始时间和结束时间。如何安排活动,使得能参加的活动数量最多?(假设同一时间只能参加一个活动)
贪心策略:按结束时间最早的活动优先选择。因为结束早,留给后面的时间就多。
#include <iostream>
#include <algorithm> // 用到sort
using namespace std;
struct Activity {
int start;
int end;
};
// 按结束时间升序排序
bool cmp(Activity a, Activity b) {
return a.end < b.end;
}
int main() {
int n;
cout << "请输入活动个数:";
cin >> n;
Activity acts[100];
for (int i = 0; i < n; i++) {
cin >> acts[i].start >> acts[i].end;
}
// 按结束时间排序
sort(acts, acts + n, cmp);
int count = 1; // 至少选第一个活动
int lastEnd = acts[0].end; // 上一个活动的结束时间
for (int i = 1; i < n; i++) {
if (acts[i].start >= lastEnd) { // 如果当前活动开始时间在上一个活动结束后
count++;
lastEnd = acts[i].end;
}
}
cout << "最多可以参加" << count << "个活动" << endl;
return 0;
}
输入示例:
5
1 3
2 5
4 7
6 9
8 10
输出:最多可以参加3个活动(比如选 1-3, 4-7, 8-10)。
实例2:背包问题(部分背包)¶
题目:有一个背包,容量为C。有n种物品,每种物品有重量 w[i] 和价值 v[i]。可以只取一部分(比如切分),问如何装使得总价值最大?
贪心策略:按单位重量价值(v[i]/w[i])从高到低选取,优先装价值率高的物品。
#include <iostream>
#include <algorithm>
using namespace std;
struct Item {
int weight;
int value;
double ratio; // 单位重量价值
};
bool cmp(Item a, Item b) {
return a.ratio > b.ratio;
}
int main() {
int n, capacity;
cout << "请输入物品数量和背包容量:";
cin >> n >> capacity;
Item items[100];
for (int i = 0; i < n; i++) {
cin >> items[i].weight >> items[i].value;
items[i].ratio = (double)items[i].value / items[i].weight;
}
sort(items, items + n, cmp); // 按价值率降序
double totalValue = 0;
int remaining = capacity;
for (int i = 0; i < n; i++) {
if (items[i].weight <= remaining) {
// 可以全拿
totalValue += items[i].value;
remaining -= items[i].weight;
} else {
// 只能拿一部分
totalValue += items[i].ratio * remaining;
break;
}
}
cout << "最大总价值:" << totalValue << endl;
return 0;
}
注意:这是部分背包问题(物品可分割),贪心有效。如果是0-1背包(物品不可分割),贪心不一定最优,需要用动态规划。
实例3:排队打水问题¶
题目:有n个人排队打水,每个人打水需要的时间不同。如何安排打水顺序,使得所有人等待的总时间最少?
贪心策略:让打水时间短的人先打。因为这样后面的人等待时间总和最小。
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n;
cout << "请输入人数:";
cin >> n;
int times[100];
for (int i = 0; i < n; i++) {
cin >> times[i];
}
sort(times, times + n); // 升序排序
int totalWait = 0;
int currentTime = 0;
for (int i = 0; i < n; i++) {
totalWait += currentTime; // 第i个人等待的时间
currentTime += times[i]; // 当前累计时间
}
cout << "总等待时间:" << totalWait << endl;
return 0;
}
实例4:删数问题¶
题目:给定一个数字字符串(如 “1432219”),删除 k 个数字,使得剩下的数字最小(保持相对顺序)。
贪心策略:从左到右遍历,如果当前数字比下一个数字大,就删除当前数字,这样能让高位变小。
#include <iostream>
#include <string>
using namespace std;
string removeKdigits(string num, int k) {
string result;
for (char digit : num) {
while (!result.empty() && result.back() > digit && k > 0) {
result.pop_back(); // 删除前面比当前大的数
k--;
}
result.push_back(digit);
}
// 如果还有剩余删除次数,从末尾删
while (k > 0 && !result.empty()) {
result.pop_back();
k--;
}
// 去除前导零
int start = 0;
while (start < result.size() && result[start] == '0') start++;
if (start == result.size()) return "0";
return result.substr(start);
}
int main() {
string num;
int k;
cout << "请输入数字字符串和要删除的个数:";
cin >> num >> k;
cout << "删除后最小的数字:" << removeKdigits(num, k) << endl;
return 0;
}
17.4 阶段性编程练习¶
- 练习1:用贪心算法解决找零钱问题,硬币面额包括 1、5、10、20、50、100,凑出指定金额,输出每种硬币的数量。
- 练习2:有 n 个任务,每个任务有截止时间和收益,每个任务耗时1个单位时间。如何安排任务使得总收益最大?(提示:按收益降序,尽量放在截止时间前完成)
- 练习3:区间覆盖问题:给定一个线段区间 [s, t] 和若干条线段,选择最少的线段覆盖整个区间。(提示:按左端点排序,每次选能覆盖当前起点的最右端点)
- 练习4:小船过河问题:n 个人要过河,只有一条船,每次最多载两人,每个人过河时间不同,求所有人过河的最短总时间。(经典贪心问题,有两种策略)
- 练习5:哈夫曼编码思想:给定一些字符及其出现频率,构造哈夫曼树(用优先队列模拟)。
17.5 第17章编程作业¶
作业1:加油站问题¶
有一条环形公路,上有 n 个加油站,每个加油站可以加油 gas[i] 升,从第 i 个加油站到第 i+1 个需要消耗 cost[i] 升。你开一辆油箱无限的车,开始时油箱为空,问能否从某个加油站出发绕一圈回到起点?如果能,返回起点编号。(经典贪心,只要总油量≥总消耗,就一定有解,然后找起点)
作业2:分发饼干¶
每个孩子有一个胃口值 g[i],每个饼干有一个尺寸 s[j]。每个孩子最多只能给一块饼干,且饼干尺寸必须大于等于孩子的胃口。问最多能满足多少个孩子?(贪心:尽量用最小的饼干满足胃口最小的孩子)
作业3:跳跃游戏¶
给定一个非负整数数组,初始位置在第一个下标,数组每个元素表示在该位置能跳跃的最大长度。判断能否到达最后一个下标。(贪心:维护最远能到达的位置)
作业4:买卖股票的最佳时机 II¶
给定一个数组,第 i 天股票价格为 prices[i]。可以多次买卖(但只能持有一股),求最大利润。(贪心:只要第二天价格比前一天高,就前一天买第二天卖)
作业5:任务调度器¶
给定任务列表和冷却时间 n,相同任务之间必须间隔 n 个单位时间。求完成所有任务的最短时间。(贪心:每次安排剩余次数最多的任务)
恭喜你完成了第十七章的学习!贪心算法是一种简单而强大的思想,但要注意它不一定总是得到最优解,需要具体问题具体分析。加油!🚀