专题九 贪心算法
一、柠檬水找零
1.题目
Leetcode:第 860 题
在柠檬水摊上,每一杯柠檬水的售价为 5
美元。顾客排队购买你的产品,(按账单 bills
支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5
美元、10
美元或 20
美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5
美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills
,其中 bills[i]
是第 i
位顾客付的账。如果你能给每位顾客正确找零,返回 true
,否则返回 false
。
示例 1:
输入:bills = [5,5,5,10,20] 输出:true 解释: 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 由于所有客户都得到了正确的找零,所以我们输出 true。
示例 2:
输入:bills = [5,5,10,10,20] 输出:false 解释: 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 由于不是每位顾客都得到了正确的找零,所以答案是 false。
2.解题思路
使用贪心算法解决柠檬水找零问题。
在 lemonadeChange
函数中,使用三个变量 five
、ten
和 twenty
来跟踪不同面额纸币的数量。我们遍历 bills
数组,根据账单面额更新纸币数量,并确保在任何时候都有足够的纸币来找零。如果在任何时候无法找零,函数将返回 false
。如果所有账单都能被成功处理,函数最终返回 true
。这个方法利用了贪心算法的思想,总是优先使用面额较大的纸币来找零,以便保留更多的小面额纸币,从而提高找零的灵活性。
3.实现代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
// lemonadeChange 函数用于检查是否有足够的零钱来找零给所有顾客
bool lemonadeChange(vector<int>& bills) {
int five = 0, ten = 0, twenty = 0;// 初始化三种纸币的数量:5美元、10美元和20美元
// 遍历 bills 中的每个账单
for (int bill : bills) {
// 情况一:顾客支付了5美元
if (bill == 5) {
five++; // 5美元纸币的数量加1
}
// 情况二:顾客支付了10美元
if (bill == 10) {
// 如果没有5美元纸币,无法找零,返回false
if (five <= 0) return false;
ten++; // 10美元纸币的数量加1
five--; // 用一张5美元纸币找零
}
// 情况三:顾客支付了20美元
if (bill == 20) {
// 优先使用10美元和5美元的组合来找零,因为这样可以保留更多的5美元纸币
if (five > 0 && ten > 0) {
five--; // 使用一张5美元纸币
ten--; // 使用一张10美元纸币
// twenty++ 这行代码可以删除,因为后续不会再使用20美元纸币
}
else if (five >= 3) {
// 如果没有10美元纸币,需要三张5美元纸币来找零
five -= 3; // 使用三张5美元纸币
// twenty++ 这行代码也可以删除,理由同上
}
else {
// 如果既没有10美元纸币也没有足够的5美元纸币,无法找零,返回false
return false;
}
}
}
// 如果遍历完所有账单都能成功找零,返回true
return true;
}
};
//测试
int main()
{
Solution p;
vector<int> bills = { 5, 5, 5, 10, 20 };
bool result = p.lemonadeChange(bills);
cout << "能否给每位顾客正确找零:" << result << endl;
cout << endl;
return 0;
}
二、根据身高重建队列
1.题目
Leetcode:第 406 题
假设有打乱顺序的一群人站成一个队列,数组 people
表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki]
表示第 i
个人的身高为 hi
,前面 正好 有 ki
个身高大于或等于 hi
的人。
请你重新构造并返回输入数组 people
所表示的队列。返回的队列应该格式化为数组 queue
,其中 queue[j] = [hj, kj]
是队列中第 j
个人的属性(queue[0]
是排在队列前面的人)。
示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 解释: 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]] 输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
2.解题思路
使用贪心算法解决根据身高重建队列问题。
在 reconstructQueue
函数中,我们首先使用 sort
函数和一个自定义的比较函数 cmp
对输入的 people
进行排序。cmp
函数确保了身高更高的人排在前面,如果身高相同,则原始位置较小的人排在前面。排序后,我们遍历 people
,并使用 vector
的 insert
方法在新队列 que
的适当位置插入每个人。由于 people
已经根据身高和原始位置排序,插入操作会保持这些人的相对顺序,从而重构出原始的队列。最终,函数返回重构后的队列 que
。
3.实现代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
// cmp 函数用于比较两个 vector<int> 对象的大小
// 这个函数被用作 sort 函数的比较函数
static bool cmp(const vector<int>& a, const vector<int>& b) {
// 如果第一个元素相同,则比较第二个元素
// 如果第二个元素小的排在前面,则返回 true
if (a[0] == b[0]) return a[1] < b[1];
// 否则,比较第一个元素,第一个元素大的排在前面
return a[0] > b[0];
}
// reconstructQueue 函数用于重构一个队列
// people 是一个 vector<vector<int>> 对象,其中每个内部 vector 包含两个整数
// 第一个整数表示人的身高,第二个整数表示在队列中的原始位置
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
// 使用自定义的比较函数对 people 进行排序
// 排序规则是:首先按照身高降序,如果身高相同,则按照原始位置升序
sort(people.begin(), people.end(), cmp);
// 初始化队列,将用于存放最终的队列顺序
vector<vector<int>> que;
// 遍历排序后的 people
for (int i = 0; i < people.size(); i++) {
// 获取当前人的原始位置
int position = people[i][1];
// 在 que 的 position 位置插入当前人
// 由于 vector 的插入操作会移动元素,所以这将重建原始的队列顺序
que.insert(que.begin() + position, people[i]);
}
// 返回重构后的队列
return que;
}
};
//测试
int main()
{
Solution p;
vector<vector<int>> people = {{7, 0}, {4, 4}, {7, 1}, {5, 0}, {6, 1}, {5, 2}};
vector<vector<int>> result = p.reconstructQueue(people);
cout << "重构后的队列:"<<endl;
for (auto& ans : result) {
cout << "[";
for (auto& i : ans) {
cout << i << ",";
}
cout << "]" << " ,";
}
cout << endl;
cout << endl;
return 0;
}
三、用最少数量的箭引爆气球
1.题目
Leetcode:第 452 题
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points
,其中points[i] = [xstart, xend]
表示水平直径在 xstart
和 xend
之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x
处射出一支箭,若有一个气球的直径的开始和结束坐标为 x
start
,x
end
, 且满足 xstart ≤ x ≤ x
end
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points
,返回引爆所有气球所必须射出的 最小 弓箭数 。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]] 输出:2 解释:气球可以用2支箭来爆破: -在x = 6处射出箭,击破气球[2,8]和[1,6]。 -在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]] 输出:4 解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]] 输出:2 解释:气球可以用2支箭来爆破: - 在x = 2处发射箭,击破气球[1,2]和[2,3]。 - 在x = 4处射出箭,击破气球[3,4]和[4,5]。
2.解题思路
使用贪心算法解决用最少数量的箭引爆气球问题。
在 findMinArrowShots
函数中,我们首先检查 points
是否为空,如果为空,直接返回 0。然后,我们使用 sort
函数和一个自定义的比较函数 cmp
对 points
进行排序,确保所有气球按照起始时间的升序排列。接下来,我们初始化 result
为 1,因为至少需要一支箭来射爆第一个气球。之后,我们遍历 points
,检查每个气球是否与前一个气球重叠。如果重叠,我们更新它们的结束时间;如果不重叠,我们增加 result
的值,因为这表示需要额外一支箭。最终,函数返回 result
,即射爆所有气球所需的最小箭数。
3.实现代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
private:
// cmp 函数用于比较两个 vector<int> 对象,即比较两个气球的起始时间
// 这个函数被用作 sort 函数的比较函数
static bool cmp(const vector<int>& a, const vector<int>& b) {
return a[0] < b[0]; // 返回 a 的起始时间是否小于 b 的起始时间
}
public:
// findMinArrowShots 函数用于找出最少需要多少支箭来射爆所有气球
int findMinArrowShots(vector<vector<int>>& points) {
// 如果 points 为空,不需要箭
if (points.size() == 0) return 0;
// 对 points 进行排序,按照气球的起始时间进行升序排序
sort(points.begin(), points.end(), cmp);
// 初始化结果 result 为 1,因为即使只有一个气球,也需要至少一支箭
int result = 1;
// 遍历 points,从第二个气球开始(索引为 1)
for (int i = 1; i < points.size(); i++) {
// 如果当前气球的起始时间大于前一个气球的结束时间,说明它们不重叠
if (points[i][0] > points[i - 1][1]) {
// 气球 i 和气球 i-1 不挨着,需要额外一支箭
result++;
}
else {
// 如果当前气球与前一个气球重叠(相邻),则更新重叠气球的最小结束时间
// 取前一个气球的结束时间和当前气球的结束时间的较小值作为新的结束时间
points[i][1] = min(points[i - 1][1], points[i][1]);
}
}
return result;// 返回所需的最小箭数
}
};
//测试
int main()
{
Solution p;
vector<vector<int>> points = { {10,16} ,{2,8},{1,6},{7,12} };
int result = p.findMinArrowShots(points);
cout << "所需的最小箭数:" << result << endl;
cout << endl;
return 0;
}
ps:以上皆是本人在探索算法旅途中的浅薄见解,诚挚地希望得到各位的宝贵意见与悉心指导,若有不足或谬误之处,还请多多指教。