Leetcode贪心算法合集

写在前面

本文的题目选取以及部分思路来自于高畅的《Leetcode101》与力扣题解中的用户思路与解法,仅供自己学习记录~

贪心算法

贪心算法的思想是保证每次操作都是局部最优,从而使最后的结果是全局最优的。

分配问题

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。

示例2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2。

思路:
因为饥饿度最小的孩子最容易吃饱,所以我们先考虑这个孩子,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。

代码:

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        //分发饼干
        sort(g.begin(),g.end());//对小孩胃口值进行从小到大的排序
        sort(s.begin(),s.end());//对饼干尺寸值进行从小到大的排序
        int i = 0, j = 0;//分别指向小孩胃口值和饼干的尺寸
        while(i < g.size() && j < s.size())//限定不超出范围
        {
            if(g[i] <= s[j]) ++i;//如果小孩胃口值小于或者等于饼干尺寸,则转到下一个小孩,否则仍停留在这个小孩
            ++j;//无论饼干尺寸是否能满足当前小孩,都需要指向下一个饼干进行判断(饼干尺寸无法满足当前小孩时,证明后面胃口更大的小孩更加无法满足,这个饼干无法被任何小孩吃)
        }
        return i;//得到最多可满足的小孩数量
    }
};

135.分发糖果

n个孩子站成一排。给你一个整数数组ratings表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

每个孩子至少分配到 1个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

示例1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。

示例2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。

思路:
两次遍历:首先把所有孩子的糖果数初始化为 1;

  1. 从左往右:如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;
  2. 从右往左:如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。

代码:

class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        if (n < 2) return n;//特殊情况
        vector<int> c(n,1);//长度为n的容器,初始化均为1
        for (int i = 0; i < n - 1;i++)//从左往右遍历
        {
            if (ratings[i] < ratings[i+1]) c[i+1] = c[i] + 1;
        }
        for (int j = n - 1;j > 0;j--)
        {
            if (ratings[j-1] > ratings[j] && c[j-1] <= c[j]) c[j-1] = c[j] + 1;
        }
        return accumulate(c.begin(),c.end(),0);//accumulate函数包含在#include<numeric> 头文件下,其中有三个参数,前两个参数是累加元素的范围,第三个参数是累加的初值。
    }
};

区间问题

435.无重叠区间

给定一个区间的集合intervals,其中intervals[i] = [starti, endi]。返回需要移除区间的最小数量,使剩余区间互不重叠 。

示例1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。这里是引用

示例2:
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:
输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

思路:
求最少的移除区间个数,等价于尽量多保留不重叠的区间。在选择要保留区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。因此,我们采取的贪心策略为,优先保留结尾小且不相交的区间。具体实现方法为,先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。

代码:

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.empty())
        {
            return 0;
        }//特殊情况
        int n = intervals.size();
        int cnt = 0;
        sort(intervals.begin(),intervals.end(),[](vector<int>& a,vector<int>& b)
        {
            return a[1] < b[1];
        });//将数组中每个区间的结尾进行递增排序
        int prev = intervals[0][1];
        for (int i = 1;i < n;i++)
        {
            if(intervals[i][0] < prev)//如果下一个区间的开头小于上一个区间的结尾,证明两区间重叠,需要移除该区间
            {
                ++cnt;
            }
            else{//否则将当前区间的结尾赋值给prev
                prev = intervals[i][1];
            }
        }
        return cnt;
    }
};

练习

605.种花问题

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给你一个整数数组 flowerbed表示花坛,由若干01组成,其中0表示没种植花,1表示种植了花。另有一个数n,能否在不打破种植规则的情况下种入n朵花?能则返回true,不能则返回false

示例 1:
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true

示例 2:
输入:flowerbed = [1,0,0,0,1], n = 2
输出:false

思路:
从左往右遍历,能种花的条件是当前位置是0 && 前一位置是0或者边界 && 后一位置是0或者边界。

代码:

class Solution {
public:
    bool canPlaceFlowers(vector<int>& flowerbed, int n) {
        int size = flowerbed.size();
        for (int i = 0;i < size;i++)
        {
            if(flowerbed[i] == 0 && (i == 0 || flowerbed[i-1] == 0) && (i == size-1 || flowerbed[i+1] == 0))
            {
                n--;
                flowerbed[i] = 1;
            }            
        }
        return n <= 0;
    }
};

452.用最少数量的箭引爆气球

有一些球形气球贴在一堵用XY平面表示的墙面上。墙面上的气球记录在整数数组 points,其中points[i] = [xstart, xend]表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆 。可以射出的弓箭的数量没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组points,返回引爆所有气球所必须射出的最小弓箭数 。

示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
在x = 6处射出箭,击破气球[2,8]和[1,6]。
在x = 11处发射箭,击破气球[10,16]和[7,12]。

示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。

示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
在x = 2处发射箭,击破气球[1,2]和[2,3]。
在x = 4处射出箭,击破气球[3,4]和[4,5]。

思路:
与435类似。

代码:

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if(points.empty()) return 0;
        int n = points.size();
        sort(points.begin(),points.end(),[](vector<int>& a,vector<int>& b)
        {
            return a[1] < b[1];
        });//按照气球的结束坐标排序
        int res = 1;
        int pre = points[0][1];
        for (int i = 1;i < n;i++)
        {
            if (points[i][0] > pre)不相交的条件:后一个气球的开始坐标大于(不能等于)前一个气球的结束坐标
            {
                res++;
                pre = points[i][1];
            }
        }
        return res;
    }
};

在排序时需要注意的是:

sort(points.begin(),points.end(),[](vector<int> a,vector<int> b){
        return a[1] < b[1];//传值,开销太大

sort(points.begin(),points.end(),[](const vector<int> &a,const vector<int> &b){
        return a[1] < b[1];//传引用

sort(points.begin(),points.end(),[](vector<int> &a,vector<int> &b){
        return a[1] < b[1];//传引用

763.划分字母区间

字符串S由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:
输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。

思路:
参考代码随想录的解题思路,贪心思路为:

  1. 统计每一个字符最后出现的位置;
  2. 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点。
    图解

代码:

class Solution {
public:
    vector<int> partitionLabels(string s) {
        //统计每一个字符最后出现的位置
        int hash[27] = {0};
        for (int i = 0;i < s.size();i++){
            hash[s[i] - 'a'] = i;
        }
        vector<int> result;
        //从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等,则找到了分割点
        int left = 0;
        int right = 0;
        for(int i = 0;i < s.size();i++){
            right = max(right,hash[s[i] - 'a']);
            if(i == right){
                result.push_back(right - left + 1);
                left = i + 1;
            }
        }
        return result;
    }
};

121. 买卖股票的最佳时机

给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i天的价格。

你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0

示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

思路:
保持在最便宜的时候买入,在最贵的时候卖出,即可获得最大的利润。

代码:
提供一种会超时的暴力解法:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //两两遍历的方法
        if (prices.size() == 1) return 0;
        int Max = 0;
        for (int i = 0;i < prices.size() - 1;i++)
        {
            int temp = prices[i];
            for (int j = i + 1;j < prices.size();j++)
            {
                if (prices[j] < temp) continue;
                Max = max(prices[j] - temp,Max);
            }
        }
        return Max;
    }
};

贪心算法:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int Max = 0;
        int cur = prices[0];//指针cur遇到比它小的元素替换,遇到比它大的元素进行一次利润的计算,保存最大的利润
        for(int i = 1;i < prices.size();i++)
        {
            if (prices[i] < cur){
                cur = prices[i];
                continue;
            }
            else if (prices[i] > cur){
                Max = max(Max,prices[i] - cur);
            }
        }
        return Max;
    }
};

122.买卖股票的最佳时机 II

给你一个整数数组prices,其中prices[i]表示某支股票第i天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。

返回你能获得的最大利润 。

示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。

示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。

示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

思路:
需要注意的是,我们可以在同一天购买和/或出售股票,也就是一天之内可以先抛售股票,再购买股票(可能与实际购买抛售情况不符),因此我们只需要找到所有的“上坡”即可获得最大的利润。

代码:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //不限制交易次数的情况下 如何获得最大利润
        if(prices.size() == 1) return 0;
        //收集所有上坡
        int ans = 0;
        for (int i = 1;i < prices.size();i++)
        {
            if (prices[i] > prices[i-1])
            {
                ans += (prices[i] - prices[i-1]);
            }
        }
        return ans;
    }
};

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]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

这里是引用
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

思路:
参照Sunny的题解思路,总体来说就是排序和插入的操作:

  1. 按照数对的元素0降序排序:对于每个元素,在其之前的元素都是大于当前元素的数;
  2. 按照数对的元素1升序排序:希望元素值相同时,K大的尽量在后面,保证插入时的正确性(防止后面的相同值插入到前面影响结果,因为值是按降序排序的)

代码:

class Solution {
public:
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(), people.end(), [](const vector<int>& u, const vector<int>& v) {
            return u[0] > v[0] || (u[0] == v[0] && u[1] < v[1]);//相同元素值,K值大的在后面
            });
        int length = people.size();
        vector<vector<int> > ans;
        for (int i = 0; i < length; ++i)
        {
            if (people[i][1] >= i)
                ans.push_back(people[i]);
            else
                ans.insert(ans.begin() + people[i][1], people[i]);//vec.insert(vec.begin()+i,a);在第i+1个元素前面插入a;
        }
        return ans;
    }
};

665. 非递减数列

给你一个长度为n的整数数组nums,请你判断在最多改变1个元素的情况下,该数组能否变成一个非递减数列。

我们是这样定义一个非递减数列的: 对于数组中任意的i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]

示例 1:
输入: nums = [4,2,3]
输出: true
解释: 你可以通过把第一个 4 变成 1 来使得它成为一个非递减数列。

示例 2:
输入: nums = [4,2,1]
输出: false
解释: 你不能在只改变一个元素的情况下将其变为非递减数列。

思路:
这道题极易出现一种错误:仅仅判断是否出现了一次下降,见下方错误代码。此种解法会在[3,4,2,3]处报错。因此需要仔细思考你的贪心策略在各种情况下,是否仍然是最优解。

问题: 在遍历比较数组中nums[i]nums[i-1]两个数的大小时,如果nums[i]<nums[i-1],无法判断是nums[i]变为nums[i-1]还是nums[i-1]变为nums[i]。因此需要引入第三个数。

  1. 如果此时nums[i]>=nums[i-2],则nums[i-1]=nums[i]nums[i]=nums[i-1]都可,但是为了“贪心”单调递增,我们希望前面的越小越好,所以nums[i-1]=nums[i]
  2. 如果此时nums[i]<nums[i-2],则nums[i]=nums[i-1]

[3,4,2,3]为例:当i=2nums[2]<nums[1],且nums[2]<nums[0],此时只能将nums[2]变为nums[1]即4(大于4也可,但是我们不知道后面数字的情况,所以尽量时当前值更小),此时数组变为[3,4,4,3]i=3,有3<4nums[3]也要变为nums[2]即4,成为非递减数列至少需要变换两次数字,因此返回false

代码:
错误代码:

class Solution {
public:
    bool checkPossibility(vector<int>& nums) {
        int n = nums.size();
        if (n <= 1) return true;
        int flag = 0;
        for(int i = 0;i < n-1;i++)
        {
            if(nums[i] > nums[i+1]) flag++;
        }
        return flag == 1 || flag == 0;
    }
};

贪心算法:

class Solution {
public:
    bool checkPossibility(vector<int>& nums) {
        int count = 0;
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] < nums[i - 1]) {
                if (i == 1 || nums[i] >= nums[i - 2]) {//需要注意i == 1的情况
                    nums[i - 1] = nums[i];
                } 
                else {
                    nums[i] = nums[i - 1];
                }
                count ++;//需要改变的元素数目
            }
        }
        return count <= 1;
    }
};

结束语

贪心算法的题目到此告一段落,后续如果有做新的题也会陆续补充~欢迎关注收藏哦

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值