贪心算法(持续更新)

理解贪心算法

贪心算法采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。
“田忌赛马”是大家耳熟能详的典故,齐威王和田忌都有上,中,下三个等级的马,整体差别不大,但是齐威王每个等级的马都比田忌的马跑得快。如果齐威王和田忌每次都使用相同等级的马进行比赛,那么田忌必输无疑。孙膑给田忌想了个办法,如下图:
在这里插入图片描述
全局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的策略,田忌可以获胜。

分配问题

455. 分发饼干

题目描述
有一群孩子和一堆饼干,每个孩子的胃口不一样,每块饼干的大小也不一样,当饼干的大小大于等于孩子的胃口时孩子能够吃饱,求最大的能够吃饱的孩子的数量。

输入输出样例
输入为两个数组,分别为孩子的胃口和饼干的大小,输出为能够吃饱的孩子的最大数量。

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

在这个样例中,我们可以给孩子吃[1, 4]、[2, 4]这两种组合的饼干。

题解
如果孩子的胃口和饼干的大小都不是按顺序排列的,可以使用两层for循环,外层遍历孩子的胃口,内层遍历饼干的大小,同时记录这个饼干的大小,如果饼干的大小大于等于孩子的胃口且小于之前记录的饼干大小,就认为给了这个孩子合适的饼干,还要将被选中的饼干从饼干数组中去除,不能再分发给其它孩子,这样做比较麻烦。
如果先将两个数组进行升序排序,从前往后遍历同时遍历两个数组,将最小的饼干分配给胃口最小的孩子(假设饼干的大小大于等于孩子的胃口),如果这块饼干的大小不能满足孩子的胃口,则选择较大的饼干和孩子的胃口进行比较。这样遍历到其中一个数组的末尾即可求出能满足胃口孩子的最大数量。

public class Solution {
	public int findContentChildren(int[] g, int[] s) {
		int gsize = g.length;
		int ssize = s.length;
		Arrays.sort(g);
		Arrays.sort(s);
		int gcount = 0;
		int scount = 0;
		while(gcount < gsize && scount < ssize) {
			if(g[gcount] <= s[scount]) {
				++gcount;
			}
			++scount;
		}
		return gcount;
	}
}

这里使用的贪心策略是:使用尽可能小的饼干满足胃口较小的孩子。

135. 分发糖果

题目描述

有一群评分不相同的孩子(评分用数组ratings表示),需要获得糖果作为奖励,每个孩子至少获得一颗糖果,而且相邻的两个孩子中评分高的孩子需要获得比评分低的孩子更多的糖果,求同时满足这些孩子的最小糖果数目。

输入输出样例

Input : [1, 0, 2]
Output : 5

在这个样例中,评分为0的孩子获得1颗糖果,和他相邻的评分为1和2的孩子因为需要满足评分更高的孩子比评分更低的孩子获得更多的糖果,所以分别获得两颗糖果,结果为[2, 1, 2],求和为5.

题解
题目要求有两点:1、每个孩子至少获得一颗糖果;2、相邻的两个孩子中评分高的孩子需要获得比评分低的孩子更多的糖果。
首先创建和评分数组长度相同的数组counts;
第一点容易满足,将数组元素赋默认值1;
第二点如果想要通过一次遍历得到结果,需要考虑一个元素左右两边的的元素取值,同时要考虑数组的边界问题,比较麻烦。可以分两次顺序和逆序遍历数组,顺序遍历时只考虑右侧的孩子的评分,如果右侧孩子的评分比左侧孩子评分更高,则右侧孩子获得的糖果数等于左侧孩子的糖果数加1;逆序遍历数组时,如果左侧孩子的评分要高,且左侧孩子的糖果数不大于右侧孩子的糖果数,那么左侧孩子的糖果数等于右侧孩子的糖果数加1。如此两次遍历之后,counts数组已经满足题目要求,返回数组元素的和即可。

class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        if(n < 2) {
            return n;
        }
        int[] counts = new int[n];
        for(int i = 0; i < n; ++i) {
            counts[i] = 1;
        }
        for(int i = 1; i < n; ++i) {
            if(ratings[i] > ratings[i - 1]) {
                counts[i] = counts[i - 1] + 1;
            }
        }
        for(int i = n - 1; i > 0; --i) {
            if(ratings[i] < ratings[i - 1]) {
                counts[i - 1] = Math.max(counts[i - 1], counts[i] + 1);
            }
        }
        return Arrays.stream(counts).sum();
    }
}

这里使用的贪心策略是:先考虑一侧糖果的取值,再考虑另一侧糖果的取值,两种情况合在一起就是最后的结果。合并所有局部最优的情况得到全局最优。

区间问题

435.无重叠区间

题目描述
给定多个区间,要求计算满足所有区间不重叠时需要移除区间的最小个数,首尾相连不算重叠。

输入输出样例

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

在这个样例中,去掉区间[1, 3]可以使区间[1, 2]和[2, 4]不重叠,所以去除区间的最小个数为1.

题解
1、如果两个区间有交集(除边界外),就可以判定两个区间是重叠的;
2、由于区间数组的大小不固定,要想更方便的判断数组之间是否重叠,需要对数组进行排序;
3、判断两个区间是否重叠,可以用第一个区间的尾部a同第二个区间的头部b进行比较,如果a <= b,说明两个区间不重叠,所以2中排序应该按照区间的尾部进行排序,这样可以保证比较的完整性;
4、遍历数组,取排序后的第一个区间的尾部a同第二个区间的头部b相比较,如果a > b,则需要移除的区间数加1,否则取第二个区间的尾部继续同第三个区间的头部进行比较,以此类推,直到循环结束,最后返回移除的区间数即可。

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if(intervals.length == 0) {
            return 0;
        }

        Arrays.sort(intervals, new Comparator<int[]>() {
            public int compare(int[] intervals1, int[] intervals2) {
                return intervals1[1] - intervals2[1];
            }
        });

        int removed = 0;
        int prev = intervals[0][1];
        for(int i = 1; i < intervals.length; ++i) {
            if(intervals[i][0] < prev) {
                ++removed;
            } else {
                prev = intervals[i][1];
            }
        }
        return removed;
    }
}

练习题

下面是书上一些贪心算法的练习题,大家有兴趣可以做做,我也会坚持在博客上更新题解,大家可以在评论区一起交流心得。
基础难度
605. 种花问题
452. 用最少数量的箭引爆气球
763. 划分字母区间
122. 买卖股票的最佳时机 II

思路与解答
605. 种花问题

题目描述
有一个很长的花池,相邻的位置不能种花,因为相邻的花会因为争夺水源而死去,花池的主人已经在花池里种了一部分花,种花的位置被标记为1,未种花的位置被标记为0,种了部分花后花池的数组为flowerbed,剩下n朵花没有种,请帮助花池的主人判断花池中剩下的位置是否可以将剩下的n朵花种完?

输入输出样例
示例1

Input : flowerbed = [1, 0, 0, 0, 1], n = 1
Output : true

示例2

Input : flowerbed = [1, 0, 0, 0, 1], n = 2
Output : false

题解
因为已经种了一部分花,所以要考虑在空位置种花时不能和已经种花的位置相邻。如果两个相邻的位置都种了花,中间没有空位置,所以相邻位置中间不能种花,其余情况画图理解为:
1、两个种花的位置中间空一位:
在这里插入图片描述
由于只有一个空位,种上花后会和左右两个位置都相邻,不能种花。

2、两个种花的位置中间空两位:
在这里插入图片描述
两个空位种在任意一个位置都会和左边或右边的花相邻,不能种花。

3、两个种花的位置中间空三位
在这里插入图片描述
标记为2的空位种花可以满足要求,可以种花的数目为1朵。

4、从上面三步进行推导,两个种花的位置分别为 i 和 j ,当 j - i >= 4时,可以在其中间种花,此时需要排除的位置为 i 的右边相邻位,j 的左边相邻位,同时还要注意,以第3步为例,4 - 0 = 4,而0和4之前只有三个空位,所以还要排除 j - i 后多出来的一个空位,所以可以种花的位置为p = j - i - 3, 又因为不能在相邻的位置种花,所以可以种花的数量为:当p为偶数时,count = p / 2;当p为奇数,count = (p + 1) / 2, 当p为偶数时,p / 2 = (p + 1) / 2,所以count = (p + 1) / 2, 即最多可以在该范围内种植( j − i − 2)/2 朵花。

5、考虑完两朵花之间可以种花的数量,还有一种情况:

  • 设L为已种的花中最左边的花的下标,当L < 2时,L的左边不能种花,当L >= 2时,可以在[0, L - 2]的范围内种花,可以种植花的位置为L - 1, 最多种植花的数量为L / 2
  • 设M为已种的花中最右边的花的下标,m为花池数组的长度,当 M >= m - 2 时,M的右边不能种花,当 M < m - 2 时,可以种花的位置为m - M - 2,最多种植花的数量为 (m - M - 1) / 2
    这里需要注意数组的下标是从0开始的,数组的长度等于数组最后一位的下标 + 1

6、当花池中没有种花时,可以种花的最大数量为count = (m + 1) / 2,m为数组的长度。

根据上述计算方法,计算花坛中可以种入的花的最多数量,判断是否大于或等于 n 即可。具体做法如下。

  • 维护 prev 表示上一朵已经种植的花的下标位置,初始时 prev = −1,表示尚未遇到任何已经种植的花。
  • 从左往右遍历数组 flowerbed,当遇到 flowerbed[i]=1 时根据 prev 和 i 的值计算上一个区间内可以种植花的最多数量,然后令 prev=i,继续遍历数组 flowerbed 剩下的元素。
  • 遍历数组 flowerbed 结束后,根据 prev 和长度 m 的值计算最后一个区间内可以种植花的最多数量。
  • 判断整个花坛内可以种入的花的最多数量是否大于或等于n。
class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int count = 0;
        int m = flowerbed.length;
        int prev = -1;
        for(int i = 0; i < m; ++i) {
            if(flowerbed[i] == 1) {
                if(prev < 0) {
                    count += i/2;
                } else {
                    count += (i - prev - 2) / 2;
                }
                if(count >= n) {
                    return true;
                }
                prev = i;
            }
        }
        if(prev < 0) {
            count += (m + 1)/2;
        } else {
            count += (m - prev - 1)/2;
        }
        return count >= n;
    }
}
  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值