算法沉淀——贪心算法一
- 01.柠檬水找零
- 02.将数组和减半的最少操作次数
- 03.最大数
- 04.摆动序列
贪心算法(Greedy Algorithm)是一种基于贪心策略的优化算法,它通常用于求解最优化问题,每一步都选择当前状态下的最优解,以期望通过局部最优的选择最终达到全局最优。贪心算法的思想是在每一步都做出在当前状态下局部最优的选择,而不考虑未来可能造成的影响。
在应用贪心算法时,通常需要满足两个基本要素:
- 最优子结构性质(Optimal Substructure): 问题的最优解包含了其子问题的最优解。这意味着通过解决子问题可以得到原问题的最优解。
- 贪心选择性质(Greedy-Choice Property): 在做每一步的选择时,选择当前状态下的最优解,即局部最优解。这样希望通过局部最优选择达到全局最优。
贪心算法适用于一些特定类型的问题,但并不适用于所有问题。它通常比动态规划算法更加高效,因为它不需要考虑所有可能的解,只需选择当前状态下的最优解。然而,贪心算法的局限性在于可能会错过全局最优解,因为它没有考虑未来的影响。
经典应用贪心算法的问题包括:
- 活动选择问题(Activity Selection Problem): 选择一组互不相交的活动,使得参与的活动数最大。
- 霍夫曼编码(Huffman Coding): 用变长编码表示不同字符,使得编码长度的加权和最小。
- 最小生成树问题(Minimum Spanning Tree Problem): 在一个连通图中找到一棵包含所有顶点的树,使得树上边的权值之和最小。
- 单源最短路径问题的Dijkstra算法: 在一个带权图中,从一个顶点出发,找到到达其他顶点的最短路径。
需要注意的是,贪心算法并不总是能够找到问题的最优解,因此在应用时需要仔细分析问题的性质,确定贪心选择是否能够保证得到全局最优解。
01.柠檬水找零
题目链接:https://leetcode.cn/problems/lemonade-change/
在柠檬水摊上,每一杯柠檬水的售价为 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。
提示:
1 <= bills.length <= 105
bills[i]
不是5
就是10
或是20
思路
- 当你收到 5 美元时,可以直接收下,不需要找零。
- 当你收到 10 美元时,你可以找零一个 5 美元,因为这样保留了足够的 5 美元找零。
- 当你收到 20 美元时,可以优先选择找零一个 10 美元和一个 5 美元的组合,因为这样可以保留更多的 5 美元找零。
代码
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0, ten = 0;
for(int& 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;
}
};
02.将数组和减半的最少操作次数
题目链接:https://leetcode.cn/problems/minimum-operations-to-halve-array-sum/
给你一个正整数数组 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 个操作使数组和减少至少一半。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 107
思路
- 选择最大值: 每次从数组中选择当前最大的数。这可以通过使用堆来实现,具体来说是最大堆(Max Heap)。最大堆可以在O(1)时间内找到当前最大的元素。
- 减半: 将所选的最大值减半。这一步确保了每次选择都在尽量降低总和。
- 重复操作: 重复上述步骤直到数组和减少到至少一半。
代码
class Solution {
public:
int halveArray(vector<int>& nums) {
priority_queue<double> heap;
double sum=0.0;
for(int x:nums){
heap.push(x);
sum+=x;
}
sum/=2.0;
int count=0;
while(sum>0){
double tmp=heap.top()/2.0;
heap.pop();
sum-=tmp;
count++;
heap.push(tmp);
}
return count;
}
};
03.最大数
题目链接:https://leetcode.cn/problems/largest-number/
给定一组非负整数 nums
,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。
**注意:**输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 1:
输入:nums = [10,2]
输出:"210"
示例 2:
输入:nums = [3,30,34,5,9]
输出:"9534330"
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 109
思路
- 将所有的数字转换为字符串,以便方便拼接和比较。
- 定义一个新的排序规则,按照以下原则排序:
- 如果
A
拼接B
大于B
拼接A
,那么A
在前,B
在后。 - 如果
A
拼接B
等于B
拼接A
,则A
和B
顺序无关。 - 如果
A
拼接B
小于B
拼接A
,那么B
在前,A
在后。
- 如果
- 按照新的排序规则对所有数字进行排序。
- 将排序后的数字依次拼接,得到最大的数字。
代码
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(string& s:strs) ret+=s;
if(ret[0]=='0') return "0";
return ret;
}
};
04.摆动序列
题目链接:https://leetcode.cn/problems/wiggle-subsequence/
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
- 例如,
[1, 7, 4, 9, 2, 5]
是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3)
是正负交替出现的。 - 相反,
[1, 4, 7, 2, 5]
和[1, 7, 4, 5, 5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums
,返回 nums
中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
思路
- 从数组的第一个元素开始,逐个比较相邻的元素,找到第一个波峰(当前元素大于前一个元素且大于后一个元素)或波谷(当前元素小于前一个元素且小于后一个元素)。
- 一旦找到波峰或波谷,将其计数,并开始寻找下一个波峰或波谷。在此过程中,确保不重复计数已经找到的波峰或波谷。
- 重复以上步骤,直到遍历整个数组。
代码
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n=nums.size();
if(n<2) return n;
int ret=0,left=0;
for(int i=0;i<n-1;i++){
int right = nums[i+1]-nums[i];
if(right==0) continue;
if(right*left<=0) ret++;
left=right;
}
return ret+1;
}
};