LeetCode 101:第二章最易懂的贪心算法

2 最易懂的贪心算法

2.1 算法解释

  • 顾名思义,贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。举一个最简单的例子:小明和小王喜欢吃苹果,小明可以吃五个,小王可以吃三个。已知苹果园里有吃不完的苹果,求小明和小王一共最多吃多少个苹果。在这个例子中,我们可以选用的贪心策略为,每个人吃自己能吃的最多数量的苹果,这在每个人身上都是局部最优的。又因为全局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的策略。

2.2 分配问题

[455. 分发饼干]

题目描述

  • 有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃一个饼干,且只有饼干的大小不小于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子可以吃饱。

输入输出样例

  • 输入两个数组,分别代表孩子的饥饿度和饼干的大小。输出最多有多少孩子可以吃饱的数量。

Input: [1,2], [1,2,3] 前面数组是孩子,后面是饼干

Output: 2

在这个样例中,我们可以给两个孩子喂 [1,2]、[1,3]、[2,3] 这三种组合的任意一种。

题解

因为饥饿度最小的孩子最容易吃饱,所以我们先考虑这个孩子。为了尽量使得剩下的饼干可以满足饥饿度更大的孩子,所以我们应该把大于等于这个孩子饥饿度的、且大小最小的饼干给这个孩子。满足了这个孩子之后,我们采取同样的策略,考虑剩下孩子里饥饿度最小的孩子,直到没有满足条件的饼干存在。

简而言之,这里的贪心策略是,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干,以此类推。

至于具体实现,因为我们需要获得大小关系,一个便捷的方法就是把孩子和饼干分别排序。这样我们就可以从饥饿度最小的孩子和大小最小的饼干出发,计算有多少个对子可以满足条件。

注意

对数组或字符串排序是常见的操作,方便之后的大小比较。

注意

在之后的讲解中,若我们谈论的是对连续空间的变量进行操作,我们并不会明确区分数组和字符串,因为他们本质上都是在连续空间上的有序变量集合。一个字符串“abc”可以被看作一个数组 [‘a’,‘b’,‘c’]。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin() , g.end());
        sort(s.begin() , s.end());

        int childen = 0;
        int cookies = 0;
        
        while(cookies < s.size() && childen < g.size() )
        {
            if(s[cookies] >= g[childen]) childen++;
            cookies++;
        }
        return childen;
    }
};
[135. 分发糖果]

题目描述

一群孩子站成一排,每一个孩子有自己的评分。现在需要给这些孩子发糖果,规则是如果一个孩子的评分比自己身旁的一个孩子要高,那么这个孩子就必须得到比身旁孩子更多的糖果;所有孩子至少要有一个糖果。求解最少需要多少个糖果。

输入输出样例

输入是一个数组,表示孩子的评分。输出是最少糖果的数量。

Input: [1,0,2]

Output: 5

在这个样例中,最少的糖果分法是 [2,1,2]。

题解

做完了题目 455,你会不会认为存在比较关系的贪心策略一定需要排序或是选择?

虽然这一道题也是运用贪心策略,但我们只需要简单的两次遍历即可:把所有孩子的糖果数初始化为 1;先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。

通过这两次遍历,分配的糖果就可以满足题目要求了。这里的贪心策略即为,在每次遍历中,只考虑并更新相邻一侧的大小关系。在样例中,我们初始化糖果分配为 [1,1,1],第一次遍历更新后的结果为 [1,1,2],第二次遍历更新后的结果为 [2,1,2]

class Solution {
public:
    int candy(vector<int>& ratings) {
        int size = ratings.size();
        if(size == 1)return 1;

        vector<int> num(size , 1);
        for(int i = 1 ; i < size ; ++i)
        {
            if(ratings[i] > ratings[i - 1])
            {
                num[i] = num[i - 1] + 1;
            }
        }

        for(int i = size - 1 ; i > 0 ; --i)
        {
            if(ratings[i] < ratings[i - 1])
            {
                //和上次分配的糖果进行比较,取最大值
                num[i - 1] = max(num[i - 1] , num[i] + 1);
            }
        }

        // std::accumulate 可以很方便地求和
        return accumulate(num.begin() , num.end() , 0);
    }
};

2.3 区间问题

[435. 无重叠区间]

题目描述

  • 给定多个区间,计算让这些区间互不重叠所需要移除区间的最少个数。起止相连不算重叠。

输入输出样例

输入是一个数组,数组由多个长度固定为 2 的数组组成,表示区间的开始和结尾。输出一个整数,表示需要移除的区间数量。

Input: [[1,2], [2,4], [1,3]]

Output: 1

在这个样例中,我们可以移除区间 [1,3],使得剩余的区间 [[1,2], [2,4]] 互不重叠。

题解

在选择要保留区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。因此,我们采取的贪心策略为,优先保留结尾小且不相交的区间。

具体实现方法为,先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。我们这里使用 C++ 的 Lambda,结合 std::sort() 函数进行自定义排序。

在样例中,排序后的数组为 [[1,2], [1,3], [2,4]]。按照我们的贪心策略,首先初始化为区间[1,2];由于 [1,3] 与 [1,2] 相交,我们跳过该区间;由于 [2,4] 与 [1,2] 不相交,我们将其保留。因此最终保留的区间为 [[1,2], [2,4]]。

注意

需要根据实际情况判断按区间开头排序还是按区间结尾排序。

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.size() == 1)return 0;

        sort(intervals.begin() , intervals.end() , [](vector<int>& a , vector<int>& b){
            return a[1] < b[1]; //按照区间第二个数来排序,越小排的越前面
        });

        int pre = intervals[0][1] , total = 0 ;

        for(int i = 1 ; i < intervals.size() ; ++i)
        {
            //如果这两个有重叠,那么就略过这个元素往后继续找
            if(pre > intervals[i][0]){
                total++;
            }
            else{
                //没有重叠的话就要更换pre
                pre = intervals[i][1];
            }
        }
        return total;
    }
};

2.4 练习

[605. 种花问题]

题目描述

  • 假设有一个很长的花坛,一部分地块种植了花,另一部分却没有,花不能种植在相邻的地块上,给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。

示例 1:

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

题解

判断每个位置是否可以种花,需要当前节点,前置节点,后置节点同时为0,

另外有两个特殊节点,就是如果是第一个节点则没有前置节点(置为 0),如果是最后一个节点,那么后置节点(置为 0)。

class Solution {
public:
    bool canPlaceFlowers(vector<int>& flowerbed, int n) {

        for(int i = 0; i< flowerbed.size() && n > 0 ; ++i)
        {
            if(flowerbed[i] == 1)continue;

            //到达此处意味着,上一个点为0
            //这里找出这个点两旁的元素是否为0,如果都为0那么可以插入
            //这里考虑到两端,一个没有前驱,一个没有后继,所以用三元运算符
            int pre = (i == 0 ? 0 : flowerbed[i - 1]);
            int next = (i == flowerbed.size() - 1 ? 0 : flowerbed[i + 1]);
            if(pre == 0 && next ==0){
                n--;
                flowerbed[i] = 1;
            }
        }
        return n == 0;
    }
};
[452. 用最少数量的箭引爆气球]

题目描述

  • 一支弓箭可以沿着 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支箭。

题解:

先给数组排序,这样好找出不重叠的区间

找到所有不重叠空间的数量,就是要射出剑的个数。

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
		  //这里必须要用引用,不然会报超时
        sort(points.begin() , points.end() , [](vector<int>& v1 , vector<int>& v2){
            return v1[1] < v2[1];//区间第二个数小的排前面,是便于后面找是否有重叠
        });

        int cur = points[0][1];
        int count = 1;
        for(int i =1 ; i < points.size() ; ++i)
        {
            if(cur >= points[i][0])continue;

            //若气球没有重叠,则再加一只箭
            cur = points[i][1];
            count++;
        }
        return count;
    }
};
[763. 划分字母区间]

题目描述

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

示例:

输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]

解释:划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。

题解:

分析题意:我们要找的切片片段中的字符串都是互不相交的,需要先找到S字符串中每一个字符最后一次出现的位置,并把它一一保存下来,后面再找到的划分后尽可能多的最大不相交的子区间,然后返回区间长度即可。

初始化每一个片段开始下标为start,结束下标为end,对于每一个访问到的字母S(i),得到当前字母的最后一次出现的下标位置end(i),此时我们调用max(end , end(i))进行比较,这样就能保证每次划分的最大子区间是局部最优解了。

这个主要是贪心结合双指针

注意

为了满足你的贪心策略,是否需要一些预处理?

在处理数组前,统计一遍信息(如频率、个数、第一次出现位置、最后一次出现位置等)可以使题目难度大幅降低。

  • 这里主要是描述了第二个片段

class Solution {
public:
    vector<int> partitionLabels(string s) {
        vector<int> ans;
        vector<int> charIndex(26 , 0);

        for(int i = 0; i < s.size() ; ++i){
            charIndex[s[i] - 'a'] = i;
        }

        int start = 0, end = 0;
        for(int i = 0 ; i<s.size() ; ++i){
            //易错点:charIndex[s[i] - 'a']写成charIndex[0]
            //因为字母是有顺序的,这里应该按字母的顺序来
            end = max(charIndex[s[i] - 'a'] , end);
            // 若相等,则end这个下标是之前所有字母的最大下标
            // 则在这个位置可以把之前所有字母都隔离
            if(end == i){
                ans.push_back(end - start + 1);
                start = end + 1;
            }
        }
        return ans;
    }
};
[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 。

题解:

首先我们是站在已经知道股票的涨幅,去卖股票的
所以可以转化为,只要股票第二天涨价,我前一天就买,第二天就卖,这样一定是赚取最多的办法

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int total = 0;
        for(int i = 1 ; i < prices.size() ; i++){
            if(prices[i] > prices[i - 1])
            {
                total+=(prices[i] - prices[i - 1]);
            }
        }
        return total;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值