- C++
C++贪心算法教程
- @ 2025-10-15 18:04:14
C++贪心算法教程(0基础入门)
一、什么是贪心算法?
贪心算法是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望导致结果是全局最优的解题策略。
核心思想:不考虑整体最优,只追求局部最优。通过一系列局部最优选择,最终得到全局最优解(但并非所有问题都适用)。
生活中的贪心例子:
- 找零钱时,先拿面额最大的货币
- 行程规划时,每次选择最近的下一个地点
二、贪心算法的适用条件
- 贪心选择性质:局部最优选择能导致全局最优解
- 最优子结构:问题的最优解包含子问题的最优解
注意:很多问题不能用贪心算法解决(如背包问题),需要根据问题特性判断。
三、贪心算法的基本步骤
- 确定问题的最优子结构
- 设计贪心策略(即如何选择局部最优)
- 证明该策略能得到全局最优解
- 实现算法
四、经典例题详解
例题1:找零钱问题
问题:用最少的硬币凑出指定金额,硬币面额为1、5、10、25。
贪心策略:每次优先选择面额最大的硬币。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 函数功能:用最少硬币凑出金额
// 参数:coins-硬币面额数组,amount-目标金额
// 返回值:最少硬币数量
int coinChange(vector<int>& coins, int amount) {
// 排序硬币面额(从大到小)
sort(coins.rbegin(), coins.rend());
int count = 0; // 记录硬币数量
int remaining = amount; // 剩余需要凑的金额
for (int coin : coins) {
// 如果当前硬币面额小于等于剩余金额,就使用它
while (remaining >= coin) {
remaining -= coin; // 减去当前硬币面额
count++; // 硬币数量加1
cout << "使用" << coin << "元硬币,剩余" << remaining << "元" << endl;
}
// 如果已凑够金额,提前退出
if (remaining == 0) break;
}
// 如果无法凑出目标金额,返回-1
return remaining == 0 ? count : -1;
}
int main() {
vector<int> coins = {1, 5, 10, 25}; // 硬币面额
int amount = 37; // 目标金额
int result = coinChange(coins, amount);
if (result != -1) {
cout << "凑出" << amount << "元最少需要" << result << "枚硬币" << endl;
} else {
cout << "无法凑出" << amount << "元" << endl;
}
return 0;
}
输出结果:
使用25元硬币,剩余12元
使用10元硬币,剩余2元
使用1元硬币,剩余1元
使用1元硬币,剩余0元
凑出37元最少需要4枚硬币
例题2:活动选择问题
问题:有n个活动,每个活动有开始时间和结束时间,如何选择最多的互不冲突的活动?
贪心策略:每次选择结束时间最早的活动。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 定义活动结构体
struct Activity {
int start; // 开始时间
int end; // 结束时间
int id; // 活动编号
// 构造函数
Activity(int s, int e, int i) : start(s), end(e), id(i) {}
};
// 排序函数:按结束时间从小到大排序
bool compare(const Activity& a, const Activity& b) {
return a.end < b.end;
}
// 函数功能:选择最多的不冲突活动
void selectActivities(vector<Activity>& activities) {
// 按结束时间排序
sort(activities.begin(), activities.end(), compare);
vector<Activity> selected; // 存储选中的活动
// 选择第一个活动(结束时间最早的)
selected.push_back(activities[0]);
int lastEnd = activities[0].end; // 记录最后一个选中活动的结束时间
// 遍历剩余活动
for (size_t i = 1; i < activities.size(); i++) {
// 如果当前活动的开始时间 >= 最后一个选中活动的结束时间
if (activities[i].start >= lastEnd) {
selected.push_back(activities[i]);
lastEnd = activities[i].end; // 更新最后结束时间
}
}
// 输出结果
cout << "选中的活动有:";
for (const Activity& a : selected) {
cout << "活动" << a.id << "(" << a.start << "-" << a.end << ") ";
}
cout << endl;
cout << "共选中" << selected.size() << "个活动" << endl;
}
int main() {
// 创建活动列表
vector<Activity> activities;
activities.emplace_back(1, 4, 1); // 活动1:1-4点
activities.emplace_back(3, 5, 2); // 活动2:3-5点
activities.emplace_back(0, 6, 3); // 活动3:0-6点
activities.emplace_back(5, 7, 4); // 活动4:5-7点
activities.emplace_back(3, 9, 5); // 活动5:3-9点
activities.emplace_back(5, 9, 6); // 活动6:5-9点
activities.emplace_back(6, 10, 7); // 活动7:6-10点
activities.emplace_back(8, 11, 8); // 活动8:8-11点
activities.emplace_back(8, 12, 9); // 活动9:8-12点
activities.emplace_back(2, 14, 10); // 活动10:2-14点
activities.emplace_back(12, 16, 11);// 活动11:12-16点
selectActivities(activities);
return 0;
}
输出结果:
选中的活动有:活动1(1-4) 活动4(5-7) 活动8(8-11) 活动11(12-16)
共选中4个活动
例题3:区间覆盖问题
问题:用最少的区间覆盖指定线段[start, end]。
贪心策略:每次选择能覆盖当前未覆盖部分且延伸最远的区间。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 区间结构体
struct Interval {
int left; // 左端点
int right; // 右端点
Interval(int l, int r) : left(l), right(r) {}
};
// 排序函数:按左端点从小到大排序
bool compare(const Interval& a, const Interval& b) {
return a.left < b.left;
}
// 函数功能:用最少区间覆盖[targetStart, targetEnd]
int coverLine(vector<Interval>& intervals, int targetStart, int targetEnd) {
// 排序区间
sort(intervals.begin(), intervals.end(), compare);
int count = 0; // 选中的区间数量
int currentEnd = targetStart; // 当前已覆盖到的位置
int nextEnd = currentEnd; // 下一步能覆盖到的最远位置
int i = 0; // 当前检查的区间索引
int n = intervals.size();
while (currentEnd < targetEnd) {
// 找到所有左端点 <= 当前已覆盖位置的区间,选择其中右端点最大的
while (i < n && intervals[i].left <= currentEnd) {
nextEnd = max(nextEnd, intervals[i].right);
i++;
}
// 如果无法再延伸,说明不能完全覆盖
if (nextEnd == currentEnd) {
return -1;
}
currentEnd = nextEnd; // 更新已覆盖位置
count++; // 区间数量加1
}
return count;
}
int main() {
// 创建区间列表
vector<Interval> intervals;
intervals.emplace_back(1, 4);
intervals.emplace_back(2, 5);
intervals.emplace_back(3, 6);
intervals.emplace_back(5, 7);
intervals.emplace_back(8, 10);
intervals.emplace_back(9, 12);
int targetStart = 1; // 目标线段起点
int targetEnd = 12; // 目标线段终点
int result = coverLine(intervals, targetStart, targetEnd);
if (result != -1) {
cout << "覆盖线段[" << targetStart << "," << targetEnd << "]最少需要" << result << "个区间" << endl;
} else {
cout << "无法完全覆盖线段[" << targetStart << "," << targetEnd << "]" << endl;
}
return 0;
}
输出结果:
覆盖线段[1,12]最少需要3个区间
五、贪心算法的优缺点
优点:
- 简单直观,容易实现
- 时间复杂度低,效率高
缺点:
- 不是所有问题都适用
- 难以证明正确性
- 可能得到局部最优而非全局最优
六、练习题
- 柠檬水找零:顾客排队买柠檬水,每杯5元。顾客支付5元、10元或20元,初始没有零钱,判断能否给所有顾客找零。
- 分发饼干:每个孩子有一个饥饿度,每个饼干有一个大小,一个饼干只能给一个孩子,且饼干大小必须>=饥饿度,求最多能满足几个孩子。
- 摆动序列:一个整数序列,相邻元素的差正负交替,求最长摆动子序列的长度。
通过以上内容,你应该对贪心算法有了基本了解。贪心算法的关键在于找到合适的贪心策略,多做练习才能熟练掌握如何设计和应用贪心算法。
1 条评论
-
admin SU @ 2025-10-15 18:06:32
C++贪心算法教程(0基础版)
贪心算法是解决问题的一种“短视”策略,核心是每一步都做当前看起来最优的选择,不回头考虑未来,最终希望得到全局最优解。它不适合所有问题,但在特定场景下能高效得出答案,是0基础学习者必须掌握的基础算法思想之一。
一、贪心算法的核心概念:什么是“贪心”?
贪心算法的本质可以用“走一步看一步,每次都选最好的”来概括,关键在于两个核心要素:
- 局部最优解:每一步决策只关注当前状态下的最优选择,比如“选最大的数”“走最短的路”。
- 全局最优解:通过所有局部最优解的积累,最终恰好得到整个问题的最优解。
生活中的贪心例子
- 找零钱问题:超市找你18元,优先用10元(最大面额),再用5元,最后用3张1元,这就是贪心策略,能让使用的纸币数量最少。
- 选课问题:想选最多课程,优先选课时最短的课,就能在有限时间内选更多课。
二、贪心算法的适用条件:不是所有问题都能“贪心”
贪心算法不是万能的,只有满足以下两个条件时才能使用,这是判断能否用贪心的关键:
- 贪心选择性质:每一步的局部最优选择,能导向全局最优解。
比如“找零钱”中,选最大面额纸币的局部最优,最终一定能得到纸币数量最少的全局最优。 - 无后效性:当前的决策不会影响未来的决策。
比如“最短路径问题”中,从A到B选了最短的路,后续从B到C的决策只和B有关,和A到B的选择无关。
反例:不适合贪心的问题
- 背包问题(0-1型):有一个容量为5的背包,物品A(重量3,价值5)、物品B(重量2,价值4)。如果用贪心(优先选价值高的A),最终只能装A,总价值5;但实际最优是装B+剩余容量装其他小物品(或仅B),总价值4?不,这里正确最优是A(5)比B(4)好?不对,重新举例:物品A(重量4,价值5)、物品B(重量3,价值4)、物品C(重量2,价值3),背包容量5。贪心选A(价值5),剩余1容量装不下其他,总价值5;但选B+C(重量5,价值7),总价值更高。这说明0-1背包不满足贪心选择性质,不能用贪心。
三、贪心算法的解题步骤:0基础也能按步走
掌握固定步骤,能让你面对贪心问题时不再迷茫,具体分为4步:
步骤1:明确问题目标
先确定你要解决的问题“要什么”,比如“找零钱时纸币数量最少”“路径总长度最短”“获得的价值最大”。
例:给定一组整数,挑选k个数字,使它们的和最大。目标就是“k个数字的和最大”。步骤2:设计“贪心策略”
根据目标,确定“每一步选什么”才是局部最优。策略必须具体、可执行。
例:要选k个数字和最大,贪心策略就是“每次都选当前剩下的数字中最大的那个”。步骤3:验证策略有效性
判断设计的策略是否满足“贪心选择性质”和“无后效性”,避免用错算法。
例:选最大数字的策略,每次选最大的,k次后总和一定最大(因为大数字贡献更多),满足贪心性质,有效。步骤4:用C++代码实现
将策略转化为代码,核心是“排序”(常用)或“遍历选最优”,具体分3小步:
- 数据准备:定义存储数据的变量(如数组),输入问题数据。
- 执行贪心策略:按设计的策略逐步选择(比如先排序,再选前k个最大数)。
- 输出结果:计算并输出最终结果(如k个数字的和)。
四、实战案例:从0开始写C++贪心代码
以“选k个数字使和最大”为例,一步步实现代码,每个步骤都标注注释,0基础也能看懂。
案例需求
输入:一个整数数组(如[3,1,4,2,5]),一个整数k(如2)。
输出:选k个数字的最大和(如5+4=9)。代码实现(带详细注释)
#include <iostream> // 用于输入输出 #include <vector> // 用于存储数组(比普通数组更灵活) #include <algorithm> // 用于排序(贪心常用工具) using namespace std; // 简化代码,不用每次写std:: int main() { // 步骤1:数据准备(输入问题数据) int n, k; // n是数组长度,k是要选的数字个数 cout << "请输入数组长度n和要选的数字个数k:"; cin >> n >> k; // 输入n和k,比如输入5 2 vector<int> nums(n); // 定义一个长度为n的数组nums cout << "请输入数组的" << n << "个数字:"; for (int i = 0; i < n; i++) { cin >> nums[i]; // 输入数组元素,比如输入3 1 4 2 5 } // 步骤2:执行贪心策略(排序+选前k个最大数) // 贪心策略:先把数组从大到小排序,再选前k个数字求和 sort(nums.rbegin(), nums.rend()); // 从大到小排序,排序后nums为[5,4,3,2,1] int max_sum = 0; // 存储最大和 for (int i = 0; i < k; i++) { max_sum += nums[i]; // 累加前k个数字,即5+4=9 } // 步骤3:输出结果 cout << "选" << k << "个数字的最大和为:" << max_sum << endl; // 输出9 return 0; }代码运行流程
- 输入:先输入
5 2(数组长度5,选2个数字),再输入3 1 4 2 5(数组元素)。 - 排序:
sort(nums.rbegin(), nums.rend())将数组变成[5,4,3,2,1]。 - 求和:循环2次,累加
5和4,得到9。 - 输出:打印“选2个数字的最大和为:9”。
五、常见贪心问题分类:知道这些,遇到题不慌
0基础学习者先掌握以下3类经典问题,能覆盖大部分贪心场景:
1. 排序类贪心(最常用)
核心思路:先对数据排序(从小到大/从大到小),再按顺序选择局部最优。
典型问题:- 选k个最大数求和(如上面的案例)。
- 活动安排问题:选最多不冲突的活动(先按活动结束时间排序,优先选结束早的)。
2. 分配类贪心
核心思路:按“优先级”分配资源,每次给最需要的对象分配。
典型问题:- 找零钱问题:用最少纸币找零(优先用大面额纸币)。
- 分糖果问题:每个孩子至少1颗糖,分数高的孩子糖更多(先给每个孩子1颗,再给分数高的补糖)。
3. 路径类贪心
核心思路:每一步选最短/最优的路径,逐步逼近终点。
典型问题:- 最短路径(Dijkstra算法):从起点到各点的最短路径,每次选当前最近的点。
- 最小生成树(Prim算法):连接所有点且总边长最小,每次选最短的边连接新点。
六、贪心算法的优缺点:理性看待,合理使用
优点
- 效率高:不需要像动态规划那样存储大量中间状态,时间复杂度通常是排序的O(nlogn)或遍历的O(n)。
- 代码简单:逻辑直观,实现难度低,0基础学习者容易上手。
缺点
- 局限性大:只适用于满足“贪心选择性质”的问题,很多问题用贪心会得到错误答案(如0-1背包)。
- 难以验证:判断一个问题是否能用贪心,需要严谨的证明,新手容易凭直觉用错。
七、学习建议:0基础如何学好贪心?
- 先掌握排序:贪心算法常依赖排序(如选最大数、按结束时间排序),先熟练掌握C++的
sort函数(正序、逆序)。 - 从经典题入手:先做“找零钱”“活动安排”“选k个最大数”等简单题,理解策略设计思路。
- 多对比错误案例:尝试用贪心解决“0-1背包”等不适合的问题,观察错误结果,加深对适用条件的理解。
为了帮你巩固基础,我可以帮你生成一份**《贪心算法经典例题及C++代码合集》**,包含找零钱、活动安排等5个经典问题,每个例题都有详细注释和思路分析,你需要吗?
- 1