1、贪心算法学习及leetcode力扣网例题详解

贪心算法案例

文章目录

  • 贪心算法案例
    • 概述
    • 思路
    • 使用条件
    • 存在问题
    • 例题
      • 分配问题
        • AssignCookies
          • 输入输出样例
          • 题解
          • 代码实现
        • Candy
          • 输入输出样例
          • 题解
          • 代码实现
      • 区间问题
        • Non-overlapping Intervals
          • 输入输出样例
          • 题解
          • 代码实现

概述

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

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。也就是说,不从整体最优上加以考虑,做出的只是在某种意义上的局部最优解。

例如:小明最多能吃5个苹果,小王最多能吃3个苹果。在一个有吃不完的苹果的苹果园里,小明和小王最多能吃多少个苹果?

在这个例子中使用的贪心策略是每个人能吃最多数量的苹果,这在每个人身上是局部最优。又因为全局结果是局部最优的简单求和,并且局部结果互不相干,因此此局部最优的策略也同样是全局最优的策略。

思路

贪心算法一般按如下步骤进行:

  1. 建立数学模型来描述问题
  2. 把求解的问题分成若干个子问题
  3. 对每个子问题求解,得到子问题的局部最优解
  4. 把子问题的解局部最优解合成原来解问题的一个解

使用条件

利用贪心法求解的问题应具备如下2个特征:

  1. 贪心选择性质
    一个问题的整体最优解可通过一系列局部的最优解的选择达到,并且每次的选择可以依赖以前作出的选择,但不依赖于后面要作出的选择。这就是贪心选择性质。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
  2. 最优子结构性质
    当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心法求解的关键所在。在实际应用中,至于什么问题具有什么样的贪心选择性质是不确定的,需要具体问题具体分析。

存在问题

贪心算法也存在如下问题:

  1. 不能保证解是最佳的。因为贪心算法总是从局部出发,并没从整体考虑
  2. 贪心算法一般用来解决求最大或最小解
  3. 贪心算法只能确定某些问题的可行性范围

例题

分配问题

AssignCookies

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

输入输出样例

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

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

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

题解

根据贪心算法,首先考虑喂饱的是饥饿度最小的孩子,满足局部最优。之后,继续寻找饥饿度最小的孩子,直到剩下的饼干不能满足任何小孩。

贪心策略即给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。

实现方法:把孩子的饥饿度和饼干大小进行排序,从饥饿度最小的孩子和最小的饼干配对,计算有多少对能满足条件。

代码实现
import java.util.Arrays;

public class Client {
	
	public static void main(String[] args) {
		int[] g = new int[] {1,2};
		int[] s = new int[] {1,2,3};
		System.out.println("能让"+findContentChildren(g,s)+"个小孩吃饱");
	}
	
	public static int findContentChildren(int[] g, int[] s) {
        //首先初始化变量
        int child = 0;
        int cookie = 0;
        //先将饼干和孩子所需大小都进行排序
        Arrays.sort(g); 
        Arrays.sort(s);
        while (child < g.length && cookie < s.length ){ //当其中一个遍历完就结束
            if (g[child] <= s[cookie]){ //当用当前饼干可以满足当前孩子的需求,可以满足的孩子数量+1
                child++;
            }
            cookie++; // 饼干只可以用一次,因为饼干如果小的话,就是无法满足被抛弃,满足的话就是被用了
        }
        return child; 
	}
}
Candy

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

输入输出样例
Input: [1,0,2] 
Output: 5
题解

首先先把所有孩子的糖果数改为1,再遍历两遍,首先从左到右遍历一遍,如果右边孩子的评分比左边的高,那么把右边孩子的糖果数变为左边孩子的糖果数+1;再从右到左遍历一遍,如果左边孩子的评分比右边孩子高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数改为右边孩子的糖果数+1。

代码实现
public class Client {
	
	public static void main(String[] args) {
		int[] ratings = new int[] {1,0,2}; //小孩评分数组
		System.out.println("最少需要"+Candy(ratings)+"个糖果");
	}
	
	public static int Candy(int[] ratings) {
		//如果数组里只有一个小孩,那么只需要给他一个人分一个糖果
		int size = ratings.length;
		if (size < 2) {
			return size;
		}
		int[] num = new int[size]; //糖果数组
		//从左往右遍历一遍
		for (int i=0; i<size; i++) {
			//如果右边孩子的评分比左边的高,那么把右边孩子的糖果数变为左边孩子的糖果数+1
			//设置i>0的条件是因为第一个小孩左边没有小孩进行比较,因此直接设置为1
			if (i>0 && ratings[i] > ratings[i-1]) {
				num[i] = num[i-1] + 1;
			}else {
				num[i] = 1;
			}
		}
		//从右往左再遍历一遍
		int candy = 0;
		for(int i=size-1; i>=0; i--) {
			//如果左边孩子的评分比右边孩子高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数改为右边孩子的糖果数+1
			//设置i
			if (i<size-1 && ratings[i] > ratings[i+1]) {
				if (num[i] <= num[i+1]) {
					num[i] = num[i+1] + 1;
				}
			}
			//计算需要的糖果数量
			candy += num[i];
		}
		return candy;
	}
}

在程序中,我们初始化糖果分配为[1,1,1],第一次遍历更新后的结果为[1,1,2],第二次遍历更新后的结果为[2,1,2]。

区间问题

Non-overlapping Intervals

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

输入输出样例

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

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

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

题解

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

具体实现方法为,先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。

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

代码实现
public class Client {

	public static void main(String[] args) {
		// [[1,2], [2,3], [3,4], [1,3]]
		int[][] intervals = new int[][] {{1,2},{2,3},{3,4},{1,3}};
		System.out.println(eraseOverlapIntervals(intervals));
	}
	
	public static int eraseOverlapIntervals(int[][] intervals) {
		//先把区间按照结尾的大小进行增序排序,使用冒泡排序
		for (int i=0; i<intervals.length-1; i++) {
			for (int j=0; j<intervals.length-1; j++) {
				if (intervals[j][1] > intervals[j+1][1]) {
					int[] temp = intervals[j];
					intervals[j] = intervals[j+1];
					intervals[j+1] = temp;
				}
			}
		}
		
		//每次选择结尾最小且和前一个选择的区间不重叠的区间
		int total=0, prev=0;
		for(int i=0; i<intervals.length; i++) {
			if (i==0) {
				prev = intervals[0][1];
				continue;
			}
			if (intervals[i][0] < prev) {
				//如果当前区间比已经选择的区间的结尾小,则移除
				total++;
			}else {
				//否则更新已选择的区间的结尾
				prev = intervals[i][1];
			}
		}
		return total;
	}
}

当然,在进行区间排序的时候,可以使用以下方法,大大减少运行时间。

import java.util.Arrays;
import java.util.Comparator;

public class Client {

	public static void main(String[] args) {
		// [[1,2], [2,3], [3,4], [1,3]]
		int[][] intervals = new int[][] {{1,2},{2,3},{3,4},{1,3}};
		System.out.println(eraseOverlapIntervals(intervals));
	}
	
	public static int eraseOverlapIntervals(int[][] intervals) {
		Arrays.sort(intervals, new Comparator<int[]>() {
            public int compare(int[] interval1, int[] interval2) {
                return interval1[1] - interval2[1];
            }
        });
		
		//每次选择结尾最小且和前一个选择的区间不重叠的区间
		int total=0, prev=0;
		for(int i=0; i<intervals.length; i++) {
			if (i==0) {
				prev = intervals[0][1];
				continue;
			}
			if (intervals[i][0] < prev) {
				//如果当前区间比已经选择的区间的结尾小,则移除
				total++;
			}else {
				//否则更新已选择的区间的结尾
				prev = intervals[i][1];
			}
		}
		return total;
	}
}

本篇文章参考书籍有:
《LeetCode101:和你一起你轻松刷题(C++)》 高畅


作者:阿涛
CSDN博客主页:https://blog.csdn.net/qq_43313113
如有不对的地方,欢迎在评论区指正
欢迎大家关注我,我将持续更新更多的文章


你可能感兴趣的:(数据结构与算法,贪心算法,算法,leetcode)