[算法系列]贪心算法策略介绍+10道经典例题解析

本文介绍了贪心算法的概念,并通过10道例题详细阐述了贪心算法的应用,包括零钱兑换、柠檬水找零、分发饼干、摇摆序列等问题,展示了贪心算法如何通过局部最优解来找到全局最优解。每个问题都提供了思路分析和代码实现,帮助读者理解贪心算法的设计和思考方式。
摘要由CSDN通过智能技术生成

本部分介绍“贪心算法“ 。 接下来会介绍动态规划。回顾一下之前脉络:

什么是递归?如何设计递归算法? 
			||
			\/
常见的递归算法应用(快排、归并、堆、) 
			||
			\/
深入递归本质:数学归纳,递推
			||
			\/
深度遍历优先搜索(DFS)、回溯、剪枝
			||
			\/
贪心算法、动态规划

那么贪心、动规与前面这些有什么联系呢?为什么要放在这里介绍?

  • 首先,贪心、动规和dfs这样的搜素算法实际很相似,是为了搜索解空间获得(满足条件)的解。DFS是按照一定的(深度优先)次序。逐一枚举遍历。相比之下:
    • 动态规划和贪心算法同样都是一种递推算法. 均用局部最优解来推导全局最优解,是对遍历空间的一种优化
    • 当问题具有最优子结构时,可用动态规划,而贪心是动态规划的特例, 特殊之处在于 眼前的最优解即是全局最终的最优解。
    • 因此我们在遍历搜索解空间时,可以按照我们所预先设定的规则,逐一进行搜寻并缩小剩余的解的空间,直至获得所有解集。
  • 其次,贪心和dp追根溯源是从子问题的解推出更大问题规模的解,这一点上和递归所追求的一脉相承。不过将会看到,很多时候由于递归本身的复杂冗余计算和资源消耗,很多时候会对其进行简化(dp中的备忘录法等),形式上可能有所不同。个人觉得放在这里介绍应该还算合理。

1. 零钱兑换

有固定面额的零钱数组coins=[1,2,5],每种有无穷多枚。现给定一个amount,由这些conins组成amount,比如(11 = 5+ 2+2+2)。

问,在所有能构成amount的硬币组合中,所需硬币最少多少枚?
例:
输入:11
输出:3
解释:11=5*2+1,两枚5元的,一枚1元的

思路:我们先不忙解题,来结合前面的介绍来体会一下贪心吧!

  • 一堆coins组成amount,很显然会有很多种不同的解。如果要求我们列出所有的解,暴力搜索、dfs都是不错的方法。
  • 在这些所有解当中,一定会有一个解,这个解中所使用的硬币数量最少。
  • 如何寻找出这个解呢?可以暴力搜索所有解,然后找出len最小的那个。
  • 贪心是怎么想的呢?正如我们平时买东西一样,我们尽量每一次用最大面额去逼近amount,比如11元就用2个5元的,而不会用5个2元的。

我们在来看看dfs与贪心的区别与联系(相信各位心里已经比较清楚了)

在这里插入图片描述

上图是全部解空间树,红色数字表示该路径上用一个这个面额的硬币去组成。可以看到11 ==》 10 == 》5==》 0 这条路径最短,即为所求。

现在进一步地,体会一下局部最优即为整体最优

  • 初始问题:amount=11,从coins中选出小于amount的最大面额硬币(5元),amount变为6元。
  • 问题变为:amount=6。同样的,从coins中选出小于amount的最大面额硬币(5元),amount变为1元。
  • 问题变为:amount=1。于是再选一个1元硬币即可。

我们每一次操作都选定一个硬币,这是我们的局部最优,当解完后,我们发现每一次的局部最优也就构成了我们的整体最优解(11=5+5+1)。

进一步的,贪心思想这样想,我们一开始就用最大的面额的最多张数去构成amount,比如11 就用两张5元的。

下面是代码,其中包含了dfs式写法和迭代写法,和前讲述的递归设计取得了形式上的统一:

class Solution_g01{
   
	int[] values = new int[] {
   1,2,5};
	//迭代版
	public int findMinCom1(int amount) {
   
		int count = 0;
		for(int i = values.length-1 ; i>=0 ; i -- ) {
   
			int use = amount / values[i];	//最多能取多少个,当前最大面额的硬币
			count += use;
			amount -= use *values[i];		//减去当前最大面额的硬币数,进入下一轮迭代
		}
		return count;
	}
	
	//递归版
	public int  findMinCom2(int amount) {
   
		int count = 0;
		return dfs(amount , values.length -1);
		
	}

	private int dfs(int amount, int i) {
   
		if(amount == 0 )
			return 0;
		
		int use = amount / values[i];
		return use+dfs(amount - use * values[i], i-1);
	}
}

2.柃檬水找零LeetCode860

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。

顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1:
输入:[5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

示例 2:
输入:[5,5,10]
输出:true

示例 3:
输入:[10,10]
输出:false

示例 4:
输入:[5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。

提示:

0 <= bills.length <= 10000
bills[i] 不是 5 就是 10 或是 20

思路:当给别人进行找零时,尽量先用大面额的找个他,比如20找15时,先用10元,再用5元。当所拥有的钱数无法满足找零时返回false。

具体而言:

  1. 用两个变量模拟5元和10各有多少张。

  2. 依次模拟买东西,根据不同的面额进行相应的处理

    1. 5元:fives++

    2. 10元:判断fives > 0 , five --, tens++

    3. 20元: if tens > 0 : t -= 10;

      ​ t>0 且 fives >0 : t-=5, fives –

  3. 循环结束返回true

    public boolean lemonadeChange(int[] bills) {
   
    	int fives = 0,tens = 0 ; 
    	for(int b : bills) {
   
    		if(b == 5 ) fives ++;
    		else if(b == 10) {
   
    			if(fives > 0) fives --;
    			else return false;
    			tens++;
    		}else {
    //拿20过来买
    			int t = 15;
    			if(tens > 0) {
   
    				t -= 10;
    				tens --;
    			}
    			while(t >0 && fives > 0) {
   
    				t -= 5;
    				fives --;
    			}
    			if(t > 0) return false;
    				
    		}
    	}
    	return true;
    }

3.分发饼干 LeetCode 455

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

注意:

你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。

示例 1:
输入: [1,2,3], [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 2:
输入: [1,2], [1,2,3]
输出: 2

思路:

  • 假设输出答案为k的话,表示一定能满足从小到大排序中前k个小孩的胃口。 因此我们反过来想,假设给定的胃口序列不是单调递增的,我们可以将其转换为单增,然后用饼干序列去满足。
  • 如何满足呢?对于每一个小孩胃口,我们尽量用所满足的最小的饼干去分配。
    public int findContentChildren(int[] g, int[] s) {
   
    	Arrays.sort(g);
    	Arrays.sort(s);
    	
    	int i = 0 , j = 0 , res = 0;
    	for(i =
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值