14 贪心算法

1. 贪心算法解决问题的步骤

  • (1)适用范围:针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。
  • (2)尝试用贪心的思路解决问题:每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大的数据。
  • (3)举例看一下贪心算法产生的结果是否是最优:大部分情况下,举几个例子验证一下就可以了,严格证明贪心算法的,是非常复杂的,需要涉及到比较多的数学推理(实际上,用贪心算法解决问题的思路,并不总能给出最优解,如有权图中求最小路径问题,选择最短的边,可能会因为这一步选择最短的,导致后面每一步选择都很糟糕,最终就无法就出全局最优解,在这个问题上贪心算法不工作的原因是前面的选择会影响后面的选择)。

2. 贪心算法实战

  • (1)分饼干(leetcode455,easy)

  • 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
  • 对每个孩子 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(int[] g, int[] s) {
            //1.先将两个数组排序
            Arrays.sort(g);
            Arrays.sort(s);
            int i=0;int j=0;
            //2.贪心策略:按胃口值从小到大依次分配对应胃口值的孩子最小尺寸能满足胃口的饼干
            while(i<g.length&&j<s.length){
                if(g[i]<=s[j]){
                    i++;
                    j++;
                }else{
                    j++;
                }
            }
            return i;
        }
    }

(2)发糖果(leetcode135,hard)

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

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

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子中,评分更高的那个会获得更多的糖果。
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
     第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。

实现原理:遍历两遍,第一遍从左到右保证右侧评分高的孩子糖果数比左侧孩子多1,第二遍从右往左,保证左侧评分高的孩子糖果数比右侧孩子多1.

实现代码如下:

class Solution {
    public int candy(int[] ratings) {
        int[]candys = new int[ratings.length];
        //1. 所有孩子的糖果数初始化为1(每个孩子至少有一颗糖果)
        for(int i=0;i<candys.length;i++){
            candys[i]=1;
        }
        //2. 从左往右,如果右侧孩子的评分高于左侧,则将右侧孩子的糖果数在左侧孩子基础上加1
        for(int i=0; i<ratings.length-1;i++){
            if(ratings[i]<ratings[i+1]){
                candys[i+1]=candys[i]+1;
            }
        }
        //3.从右往左,如果左侧孩子的评分高于右侧,且左侧孩子的糖果数小于等于右侧,则左侧孩子糖果数等于右侧孩子糖果数+1
        for(int i=ratings.length-1;i>0;i--){
            if(ratings[i-1]>ratings[i]){
                if(candys[i-1]<=candys[i]){
                    candys[i-1] = candys[i]+1;
                }
            }
        }
        int sum=0;
        for(int i=0;i<candys.length;i++){
            sum+=candys[i];
        }
        return sum;
    }
}

(3)无重叠区间(leetcode435,medium)

  • 给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 
  • 注意 只在一点上接触的区间是 不重叠的。例如 [1, 2] 和 [2, 3] 是不重叠的。

示例 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(int[][] intervals) {
        if(intervals.length<=1){
            return 0;
        }
        //1. 将二维数组按照第二列的排序
        Arrays.sort(intervals,(a,b)->Integer.compare(a[1],b[1]));
        int delNum = 0;
        int previous = intervals[0][1];
        for(int i=0;i<intervals.length-1;i++){
            //2. 若前一个区间(未去除)的右边界大于后一个区间的左边界,则后一个区间应该去除
            if(previous>intervals[i+1][0]){
                delNum++;
            }else{
                previous = intervals[i+1][1];
            }
        }
        return delNum;
    }
}

变种 :用最少数量的箭引爆气球(leetcode452,medium)

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

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstartxend, 且满足  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]。

   实现原理:与(3)中问题相似的点在于重叠区间同样需要去掉,因为有重叠的区间的气球可以一箭击破,区别在于一点上接触的区间是 重叠的,因为同样可以一箭击破

实现方式如下:

class Solution {
    public int findMinArrowShots(int[][] points) {
        //1. 将points数组按照第二列排序
        Arrays.sort(points,(a,b)->Integer.compare(a[1],b[1]));
        //2. points数组中有重叠的区间排除掉(包括只有一点接触的)
        int delNum = 0;
        int previous = points[0][1];
        for(int i=1; i<points.length; i++){
            if(previous>=points[i][0]){
                delNum++;
            }else{
                previous=points[i][1];
            }
        }
        return points.length - delNum;
    }
}

 (4)种花问题(leetcode605,easy)

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

给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 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

实现思路:只有flowerbed[i-1]==0,flowerbed[i]==0,flowerbed[i+1]==0三者同时满足时,才能在位置i种花,注意考虑边界位置

实现代码如下:

class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int x = 0;
        if(flowerbed.length==1){
            if(flowerbed[0]==0){
                x++;
            }
            return x>=n;
        }
        
        for(int i=0;i<flowerbed.length;i++){
            //1.考虑左边界
            if(i==0){
                if(flowerbed[i]==0&&flowerbed[i+1]==0){
                    flowerbed[i]=1;
                    x++;
                }
            //2.考虑右边界
            }else if(i==flowerbed.length-1){
                if(flowerbed[i-1]==0&&flowerbed[i]==0){
                    flowerbed[i]=1;
                    x++;
                }
            //3.中间正常位置
            }else{
                if(flowerbed[i-1]==0&&flowerbed[i+1]==0&&flowerbed[i]==0){
                    flowerbed[i]=1;
                    x++;
                }
            }
        }
        return x>=n;
    }
}

(5)划分字母区间(leetcode763,medium) 

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 "ababcc" 能够被分为 ["abab", "cc"],但类似 ["aba", "bcc"] 或 ["ab", "ab", "cc"] 的划分是非法的。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

示例 1:

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

示例 2:

输入:s = "eccbbbbdec"
输出:[10]

实现原理:将每个字符最后一次出现在字符串中的位置记录下来,再从前往后遍历字符串,取所遍历字符最后出现位置的最大值,若该最大值正好和数组当前下标相等,说明所遍历的所有字符都不会在后续字符串中出现,即得到了一个有效的划分片段。

实现代码如下:

class Solution {
    public List<Integer> partitionLabels(String s) {
        char[] chars = s.toCharArray();
        //1.求出字符串中每个字符最后出现的位置
        int[] last = new int[26];
        for(int i=0; i<chars.length;i++){
            last[chars[i]-'a']=i;
        }
        List<Integer> lens = new ArrayList<>();
        int start=0,end=0;
        //2. 从前往后遍历字符,取每个字符出现的最后位置的最大值,若最大值与数组下标相同,说明这一片段中所有字符都只出现在当前片段中
        for(int i=0; i<chars.length;i++){
            end = Math.max(end,last[chars[i]-'a']);
            if(end == i){
                lens.add(end-start+1);
                start = i+1;
            }
        }
        return lens;
    }
}
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值