本部分介绍“贪心算法“ 。 接下来会介绍动态规划。回顾一下之前脉络:
什么是递归?如何设计递归算法?
||
\/
常见的递归算法应用(快排、归并、堆、)
||
\/
深入递归本质:数学归纳,递推
||
\/
深度遍历优先搜索(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。
具体而言:
-
用两个变量模拟5元和10各有多少张。
-
依次模拟买东西,根据不同的面额进行相应的处理
-
5元:fives++
-
10元:判断fives > 0 , five --, tens++
-
20元: if tens > 0 : t -= 10;
t>0 且 fives >0 : t-=5, fives –
-
-
循环结束返回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 =