贪心算法是大厂笔试面试最常考算法之一,其算法思想大概为保证每一步的操作都是局部最优的,从而使得最后得到的结果是全局最优的。贪心问题没有固定模板,是一种算法思想,不过我想我们仍然可以通过刷题总结一些用贪心算法解题的套路。
区间问题总结:
最大不相交区间数量
https://www.acwing.com/problem/content/910/
问题叙述:给定N个区间,在数轴上选择若干区间,使得选中区间之间互相没有交集,输出可选取的区间最大数量
思路:将每个区间按右端点进行排序,遍历每个区间,如果当前区间的左端点大于上一个区间的右端点,则增加选中的区间数量,定义一个ed变量辅助端点的判断,新增区间后,将ed的值实时更新为当前区间右端点。
代码:
#include<bits/stdc++.h> using namespace std; const int N=1e5+10; int n; int res; struct Range{ int l,r; bool operator< (const Range &w)const{ return r<w.r; } }; Range arr[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=0;i<n;i++){ cin>>arr[i].l; cin>>arr[i].r; } sort(arr,arr+n); int ed=-2e9; for(int i=0;i<n;i++){ if(arr[i].l>ed){ res++; ed=arr[i].r; } } cout<<res<<endl; return 0; }
区间选点问题
https://www.acwing.com/problem/content/907/
题目叙述:给定N个区间,在数轴上选择尽可能少的点,使得每个区间内都至少包含一个选出的点
解析:这个与最大不相交区间数量问题思路相同,如果要找覆盖所有区间的点集合数量最小值,则直接求最大不相交区间数量即可
无重叠区间问题
题目叙述:给定若个区间,移除部分区间,使得最终的区间集合内无重叠区间,求需要移除区间的最小数量
思路:与前两题类似,可以直接求最大不相交子区间数量,然后用区间总数量减去最大不相交子区间数量,也可以换一种思路,将区间按左端点排序后,遍历每个区间,如果当前区间的左端点大于上一个区间的右端点,则此时存在交集,将移除区间的数量加一,同时,将当前区间的右端点更新为当前区间和上一个区间之间的最小值。
代码:
class Solution { public: int eraseOverlapIntervals(vector<vector<int>>& intervals) { sort(intervals.begin(),intervals.end(),[](const vector<int> &a,const vector<int> &b){ return a[0]<b[0]; }); int res=0; for(int i=1;i<intervals.size();i++){ if(intervals[i][0]<intervals[i-1][1]){ res++; intervals[i][1]=min(intervals[i][1],intervals[i-1][1]); } } return res; } };
合并区间问题
题目描述:给定若干区间,合并所有区间,返回一个覆盖所有区间的不重叠的区间数组。
思路:将区间按左端点排序,构建结果集并将第一个区间加入结果集,遍历区间,取出结果集中最后一个区间,若当前区间和结果集中最后一个区间重叠,更新结果集中最后一个区间的右边界,否则,将当前节点直接加入结果集。
代码:
class Solution { public: vector<vector<int>> merge(vector<vector<int>>& intervals) { sort(intervals.begin(),intervals.end(),[](const vector<int>&a,const vector<int> &b){ return a[0]<b[0]; }); vector<vector<int>> res; res.push_back(intervals[0]); for(int i=1;i<intervals.size();i++){ vector<int> &t=res.back(); if(intervals[i][0]<=t[1]){ t[1]=max(t[1],intervals[i][1]); } else { res.push_back(intervals[i]); } } return res; } };
区间分组问题
https://www.acwing.com/problem/content/908/
问题描述:
给定若干区间,将区间分成若干组,要求每组内部的区间两两之间没有交集,求分的最小组数
思路:将所有区间按左端点排序,遍历所有区间,对于每个区间,尝试放入已有的组中,如果当前区间的开始点大于要求该组放入的组中的最后一个区间的结束点,则能放入该组中,如果不能放入已有的组中,则新开一个组。我们引入小根堆维护每个组的最后一个区间的结束点。
代码:
#include<bits/stdc++.h> using namespace std; const int N=1e5+10; int n; struct Range{ int l,r; //将区间按左端点排序 bool operator< (const Range &w) const{ return l<w.l; } }; Range arr[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=0;i<n;i++) { cin>>arr[i].l; cin>>arr[i].r; } sort(arr,arr+n); //对于每个新区间,尝试放入已有的组 //要求该组的最后一个区间结束点 < 当前区间的开始点 //能放入,更新该组最后一个区间,不能放入,新开一个组 //小根堆,存储各组数据最后结束点 priority_queue<int,vector<int>,greater<int>> heap; for(int i=0;i<n;i++){ auto r=arr[i]; //最早结束的组的结束点>=当前区间的开始点 if(heap.empty() || heap.top()>=r.l){ heap.push(r.r); //新增组 } else { heap.pop(); //移除原来的堆顶 heap.push(r.r); } } cout<<heap.size()<<endl; return 0; }
线段最大重合问题
问题叙述:给出N个区间,每个区间给出左右端点,求同一位置最多重合多少条线段
思路:区间按左端点排序,用小根堆维护当前未结束的区间的右端点,遍历区间,当前区间的左端点大于等于小根堆中维护的最早结束的区间的右端点时,重新开一轮重构小根堆。
代码:
#include<bits/stdc++.h> using namespace std; const int N=1e4+10; int n; struct Range{ int l,r; }; Range arr[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=0;i<n;i++) { cin>>arr[i].l; cin>>arr[i].r; } //按左端点排序 sort(arr,arr+n,[](const Range &a,const Range &b){ return a.l<b.l; }); //小根堆记录当前未结束的线段的右端点 priority_queue<int,vector<int>,greater<int>> heap; int res=0; for(int i=0;i<n;i++){ int l=arr[i].l; int r=arr[i].r; while(!heap.empty() && heap.top()<=l){ heap.pop(); } heap.push(r); res=max(res,(int)heap.size()); } cout<<res<<endl; }
区间覆盖问题
https://www.acwing.com/problem/content/909/
问题叙述:给定N个区间以及一个区间[s,t],请选择尽量少的区间,将指定区间完全覆盖,输出所需的最少区间数。
解析:将所有区间按左端点排序,遍历区间,找到在能覆盖s的区间中右端点最大的区间。
代码:
#include<bits/stdc++.h> using namespace std; const int N=1e5+10; struct Range{ int l,r; }; int n,s,t; Range arr[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>s>>t; cin>>n; for(int i=0;i<n;i++){ cin>>arr[i].l; cin>>arr[i].r; } //按左端点排序 sort(arr,arr+n,[](const Range &a,const Range &b){ return a.l<b.l; }); int res=0; bool flag=false; for(int i=0;i<n;i++){ int j=i; int r=-2e9; while(j<n && arr[j].l<=s){ r=max(r,arr[j].r); j++; } if(r<s){ res=-1; break; } res++; if(r>=t){ flag=true; break; } s=r; i=j-1; } if(!flag) res=-1; cout<<res<<endl; return 0; }
几个简单的贪心思想应用:
合并果子
https://www.acwing.com/problem/content/150/
问题描述:有N种果子,每种果子对应一定的数目且每个果子重量为1,将两堆果子合并耗费的体力值为两堆果子的重量之和,要求最终将所有果子合并耗费的最小体力。
思路:每次选择合并当前重量最小的两堆,使得本次合并耗费的体力最小,局部最优推广到全局最优,使得最终整体耗费的体力值最小。我们用小顶堆来维护最小重量的集合。
代码:
#include<bits/stdc++.h> using namespace std; const int N=1050; int n; priority_queue<int,vector<int>,greater<int>> heap; int res; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=1;i<=n;i++){ int x; cin>>x; heap.push(x); } while(heap.size()>1){ int a=heap.top(); heap.pop(); int b=heap.top(); heap.pop(); int t=a+b; res+=t; heap.push(t); } cout<<res<<endl; return 0; }
排队打水
https://www.acwing.com/problem/content/description/915/
问题简述:有n个人排队打水,每个人对应一个打水时间t,请问按什么顺序排队打水才能使得所有人等待时间之和最小
思路:短作业优先,让打水时间短的先打水,这样,后面人整体等待的时间也是下降的。
代码:
#include<bits/stdc++.h> using namespace std; const int N=1e5+10; typedef long long ll; int n; int arr[N]; ll res; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=0;i<n;i++) cin>>arr[i]; sort(arr,arr+n); reverse(arr,arr+n); for(int i=0;i<n;i++){ res+=arr[i]*i; } cout<<res<<endl; return 0; }
货仓选址
https://www.acwing.com/problem/content/106/
问题叙述:有N家商店,现要在这条商店路上建一个货舱,使得货舱到各家商店的距离之和最小
思路:取两个商店的中点时到两个商店的距离最小,由此推广到全局最优,取整个数轴的中点
代码:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e6+10; int n; int arr[N]; ll res; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+1+n); int mid=arr[(n+1)/2]; for(int i=1;i<=n;i++){ res+=abs(arr[i]-mid); } cout<<res<<endl; return 0; }
耍杂技的牛
https://www.acwing.com/problem/content/127/
问题叙述:给出N头奶牛,每头牛都有自己的重量w和强壮程度s,一头牛的风险值=这头牛上面的所有牛的重量和-这头牛自己的身体强壮程度值,风险值越大,这头牛撑不住的可能性越高,要求确定一个排序,使得所有奶牛种风险值最大者尽可能小。
思路:按w[i]+s[i]从小到大排,得到的最大风险系数是最小的
代码:
#include<bits/stdc++.h> using namespace std; const int N=50050; struct cow{ int w,sum; }; int n; cow arr[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=0;i<n;i++){ int w,s; cin>>w>>s; arr[i].w=w; arr[i].sum=w+s; } sort(arr,arr+n,[](const cow &a,const cow &b){ return a.sum<b.sum; }); int res=-1e9; int sumd=0; for(int i=0;i<n;i++){ int w=arr[i].w; int s=arr[i].sum-arr[i].w; res=max(res,sumd-s); sumd+=w; } cout<<res<<endl; return 0; }
分发饼干
问题叙述:有一堆孩子,每个孩子对应一个胃口,同时有一个饼干数组,每个饼干对应一个尺寸,当尺寸大于等于孩子的胃口时才能满足孩子的胃口,且每个孩子只能分配一块饼干,求我们能满足的最多的孩子数量。
思路:将饼干数组和胃口数组进行从小到大的排序,我们需要先尽可能先用大饼干满足大胃口的孩子,这样能保证我们满足尽可能多的孩子。
代码:
class Solution { public: int findContentChildren(vector<int>& g, vector<int>& s) { sort(g.begin(),g.end()); sort(s.begin(),s.end()); int j=s.size()-1; //饼干尺寸 int res=0; for(int i=g.size()-1;i>=0;i--){ if(j>=0 && s[j]>=g[i]){ res++; j--; } } return res; } };
摆动序列
问题叙述:有一个连续的数组,如果连续数组的差为一正一负交替,则称为摆动序列。现有一个连续数组可能并非摆动序列,我们可以删除一部分元素,构造一个子数组构成摆动序列,求这个子数组的最长长度。
思路:遍历这个数组,每次遍历到这个元素判断当前元素与上一个元素的差值以及上一个元素与上上个元素的差值关系,如果满足一正一负,则将摆动序列长度加一并更新记录的差值信息。
代码:
class Solution { public: int wiggleMaxLength(vector<int>& nums) { int pre=0; int cur=0; int res=1; for(int i=1;i<nums.size();i++){ cur=nums[i]-nums[i-1]; if((cur>0 && pre<=0)||(cur<0 && pre>=0)){ res++; pre=cur; } } return res; } };
最大子数组和
问题叙述:给定一个整数数组,找出一个具有最大和的连续子数组,返回最大和
思路:遍历数组并每次累计元素,用sum记录累加和,如果当前记录的累加和大于记录的最大和,则将最大和更新为当前累加和,如果记录的累加和为负数,则将当前累加和清0重新计算累加和
代码:
class Solution { public: int maxSubArray(vector<int>& nums) { int res=-0x3f3f3f3f; int sum=0; for(int i=0;i<nums.size();i++){ sum+=nums[i]; if(sum>res) res=sum; if(sum<0) sum=0; } return res; } };
买卖股票最佳时机2-贪心解法
122. 买卖股票的最佳时机 II - 力扣(LeetCode)
问题叙述:有一个价格数组,每个元素表示某只股票第i天的价格,在每一天,你可以决定买入和卖出股票,同一时间你只能持有一支股票,你可以在同一天多次买卖股票,得到你能获得的最大利润。
思路:可以使用DP或贪心解题,我们这里基于贪心思想去解。把问题转化为我们要赚到所有上涨段的差价利润,拆分成连续的当天买当天卖,只要次日价格高于当日,就赚这个差价,如果次日股价更低了,则不做这个交易。
代码:
class Solution { public: int maxProfit(vector<int>& prices) { int res=0; for(int i=1;i<prices.size();i++){ res+=max(prices[i]-prices[i-1],0); } return res; } };
跳跃游戏
题目叙述:给一个正数数组,数组中每个元素代表你在该位置可跳跃的最大长度,起始你位于第一个元素,判断你是否能到达最后一个下标
思路:定义遍历当前能覆盖的最远距离下标,遍历元素更新当前能覆盖的最远距离下标,如果已经过了数组长度,则一定可以覆盖到最后一个下标。
代码:
class Solution { public: bool canJump(vector<int>& nums) { int cover=0; //当前能覆盖的最远距离 for(int i=0;i<=cover;i++){ cover=max(cover,i+nums[i]); if(cover>=nums.size()-1) return true; } return false; } };
跳跃游戏2
题目叙述:给一个正数数组,数组中每个元素代表你在该位置可跳跃的最大长度,起始你位于第一个元素,返回到达数组末尾位置的最小跳跃次数
思路:核心思想就是每次都选择能到达的最远位置,维护双指针cur(在当前圈内能跳到的最远距离),next(在当前圈内,能向外跳到的最远距离),遍历数组维护这两个指针向后迭代跳跃。
代码:
class Solution { public: int jump(vector<int>& nums) { if(nums.size()==1) return 0; int cur=0;//在当前圈内能跳到的最远距离 int next=0;//在当前圈内,能向外跳到的最远距离 int res=0;//跳的次数 for(int i=0;i<nums.size();i++){ //在当前范围内,寻找能跳到圈外的最优解 next=max(next,nums[i]+i); //走到圈边界必须进行一次跳跃 if(i==cur){ res++; cur=next; if(next>=nums.size()-1) break; } } return res; } };
K次取反后最大化的数组和
1005. K 次取反后最大化的数组和 - 力扣(LeetCode)
题目叙述:给定一个整数数组和一个数字K,进行K次数组元素取反操作,每次取反操作选中一个元素且每个元素可以被选中多次,求以这种方式修改数组后,数组的最大和。
思路:将元素按绝对值从大到小排序,根据排序顺序遍历元素,优先将排在前面的负数进行取反操作,如果负数都取完了K还没耗尽,K此时为偶数则不进行操作,如果为奇数则取末尾最小的元素(即数组中最小正数)进行一次取反操作,这样得到的最终数组和就是最大的。
代码:
class Solution { public: int largestSumAfterKNegations(vector<int>& nums, int k) { sort(nums.begin(),nums.end(),[](const int &a,const int &b){ return abs(a)>abs(b); }); int res=0; for(int i=0;i<nums.size();i++){ if(k>0 && nums[i]<0){ nums[i]=-nums[i]; k--; } } if(k%2!=0) nums[nums.size()-1]*=-1; for(int i=0;i<nums.size();i++){ res+=nums[i]; } return res; } };
加油站
问题叙述:一条环路上有N个加油站,每个加油站对应一个汽油数量信息gas[i]升,一辆油箱无限的油车从第i个加油站开往第i+1个加油站需要消耗汽油cost[i]升。如果一辆初始油箱为空的油车从某个位置起始,可以按顺序环路一周,求这个起始位置的编号。
思路:维护两个可行性逻辑,一个是全局不可行性:所有加油站的总油量<总消耗量则一定无法完成循环。一个是局部不可行性,从起点到某个点i时,油箱油量为负数,则无法满足起点到i点。
代码:
class Solution { public: int canCompleteCircuit(vector<int>& gas, vector<int>& cost) { int sum=0; //总油量-耗油量 int cur=0;//当前从起点出发累计的 总油量-耗油量 int start=0; for(int i=0;i<gas.size();i++){ sum+=gas[i]-cost[i]; cur+=gas[i]-cost[i]; if(cur<0){ cur=0; start=i+1; } } if(sum<0)return -1; else return start; } };
分发糖果
问题叙述:n个孩子站成一排并且每个孩子对应一个评分,按如下两个需求给这些孩子分发糖果:1.每个孩子至少分到一个糖果。2.相邻的两个孩子中评分更高的孩子会获取更多糖果。请给每个孩子分发糖果,返回需要准备的最少糖果数。
思路:分阶段的局部最优考虑。第一轮从左到右遍历,只关注每个孩子是否比他左边的孩子评分高,如果更高则给他多分一块。同理,第二轮从右到左遍历,考虑每个孩子是否比他右边的孩子评分高。
代码:
class Solution { public: int candy(vector<int>& ratings) { int n=ratings.size(); vector<int> cnt(n,1); //每个孩子最少分到一块 for(int i=1;i<n;i++){ if(ratings[i]>ratings[i-1]) cnt[i]=cnt[i-1]+1; } for(int i=n-2;i>=0;i--){ if(ratings[i]>ratings[i+1]) cnt[i]=max(cnt[i],cnt[i+1]+1); } int res=0; for(int i=0;i<n;i++){ res+=cnt[i]; } return res; } };
柠檬水找零
问题简述:柠檬水摊上,每一杯柠檬水售价为5美分,顾客排队购买产品(按bill账单数组的顺序),每位顾客购买一杯柠檬水,然后向你付5,10或20美元,你必须给每个顾客正确找零,返回是否能给每位顾客正确找零。
思路:贪心思想,找零时优先选择用大面额的钱找零,以便于更好的满足于后续顾客的找零。
代码:
class Solution { public: bool lemonadeChange(vector<int>& bills) { int five=0,ten=0,twenty=0; for(int bill:bills){ if(bill==5){ five++; } else if(bill==10){ if(five>0){ five--; ten++; }else { return false; } } else if(bill==20){ if(ten>0 && five>0){ ten--; five--; twenty++; } else if(five>=3){ five-=3; twenty++; } else { return false; } } } return true; } };
根据身高重建队列
题目概述:有一个打乱顺序的二元组集合,每个元素两部分:某个人的身高,这个人前面身高大于等于他的人的数量。将这个乱序的集合重新排序,构成符合其含义的有序集合。
思路:将集合元素按身高降序排,身高相同再按照另一个属性升序排列。身高高的优先处理,将其往前排,身高相同的情况下,另一个元素越小,则越往前排。
代码:
class Solution { public: vector<vector<int>> reconstructQueue(vector<vector<int>>& people) { sort(people.begin(),people.end(),[](const vector<int> &a,const vector<int> &b){ if(a[0]==b[0]) return a[1]<b[1]; else return a[0]>b[0]; }); vector<vector<int>> res; for(vector<int> &p:people){ res.insert(res.begin()+p[1],p); } return res; } };
用最少的箭引爆气球
452. 用最少数量的箭引爆气球 - 力扣(LeetCode)
问题叙述:给定一堆气球并给出每个气球处于的横坐标区间,要求用最少数量的箭能覆盖穿透所有气球
思路:参考区间问题中的最大不相交子区间问题和区间覆盖问题。
代码:
class Solution { public: int findMinArrowShots(vector<vector<int>>& points) { sort(points.begin(),points.end(),[](const vector<int> &a,const vector<int> &b){ return a[1]<b[1]; }); int res=1; int ed=points[0][1]; for(int i=1;i<points.size();i++){ if(points[i][0]>ed){ res++; ed=points[i][1]; } } return res; } };
划分字母区间
题目叙述:现给出一个字符串,要将这个字符串分割成若干片段,要求分割后同一字母最多出现在一个片段中,求分割后每个字符串的长度的列表。
思路:哈希表+双指针解题,哈希表存每个字母最远的位置,利用双指针划分子区间,利用左右边界和哈希表实时更新右区间。
代码:
class Solution { public: vector<int> partitionLabels(string s) { vector<int> res; vector<int> hash(27,0); for(int i=0;i<s.size();i++) hash[s[i]-'a']=i; int l=0,r=0; for(int i=0;i<s.size();i++){ r=max(r,hash[s[i]-'a']); if(i==r){ res.push_back(r-l+1); l=i+1; } } return res; } };
单调递增的数字
题目简述:给定整数n,返回小于等于n的最大数字,且数字呈单调递增
思路:将数字转为字符串,从后往前遍历,如果当前数前一位大于后一位则将前一位减一并且标记当前位,然后将标记位开始的后续位置全取值9;
代码:
class Solution { public: int monotoneIncreasingDigits(int n) { string nums=to_string(n); int flag=nums.size(); for(int i=nums.size()-1;i>0;i--){ if(nums[i-1]>nums[i]){ nums[i-1]--; flag=i; } } for(int i=flag;i<nums.size();i++){ nums[i]='9'; } return stoi(nums); } };
监控二叉树
题目叙述:略
思路:
//节点的三种状态 0:无覆盖 1:有摄像头 2:有覆盖
//左右节点都有覆盖,则父节点为无覆盖
//左右节点至少一个无覆盖,则父节点放摄像头
//左右节点至少一个摄像头,则父节点为有覆盖
//处理完最终头节点无覆盖,给头节点加一个摄像头
代码:
//节点的三种状态 0:无覆盖 1:有摄像头 2:有覆盖 //左右节点都有覆盖,则父节点为无覆盖 //左右节点至少一个无覆盖,则父节点放摄像头 //左右节点至少一个摄像头,则父节点为有覆盖 //处理完最终头节点无覆盖,给头节点加一个摄像头 class Solution { public static int res; public int minCameraCover(TreeNode root) { res=0; if(cover(root)==0){ res++; } return res; } public static int cover(TreeNode cur){ if(cur==null) return 2; int left=cover(cur.left); int right=cover(cur.right); if(left==2 && right==2) return 0; if(left==0 || right==0) { res++; return 1; } if(left==1 || right==1) { return 2; } return -1; } }