- 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
题目:分发饼干
-
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子
i
,都有一个胃口值g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j
,都有一个尺寸s[j]
。如果s[j] >= g[i]
,我们可以将这个饼干j
分配给孩子i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 -
class Solution { public: int findContentChildren(vector<int>& g, vector<int>& s) { int g_p=g.size()-1,s_p=s.size()-1; sort(g.begin(),g.end()); sort(s.begin(),s.end()); int count = 0; while(s_p>-1 && g_p>-1){ if(g[g_p]<=s[s_p]){ g_p--; s_p--; count++; }else{ g_p--; } } return count; } };
-
为了满足更多的小孩,就不要造成饼干尺寸的浪费。大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。可以尝试使用贪心策略,先将饼干数组和小孩数组排序。然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
-
class Solution { public: int findContentChildren(vector<int>& g, vector<int>& s) { sort(g.begin(),g.end()); sort(s.begin(),s.end()); int index=s.size()-1,res=0; for(int i=g.size()-1;i>=0;i--){ if(index<0){ break; } if(g[i]<=s[index]){ index--; res++; } } return res; } };
-
时间复杂度:O(nlogn);空间复杂度:O(1)。
-
从代码中可以看出我用了一个 index 来控制饼干数组的遍历,遍历饼干并没有再起一个 for 循环,而是采用自减的方式,这也是常用的技巧。
题目:摆动序列
-
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。给你一个整数数组
nums
,返回nums
中作为 摆动序列 的 最长子序列的长度 。 -
class Solution { public: int wiggleMaxLength(vector<int>& nums) { if(nums.size()<2){ return nums.size(); } int curdiff=0; int prediff=0; int count=1; for(int i=0;i<nums.size()-1;i++){ curdiff=nums[i+1]-nums[i]; if((prediff<=0&&curdiff>0)||(prediff>=0&&curdiff<0)){ count++; prediff=curdiff;//注意这里,只在摆动变化的时候更新prediff } } return count; } };
-
本题异常情况的本质,就是要考虑平坡, 平坡分两种,一个是 上下中间有平坡,一个是单调有平坡,如图:
-
时间复杂度:O(n);空间复杂度:O(1)
题目:最大子数组和
-
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。 -
暴力解法的思路,第一层 for 就是设置起始位置,第二层 for 循环遍历数组寻找最大值
-
class Solution { public: int maxSubArray(vector<int>& nums) { int res = INT32_MIN; int count; for(int i=0;i<nums.size();i++){ count=0; for(int j=i;j<nums.size();j++){ count += nums[j]; res = count>res?count:res; } } return res; } };//超时 class Solution { public: int maxSubArray(vector<int>& nums) { int res = INT32_MIN; int count; for(int i=0;i<nums.size();i++){ if(nums[i]<=0){ continue; } count=0; for(int j=i;j<nums.size();j++){ count += nums[j]; res = count>res?count:res; } } if(res==INT32_MIN){ res=nums[0]; for(int i=1;i<nums.size();i++){ res= res>nums[i]?res:nums[i]; } } return res; } };
-
-
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。全局最优:选取最大“连续和”。从代码角度上来讲:遍历 nums,从头开始用 count 累积,如果 count 一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 count 了,因为已经变为负数的 count,只会拖累总和。这相当于是暴力解法中的不断调整最大子序和区间的起始位置。
-
class Solution { public: int maxSubArray(vector<int>& nums) { int res=INT32_MIN; int count=0; for(int i=0;i<nums.size();i++){ count+=nums[i]; if(count>res){ res=count; } if(count<=0){ count=0; } } return res; } };
-
时间复杂度:O(n);空间复杂度:O(1)
题目:买卖股票的最佳时机 II
-
给你一个整数数组
prices
,其中prices[i]
表示某支股票第i
天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润 。 -
选一个低的买入,再选个高的卖,再选一个低的买入…循环反复。局部最优:收集每天的正利润,全局最优:求得最大利润。
-
class Solution { public: int maxProfit(vector<int>& prices) { int res=0; for(int i=0;i<prices.size()-1;i++){ res += (prices[i+1]-prices[i]) > 0?(prices[i+1]-prices[i]):0; } return res; } };
题目:跳跃游戏
-
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
-
不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。这个范围内,别管是怎么跳的,反正一定可以跳过来。**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!**每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
-
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
-
class Solution { public: bool canJump(vector<int>& nums) { int cover=0; if(nums.size()<=1) return true; for(int i=0;i<=cover;i++){ cover=max(i+nums[i],cover); if(cover>=nums.size()-1){ return true; } } return false; } };
-
不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。
题目:跳跃游戏 II
-
给定一个长度为
n
的 0 索引整数数组nums
。初始位置为nums[0]
。每个元素nums[i]
表示从索引i
向前跳转的最大长度。换句话说,如果你在nums[i]
处,你可以跳转到任意nums[i + j]
处:0 <= j <= nums[i] ;i + j < n。返回到达nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达nums[n - 1]
。 -
贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。**真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!**如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。
-
移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。
-
如果当前覆盖最远距离下标不是集合终点,步数就加一,还需要继续走。如果当前覆盖最远距离下标就是集合终点,步数不用加一,因为不能再往后走了。
-
class Solution { public: int jump(vector<int>& nums) { if(nums.size()<=1) return 0; int max_len=0;// 当前覆盖的最大距离 int count = 0; int next_len=0;// 下一步覆盖最远距离下标 for(int i=0;i<nums.size();i++){ next_len=max(nums[i]+i,next_len);// 更新下一步覆盖最远距离下标 if(i==max_len){ // 遇到当前覆盖最远距离下标 count++; max_len=next_len; if(next_len>=nums.size()-1) break; } } return count; } };
-
时间复杂度: O(n);空间复杂度: O(1)。理解本题的关键在于:以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点,这个范围内最少步数一定可以跳到,不用管具体是怎么跳的,不纠结于一步究竟跳一个单位还是两个单位。
题目:K 次取反后最大化的数组和
-
给你一个整数数组
nums
和一个整数k
,按以下方法修改该数组:选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。重复这个过程恰好k
次。可以多次选择同一个下标i
。以这种方式修改数组后,返回数组 可能的最大和 。 -
贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
-
那么本题的解题步骤为:
- 第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
- 第二步:从前向后遍历,遇到负数将其变为正数,同时K–
- 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
- 第四步:求和
-
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(k>0 && nums[i]<0){ nums[i] = -nums[i]; k--; } } if(k%2==1) nums[nums.size()-1] = -nums[nums.size()-1]; int res=0; for(int item:nums){ res+=item; } return res; } };
题目:加油站
-
在一条环路上有
n
个加油站,其中第i
个加油站有汽油gas[i]
升。你有一辆油箱容量无限的的汽车,从第i
个加油站开往第i+1
个加油站需要消耗汽油cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。给定两个整数数组gas
和cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回-1
。如果存在解,则 保证 它是 唯一 的。 -
暴力的方法很明显就是 O ( n 2 ) O(n^2) O(n2) 的,遍历每一个加油站为起点的情况,模拟一圈。如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while。
-
class Solution { public: int canCompleteCircuit(vector<int>& gas, vector<int>& cost) { for(int i=0;i<cost.size();i++){ int rest = gas[i]-cost[i]; int index=(i+1)%cost.size(); while(rest>0&&index!=i){// 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了) rest+= gas[index]-cost[index]; index=(index+1)%cost.size(); } // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置 if(rest>=0 && index==i) return i; } return -1; } };//超时
-
直接从全局进行贪心选择,情况如下:
- 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
- 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
- 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
-
class Solution { public: int canCompleteCircuit(vector<int>& gas, vector<int>& cost) { int cursum=0; int min=INT_MAX;// 从起点出发,油箱里的油量最小值 for(int i=0;i<gas.size();i++){ int rest = gas[i]-cost[i]; cursum += rest; if(cursum<min){ min=cursum; } } if(cursum<0) return -1; if(min>=0) return 0; for(int i=gas.size()-1;i>=0;i--){ int rest=gas[i]-cost[i]; min += rest; if(min>=0){ return i; } } return -1; } };
-
首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。
-
那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。
-
class Solution { public: int canCompleteCircuit(vector<int>& gas, vector<int>& cost) { int cursum=0; int tosum =0; int start=0; for(int i=0;i<gas.size();i++){ cursum+=gas[i]-cost[i]; tosum+=gas[i]-cost[i]; if(cursum<0){ // 当前累加rest[i]和 curSum一旦小于0 start=i+1;// 起始位置更新为i+1 cursum=0;// curSum从0开始 } } if(tosum<0) return -1; return start; } };
-
时间复杂度:O(n);空间复杂度:O(1)
题目:分发糖果
-
n
个孩子站成一排。给你一个整数数组ratings
表示每个孩子的评分。你需要按照以下要求,给这些孩子分发糖果:每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获得更多的糖果。请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。 -
这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
-
class Solution { public: int candy(vector<int>& ratings) { vector<int> res_vec(ratings.size(),1); for(int i=1;i<ratings.size();i++){ if(ratings[i]>ratings[i-1]){ res_vec[i] += res_vec[i-1]; } } for(int i=ratings.size()-2;i>=0;i--){ if(ratings[i]>ratings[i+1]){ res_vec[i] = max(res_vec[i+1]+1,res_vec[i]); } } int res=0; for(int res_one:res_vec){ res+=res_one; } return res; } };
-
时间复杂度: O(n);空间复杂度: O(n)
-
那么本题我采用了两次贪心的策略:一次是从左到右遍历,只比较右边孩子评分比左边大的情况。一次是从右到左遍历,只比较左边孩子评分比右边大的情况。这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。
题目:柠檬水找零
-
在柠檬水摊上,每一杯柠檬水的售价为
5
美元。顾客排队购买你的产品,(按账单bills
支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付5
美元、10
美元或20
美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付5
美元。注意,一开始你手头没有任何零钱。给你一个整数数组bills
,其中bills[i]
是第i
位顾客付的账。如果你能给每位顾客正确找零,返回true
,否则返回false
。 -
class Solution { public: bool lemonadeChange(vector<int>& bills) { int count5=0,count10=0; for(int i=0;i<bills.size();i++){ if(bills[i]==5) count5++; if(bills[i]==10){ if(count5<=0) return false; count10++; count5--; } if(bills[i]==20){ if(count10>0&&count5>0){ count10--; count5--; }else if(count5>=3){ count5-=3; }else return false; } } return true; } };
-
只需要维护三种金额的数量,5,10和20。有如下三种情况:
- 情况一:账单是5,直接收下。
- 情况二:账单是10,消耗一个5,增加一个10
- 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
-
所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
题目:根据身高重建队列
-
假设有打乱顺序的一群人站成一个队列,数组
people
表示队列中一些人的属性(不一定按顺序)。每个people[i] = [hi, ki]
表示第i
个人的身高为hi
,前面 正好 有ki
个身高大于或等于hi
的人。 -
请你重新构造并返回输入数组
people
所表示的队列。返回的队列应该格式化为数组queue
,其中queue[j] = [hj, kj]
是队列中第j
个人的属性(queue[0]
是排在队列前面的人)。 -
如果两个维度一起考虑一定会顾此失彼。如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!
-
按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。
-
所以在按照身高从大到小排序后:局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性;全局最优:最后都做完插入操作,整个队列满足题目队列属性
-
class Solution { public: static bool cmp(const vector<int>& a,const vector<int>& b){ if(a[0]==b[0]) return a[1]<b[1]; return a[0]>b[0]; } vector<vector<int>> reconstructQueue(vector<vector<int>>& people) { sort(people.begin(),people.end(),cmp); vector<vector<int>> res; for(int i=0;i<people.size();i++){ int position = people[i][1]; res.insert(res.begin()+position,people[i]); } return res; } };
vector& b){
if(a[0]==b[0]) return a[1]<b[1];
return a[0]>b[0];
}
vector<vector> reconstructQueue(vector<vector>& people) {
sort(people.begin(),people.end(),cmp);
vector<vector> res;
for(int i=0;i<people.size();i++){
int position = people[i][1];
res.insert(res.begin()+position,people[i]);
}
return res;
}
};