基本概念
贪心算法:解决问题的策略,局部最优->全局最优
1.把解决问题的过程分为若干步;解决每一步的时候,都选择当前看起来最优的解法;“希望”得到全局最优
2.贪心策略的提出是没有标准答案的;3.“贪心策略”可能是一个错误的方法。
4.正确的贪心策略需要证明。
5.不同题目的贪心策略不同。吸收各种策略,学会证明。
题一.柠檬水找零问题(LeetCode)
题目描述
在柠檬水摊上,每一杯柠檬水的售价为
5
美元。顾客排队购买你的产品,(按账单bills
支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付
5
美元、10
美元或20
美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付5
美元。注意,一开始你手头没有任何零钱。
给你一个整数数组
bills
,其中bills[i]
是第i
位顾客付的账。如果你能给每位顾客正确找零,返回true
,否则返回false
。
输入:bills = [5,5,5,10,20] 输出:true 解释:前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 由于所有客户都得到了正确的找零,所以我们输出 true。输入:bills = [5,5,10,10,20] 输出:false 解释: 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 由于不是每位顾客都得到了正确的找零,所以答案是 false。
题目分析
我们可能收到5元,10元,20元三种数值。5元直接收下即可;10元只有找零5元一种方法;20元有找零一张5元、一张10元和三张5元两种方法。
贪心策略
若收到20元,优先考虑找零5+10,然后再考虑3×5。
题解
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five=0,ten=0;
int x;
for(auto x:bills)
{
if(x==5) five++;
else if(x==10)
{
if(five==0) return false;
five--;ten++;
}
else
{
if(ten&&five)
{
ten--;five--;
}
else if(five>=3)
{
five-=3;
}
else return false;
}
}
return true;
}
};
题二.将数组和减半的最小操作次数(LeetCode)
给你一个正整数数组
nums
。每一次操作中,你可以从nums
中选择 任意 一个数并将它减小到 恰好 一半。(注意,在后续操作中你可以对减半过的数继续执行操作)请你返回将
nums
数组和 至少 减少一半的 最少 操作数。
示例 1:
输入:nums = [5,19,8,1] 输出:3 解释:初始 nums 的和为 5 + 19 + 8 + 1 = 33 。 以下是将数组和减少至少一半的一种方法: 选择数字 19 并减小为 9.5 。 选择数字 9.5 并减小为 4.75 。 选择数字 8 并减小为 4 。 最终数组为 [5, 4.75, 4, 1] ,和为 5 + 4.75 + 4 + 1 = 14.75 。 nums 的和减小了 33 - 14.75 = 18.25 ,减小的部分超过了初始数组和的一半,18.25 >= 33/2 = 16.5 。 我们需要 3 个操作实现题目要求,所以返回 3 。 可以证明,无法通过少于 3 个操作使数组和减少至少一半。示例 2:
输入:nums = [3,8,20] 输出:3 解释:初始 nums 的和为 3 + 8 + 20 = 31 。 以下是将数组和减少至少一半的一种方法: 选择数字 20 并减小为 10 。 选择数字 10 并减小为 5 。 选择数字 3 并减小为 1.5 。 最终数组为 [1.5, 8, 5] ,和为 1.5 + 8 + 5 = 14.5 。 nums 的和减小了 31 - 14.5 = 16.5 ,减小的部分超过了初始数组和的一半, 16.5 >= 31/2 = 15.5 。 我们需要 3 个操作实现题目要求,所以返回 3 。 可以证明,无法通过少于 3 个操作使数组和减少至少一半。
贪心策略
容易想到:每次都选择数组中最大的那个数减半,直到数组和减半为止。
但是,如何找到数组中的最大的那个数决定了这个题的难易复杂程度。这里我们采用priority_queue。
priority_queue即优先级队列,它的使用场景很多,它底层是用大小根堆实现的,可以用log(n)的时间动态地维护数据的有序性。
特点:
自动排序:自动按照元素的大小进行排序,因此每次取出的元素都是当前最大(或最小)的元素。
高效的插入和删除操作:使用堆的数据结构,插入和删除元素的时间复杂度为O(logn),其中n是容器中的元素数量。
题解
class Solution {
public:
int halveArray(vector<int>& nums) {
double sum = 0.0;
int count = 0;
priority_queue<double> heap;
for (auto x : nums)
{
heap.push(x);
sum += x;
}
double half_sum = sum / 2;
while (half_sum > 0)
{
double t = heap.top() / 2;
heap.pop();
half_sum -= t;
count++;
heap.push(t);
}
return count;
}
};
题三.最大数(LeetCode)
给定一组非负整数
nums
,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 1:
输入:nums = [10,2]
输出:“210”
示例 2:
输入:nums = [3,30,34,5,9]
输出:“9534330”
题目分析
排序本质:确定元素的先后顺序。我们需要定义一种排序方法。
贪心策略
把拼接成字符串后,最大的元素放在最前面。如“9”“43”组成的“943”大于“439”,则把“9”放在前面。
优化后的思路:把数转化成字符串,拼接字符串,比较字典序 。
题解
class Solution {
public:
string largestNumber(vector<int>& nums) {
vector<string> strs;
for (int x : nums)
{
strs.push_back(to_string(x));
}
sort(strs.begin(), strs.end(), [](const string& s1, const string& s2)
{
return s1 + s2 > s2 + s1;
});
string ret;
for (auto s : strs)
{
ret += s;
}
if (ret[0] == '0') return "0";
return ret;
}
};
证明这种排序成立
引入全序关系的概念
!! 设a为x位数,b为y位数,c为z位数,则ab可以表示成
完全性
因为ab仍是数,可以比大小,因此具有完全性。
反对称性
ab ≤ ba且ba ≥ ab,则 ab = ba(ab是两数拼接之后的数)
且 ,由夹逼定理则可知道二者相等。
传递性
ab ≤ ba 且 bc ≥ cb,则ac ≥ ca
且 ,需要得到
移项后得
和
相乘后化简得到
即证!
因此我们假设的这种排序成立。