资料来源:代码随想录
1.贪心算法理论基础
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
2.分发饼干 455
有两种解法:第一种是尽可能用大饼干喂饱胃口大的小孩,遍历顺序就是从后往前遍历;第二种是尽可能用小饼干喂饱胃口小的小孩,遍历顺序就是从前往后遍历。基本思想都是一样的,但第二种是先遍历饼干再遍历小孩。
注意,小孩必须被放在外层的for循环里,因为如果饼干都不满足这个小孩的话,就要跳过这个小孩,但饼干不能一直被跳过,否则最后一个都喂不饱。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(),g.end()); //先从小到大排序,后面从后往前遍历
sort(s.begin(),s.end());
int result=0; //喂饱的小孩数量
int index=s.size()-1; //饼干的起始下标
for(int i=g.size()-1; i>=0; i--) //小孩从后往前遍历
{
if(index>=0 && s[index]>=g[i]) //如果饼干能够喂饱小孩。注意必须先判断Index不为负数
{
result++; //喂饱小孩数量+1
index--; //这个饼干喂小孩了,就往前移动一个
}
}
return result;
}
};
3.摆动序列 376(别看代码随想录)
本题求摆动序列的长度,即摆动序列里元素的个数。可以统计原序列中相邻两元素之间差值的正负交替次数,最后再+1,就是摆动序列里元素的个数。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if(nums.size()<=1) return nums.size();
int up=1;
int down=1;
for(int i=1; i<nums.size(); i++)
{
if(nums[i]>nums[i-1])
{
up=down+1;
}
if(nums[i]<nums[i-1])
{
down=up+1;
}
}
return max(up,down);
}
};
4.最大子数组和 53
用count来做数值累加,用result来记录过程中的最大值。
注意当count变成负数的时候,它对后面数据的累加一定只会起到降低总和的效果,这时候就不能要现在这个count了,直接置0,等于从下一个元素开始重新累加count。但只要count还是正数,哪怕当前累加的元素是负的,它都可以对后面的累加起到增大总和的作用,所以这个时候还是可以继续使用现在这个count的。不用担心当前元素为负会导致错过最大总和,因为还用result来记录过程中的最大累加值了。数组里全是负数的话代码也没有问题,count每次都会被清零,但result会记录下最小的负数。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result=INT_MIN;
int count=0;
for(int i=0; i<nums.size(); i++)
{
count+=nums[i];
if(count>result)
{
result=count; //更新最大值
}
if(count<0)
{
count=0; //累加为负值时,重新开始
}
}
return result;
}
};
5.买卖股票的最佳时机II 122
每天都可以买卖股票,所以每天都可以产生利润(除了第一天)。所以可以计算出每一天能产生的利润,把所有正利润加起来,就可以获得最大利润了。因为要求的是最大利润,所以不用关心这些利润都是在哪个区间产生的。
局部最优:只收集每天的正利润;全局最优:获得最大利润。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int result=0;
for(int i=1; i<prices.size(); i++)
{
result+=max(prices[i]-prices[i-1],0); //确保只累加正利润
}
return result;
}
};
6.跳跃游戏 55
怎么跳的不重要,关键是看跳跃的覆盖范围能不能覆盖到终点。
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
局部最优:每次跳跃取最大覆盖范围;全局最优:得到整体的最远覆盖范围,看能否覆盖到终点。
i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。
而 cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)。
如果 cover 大于等于了终点下标,直接 return true 就可以了。
class Solution {
public:
bool canJump(vector<int>& nums) {
if(nums.size()==1) return true; //只有一个元素的话必能到达最后一个下标
int cover=0; //可覆盖范围
for(int i=0; i<=cover; i++) //i只能在cover的范围内移动
{
cover=max(i+nums[i],cover); //cover不断在当前元素的基础上扩充
//判断要写在循环里面,因为未必需要循环完才能到最后一个下标
if(cover>=nums.size()-1) return true;
}
//走完整个数组了还没有能cover到最后一个下标,说明不可以
return false;
}
};
7.跳跃游戏II 45
for循环逻辑:每到一个元素,就要更新站在这个元素上,下一步能到达的最远距离范围nextDistance。i一直在++,如果i没到当前能够到达的最远距离范围curDistance,就暂且让它先自增,不做其它处理;如果已经到了当前能够到达的最远距离范围,但还没有到终点,那就说明现在这个不够了,必须要再往前迈一步。“往前迈一步”这个动作不是由i完成的,而是令记录步数的ans+1、并更新当前能够到达的最远距离范围,也就是之前记录的下一步能够到达的最远距离范围。更新之后的当前能够到的最远距离范围如果涵盖了末尾元素,说明可以跳到末尾。而“走到最远距离范围再往前迈一步”的操作也保证了使用最小步数。
class Solution {
public:
int jump(vector<int>& nums) {
if(nums.size()==1) return 0;
int ans=0; //记录最小步数
int curDistance=0; //当前能够到达的最远距离范围
int nextDistance=0; //下一步能够到达的最远距离范围
for(int i=0; i<nums.size(); i++)
{
nextDistance=max(nums[i]+i,nextDistance); //每到一个元素,就更新下一步能够到达的最远距离范围
if(i==curDistance) //i走到当前能够到达的最远距离
{
ans++; //这两行是“往前一步”的操作
curDistance=nextDistance;
if(curDistance>=nums.size()-1) break; //当前能够到达的最远范围包含终点,直接结束循环
}
}
return ans;
}
};
8.K次取反后的最大化数组和 1005
第一次贪心:
局部最优:把绝对值最大的负数取反为正数,使该数值达到最大
全局最优:每个数值都最大后,数组和能够达到最大
第二次贪心:以上过程走完一遍之后K还没消耗完
局部最优:此时已经是一个正整数序列,所以把最小的正整数不断取反,消耗K
全局最优:可能为负的是最小的那个数,所以数组和能够达到最大
步骤:
1.按照绝对值的大小,对数组进行从大到小排序;
2.从前到后,把所有负数进行取反,每取反一次,K--;
3.如果K还没被消耗完,则对最小的数反复取反消耗K,注意这里并不是非要把“不断取反”的过程体现出来,可以简化一下,比如如果K现在是奇数,那么反复取反等于只取反一次,如果是偶数,那么反复取反等于没操作,可以直接跳过这个过程;
4.对数组求和。
class Solution {
public:
static bool cmp(int a, int b)
{
return abs(a)>abs(b);
}
int largestSumAfterKNegations(vector<int>& nums, int K) {
sort(nums.begin(),nums.end(),cmp);
for(int i=0; i<nums.size(); i++)
{
if(nums[i]<0 && K>0)
{
nums[i]*=-1;
K--;
}
}
if(K%2==1) //K是奇数
{
nums[nums.size()-1]*=-1; //这就是对最小整数不断取反的过程
}
int result=0;
for(int a:nums)
{
result+=a;
}
return result;
}
};
10.加油站 134
如果整体的耗油量>加油量,那必然不能跑完一圈。
每个加油站点i的剩余油量为rest[i]=gas[i]-cost[i],i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再次从0开始计算curSum。
局部最优:rest[i]的累加和curSum一旦小于0,说明i及之前的都不能支撑完跑一圈,那么起始位置从i+1重新开始,curSum从0重新开始。
全局最优:找到能跑一圈的起始位置。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int start=0; //起始位置
int curSum=0; //会更新的rest累加和
int totalSum=0; //rest总和
for(int i=0; i<gas.size(); i++)
{
curSum+=gas[i]-cost[i];
totalSum+=gas[i]-cost[i];
if(curSum<0)
{
start=i+1;
curSum=0;
}
}
if(totalSum<0) return -1; //总体加油量小于耗油量,说明必不可以走完一圈
return start;
}
};
11.分发糖果 135
既要比较每个孩子分数和左边的大小,也要比较和右边的大小。所以本题重点在于有两个循环,一个从左到右遍历,得到右>左的结果;一个从右到左遍历,得到左>右的结果。
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candy(ratings.size(),1); //建立和ratings一样大小的数组,初始值为1,保证每个孩子都有一个糖果
//从左向右遍历,找出右>左的,是i和i-1比较,所以从i=1开始
for(int i=1; i<ratings.size(); i++)
{
if(ratings[i]>ratings[i-1])
{
candy[i]=candy[i-1]+1;
}
}
//从右向左遍历,找出左>右的,是i和i+1比较,所以从倒数第二个开始
for(int i=ratings.size()-2; i>=0; i--)
{
if(ratings[i]>ratings[i+1])
{
candy[i]=max(candy[i+1]+1,candy[i]); //第二个candy[i]是上面从左向右遍历时得到的结果
}
}
//收集结果
int result=0;
for(int i=0; i<ratings.size(); i++)
{
result+=candy[i];
}
return result;
}
};
12.柠檬水找零 860
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five=0, ten=0, twenty=0;
for(int bill : bills)
{
//情况1:给的是5美元,不需要找零
if(bill==5)
{
five++;
}
//情况2:给的是10美元,需要找5美元,所以要做能否找零的判断
if(bill==10)
{
if(five<=0) return false;
five--;
ten++;
}
//情况3:给的是20,优先用10找零,因为5用处更多
if(bill==20)
{
if(five>0 && ten>0)
{
five--;
ten--;
}
else if(five>=3)
{
five=five-3;
}
else
{
return false; //找不开了
}
}
}
return true;
}
};
13.根据身高重建队列 406
二维数组,所以排序也有两个维度:h和k。有两个维度的时候,一定要分别考虑,否则容易顾此失彼。
class Solution {
public:
static bool cmp(vector<int>& a, vector<int>& b)
{
if(a[0]==b[0]) return a[1]<b[1]; //身高相同的时候,按照k从小到大排序
return a[0]>b[0]; //否则按照身高从大到小排序
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(),people.end(),cmp); //先按照要求给原二维数组排序
vector<vector<int>> que; //定义一个新数组放结果
//排好序后,就该按照k来插入调整位置了
for(int i=0; i<people.size(); i++)
{
int position=people[i][1]; //插入位置的下标就是k
que.insert(que.begin()+position,people[i]);
}
return que;
}
};
这部分先学到这里。