贪心的本质是选择每一阶段的局部最优,从而达到全局最优解。
贪心算法一般分为如下四步:
1、将问题分解为若干个子问题
2、找出适合的贪心策略
3、求解每一个子问题的最优解
4、将局部最优解堆叠成全局最优解
455.分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
- 输入: g = [1,2,3], s = [1,1]
- 输出: 1 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以你应该输出 1。
思路:每次用最大的饼干去满足胃口比较大的孩子 ,这样才能尽量多地满足孩子的需求。因此将两个数组进行排序,然后必须遍历孩子的胃口,用一个指针指向饼干,一旦能够满足当前孩子的胃口,就进行更新ans。
int cmp(int *a,int* b){
return *a-*b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize) {
int ans=0;
qsort(g,gSize,sizeof(int),cmp);
qsort(s,sSize,sizeof(int),cmp);
int idx=sSize-1;
//遍历孩子的胃口值
for(int i=gSize-1;i>=0;--i){
if(idx>=0&&g[i]<=s[idx]){
//饼干能够满足孩子胃口
ans++;
idx--;
}
}
return ans;
}
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
- 输入: [1,7,4,9,2,5]
- 输出: 6
- 解释: 整个序列均为摆动序列。
本题异常情况的本质,就是要考虑平坡, 平坡分两种,一个是 上下中间有平坡,一个是单调有平坡。
int wiggleMaxLength(int* nums, int numsSize){
if(numsSize<2) return numsSize;
int predef=0;//前一对差值
int curdef=0;//当前差值
int ans=1;//记录峰值个数,序列默认序列最右边有一个峰值
for(int i=0;i<numsSize-1;++i){
curdef=nums[i+1]-nums[i];
if((predef>=0&&curdef<0)||(predef<=0&&curdef>0)){
ans++;
predef=curdef;//只有当出现摆动时才会更新predef
}
}
return ans;
}
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
- 输入: [-2,1,-3,4,-1,2,1,-5,4]
- 输出: 6
- 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
贪心的思路为局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。从而推出全局最优:选取最大“连续和” 。
int maxSubArray(int* nums, int numsSize) {
int mmax=INT_MIN;
int cur=0;//存放当前结果
for(int i=0;i<numsSize;++i){
cur+=nums[i];
//当局部结果大于mmax时更新结果
if(cur>mmax){
mmax=cur;
}
//当局部结果为负时,对最终结果无益,重新开始计算
if(cur<0){
cur=0;
}
}
return mmax;
}
动态规划:用dp[i]表示[0,i]的连续子序列之和的最大值。
int maxSubArray(int* nums, int numsSize) {
if(numsSize==0) return 0;
int dp[numsSize];//表示i之前的最大连续子数组
dp[0]=nums[0];
int ans=dp[0];
for(int i=1;i<numsSize;++i){
dp[i]=fmax(dp[i-1]+nums[i],nums[i]);//状态转移
if(dp[i]>ans) ans=dp[i];
}
return ans;
}
122.买卖股票的最佳时机 II
给你一个整数数组
prices
,其中prices[i]
表示某支股票第i
天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4] 输出:7
贪心思想:检查相邻的两个价格之间是否能够得到利润,如果不能得到利润就不加上差值,这样就能得到最大利润。局部最优:只收集每天的正利润,全局最优:得到最大利润。
int maxProfit(int* prices, int pricesSize) {
int ans=0;
for(int i=1;i<pricesSize;++i){
int cur=prices[i]-prices[i-1];
if(cur>0) ans+=cur;
}
return ans;
}
动态规划:dp[i][2]其中0表示买入,1表示卖出,状态转移需要考虑买与不买两种情况。
int maxProfit(int* prices, int pricesSize) {
int n=pricesSize;
int dp[n][2];
dp[0][0]=0;
dp[0][1]=-prices[0];
for(int i=1;i<n;++i){
dp[i][0]=fmax(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1]=fmax(dp[i-1][1],dp[i-1][0]-prices[i]);
}
return dp[n-1][0];
}
55. 跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
- 输入: [2,3,1,1,4]
- 输出: true
- 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
本题贪心的关键是:不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。
bool canJump(int* nums, int numsSize) {
int cover=0;
for(int i=0;i<=cover;++i){//i的值不能超过cover
cover=fmax(i+nums[i],cover);
if(cover>=numsSize-1) return true;
}
return false;
}
45.跳跃游戏 II
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
- 输入: [2,3,1,1,4]
- 输出: 2
- 解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点, 要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数。
那么局部最优:求当前这步的最大覆盖,那么尽可能多走,到达覆盖范围的终点,只需要一步。整体最优:达到终点,步数最少。这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
int jump(int* nums, int numsSize) {
if(numsSize==1) return 0;
int ans=0;
int curDis=0;//当前最远距离的下标
int nextDis=0;//下一步最远距离的下标
for(int i=0;i<numsSize;++i){
nextDis=fmax(i+nums[i],nextDis);
if(i==curDis){//遇到当前覆盖最远距离下标
ans++;
curDis=nextDis;
if(nextDis>=numsSize-1) break;
}
}
return ans;
}
1005.K次取反后最大化的数组和
给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)
以这种方式修改数组后,返回数组可能的最大和。
示例 1:
- 输入:A = [4,2,3], K = 1
- 输出:5
- 解释:选择索引 (1,) ,然后 A 变为 [4,-2,3]。
第一次贪心:局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。处理之后,如果K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
第二次贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和达到最大。
int cmp(int* a,int* b){
return abs(*b)-abs(*a);
}
int largestSumAfterKNegations(int* nums, int numsSize, int k) {
//注意排序的顺序,要按照绝对值的顺序从大到小排序
qsort(nums,numsSize,sizeof(int),cmp);
//每次从绝对值最大的值开始逆转
for(int i=0;i<numsSize;++i){
if(nums[i]<0&&k>0){
nums[i]=nums[i]*(-1);
k--;
}
}
//若遍历完数组后k还有剩余(此时所有元素应均为正),并且k为奇数时,则将绝对值最小的元素nums[numsSize-1]变为负
if(k%2==1){
nums[numsSize-1]*=-1;
}
int sum=0;
for(int i=0;i<numsSize;++i){
sum+=nums[i];
}
return sum;
}
134. 加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
示例 1: 输入:
- gas = [1,2,3,4,5]
- cost = [3,4,5,1,2]
暴力法:枚举起始节点,然后模拟跑一圈的过程,检查是否能够回到起始节点。此法超时。
int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
for(int i=0;i<gasSize;++i){
int rest=gas[i]-cost[i];
int index=(i+1)%costSize;
while(rest>0&&index!=i){
rest=rest-cost[index]+gas[index];
index=(index+1)%costSize;
}
if(rest>=0&&index==i) return index;
}
return -1;
}
贪心算法:那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。
int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
int cursum=0;
int totalsum=0;
int start=0;
for(int i=0;i<gasSize;++i){
cursum+=gas[i]-cost[i];
totalsum+=gas[i]-cost[i];
if (cursum<0) { // 当前累加rest[i]和 curSum一旦小于0
start=i+1; // 起始位置更新为i+1
cursum=0; // curSum从0开始
}
}
if(totalsum<0) return -1;
return start;
}
135. 分发糖果
n
个孩子站成一排。给你一个整数数组ratings
表示每个孩子的评分。你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到
1
个糖果。- 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
输入:ratings = [1,0,2] 输出:5
先确定右边评分大于左边的情况(也就是从前向后遍历)
此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果。
再确定左孩子大于右孩子的情况(从后向前遍历)
局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多。
int candy(int* ratings, int ratingsSize) {
int candysize[ratingsSize];
for(int i=0;i<ratingsSize;++i){
candysize[i]=1;
}
//检查右边是否比左边高
for(int i=1;i<ratingsSize;++i){
if(ratings[i]>ratings[i-1]){
candysize[i]=candysize[i-1]+1;
}
}
//检查左边是否比右边高
for(int i=ratingsSize-2;i>=0;--i){
if(ratings[i]>ratings[i+1]){
candysize[i]=fmax(candysize[i+1]+1,candysize[i]);
}
}
int res=0;
for(int i=0;i<ratingsSize;++i){
res+=candysize[i];
}
return res;
}
860.柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
- 输入:[5,5,5,10,20]
- 输出:true
局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
bool lemonadeChange(int* bills, int billsSize) {
int five=0,ten=0,twenty=0;
for(int i=0;i<billsSize;++i){
if(bills[i]==5){
five++;
}else if(bills[i]==10){
ten++;
if(five>0){
five--;
}else{
return false;
}
}else{
twenty++;
if(ten>0&&five>0){
ten--;
five--;
}else if(five>=3){
five-=3;
}else{
return false;
}
}
}
return true;
}
406.根据身高重建队列
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
- 输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
- 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
所以在按照身高从大到小排序后:
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
class Solution {
public:
// 身高从大到小排(身高相同k小的站前面)
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);
list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
for (int i = 0; i < people.size(); i++) {
int position = people[i][1]; // 插入到下标为position的位置
std::list<vector<int>>::iterator it = que.begin();
while (position--) { // 寻找在插入位置
it++;
}
que.insert(it, people[i]);
}
return vector<vector<int>>(que.begin(), que.end());
}
};
452. 用最少数量的箭引爆气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
示例 1:
- 输入:points = [[10,16],[2,8],[1,6],[7,12]]
- 输出:2
class Solution {
private:
static bool cmp(const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
}
public:
int findMinArrowShots(vector<vector<int>>& points) {
if (points.size() == 0) return 0;
sort(points.begin(), points.end(), cmp);
int result = 1; // points 不为空至少需要一支箭
for (int i = 1; i < points.size(); i++) {
if (points[i][0] > points[i-1][1]) { // 气球i和气球i-1不挨着,注意这里不是>=
result++; // 需要一支箭
}
else { // 气球i和气球i-1挨着
points[i][1] = min(points[i-1][1], points[i][1]); // 更新重叠气球最小右边界
}
}
return result;
}
};
435. 无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
- 输入: [ [1,2], [2,3], [3,4], [1,3] ]
- 输出: 1
- 解释: 移除 [1,3] 后,剩下的区间没有重叠。
class Solution {
public:
// 按照区间右边界排序
static bool cmp (const vector<int>& a, const vector<int>& b) {
return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);
int count = 1; // 记录非交叉区间的个数
int end = intervals[0][1]; // 记录区间分割点
for (int i = 1; i < intervals.size(); i++) {
if (end <= intervals[i][0]) {
end = intervals[i][1];
count++;
}
}
return intervals.size() - count;
}
};
763.划分字母区间
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
示例:
- 输入:S = "ababcbacadefegdehijhklij"
- 输出:[9,7,8] 解释: 划分结果为 "ababcbaca", "defegde", "hijhklij"。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。
class Solution {
public:
vector<int> partitionLabels(string s) {
int hash[27]={0};
int n=s.size();
//记录每个字母最后一次出现的位置
for(int i=0;i<n;++i){
hash[s[i]-'a']=i;
}
int left=0;
int right=0;
vector<int> res;
for(int i=0;i<n;++i){
right=max(right,hash[s[i]-'a']);//找到每个字母最后的一个位置
if(i==right){
res.push_back(right-left+1);
left=i+1;
}
}
return res;
}
};
56. 合并区间
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
- 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
- 输出: [[1,6],[8,10],[15,18]]
- 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> result;
if (intervals.size() == 0) return result; // 区间集合为空直接返回
// 排序的参数使用了lambda表达式
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){return a[0] < b[0];});
// 第一个区间就可以放进结果集里,后面如果重叠,在result上直接合并
result.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
if (result.back()[1] >= intervals[i][0]) { // 发现重叠区间
// 合并区间,只更新右边界就好,因为result.back()的左边界一定是最小值,因为我们按照左边界排序的
result.back()[1] = max(result.back()[1], intervals[i][1]);
} else {
result.push_back(intervals[i]); // 区间不重叠
}
}
return result;
}
};
738.单调递增的数字
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
- 输入: N = 10
- 输出: 9
- 输入: N = 1234
- 输出: 1234
暴力解:从大到小遍历判断
bool check(int n){
int mmax=10;
while(n){
int cur=n%10;
if(mmax>=cur) mmax=cur;
else return false;
n/=10;
}
return true;
}
int monotoneIncreasingDigits(int n) {
for(int i=n;i>=0;--i){
if(check(i)){
return i;
}
}
return -1;
}
例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。
想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。
最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。
int monotoneIncreasingDigits(int n) {
char s[12];
sprintf(s,"%d",n);
int l=strlen(s);
// flag用来标记赋值9从哪里开始
// 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
int flag=l;
for(int i=l-1;i>0;--i){
//找到第一个非递增的下标,并
if(s[i-1]>s[i]){
flag=i;
s[i-1]--;
}
}
for(int i=flag;i<l;++i){
s[i]='9';
}
return atoi(s);
}