一、概念
贪心算法,又称贪婪算法,是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的事在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
贪心算法的基本思路是从问题的某一个初始解出发一步一步的进行,根据某个优化测度,每一步,都要确保获得局部最优解。每一步只考虑一个数据,他的选择应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不该把数据添加到部分解中,直到所有数据枚举完,或者不能再添加算法停止。
虽然叫贪心算法,其实是一种策略,可以用一个简单的例子说明一下这种算法,比如在所有面值钞票里,选择10张,如何让选出来的钞票最后总额最大。从贪心算法去分析,想要结果最优,即总额最大,那就要局部最优,即单张面值最大,那么就是10张每张都拿当前可选的最大的面额,就能达到整体总额最大。
二、实例及其解决方案
1、栗子--钱币找零
1.1 题目
面对一个给定的以及一定数量的纸币和硬币,给出一个总数,问如何使用这些钞票硬币,使得总数恰好等于所需的总数,并且要求使用的钞票硬币数量最少。
1.2 思路
贪心策略:选择一个钞票/硬币时,选择最大的金额,并且保证所选金额小于所需总数。并继续选择下一个金额最大的,直到所需总数为0为止。
比如,当我们需要找零11元,零钱面值只有1元、2元、5元的时,我们可以采取以下策略:
选择最大面额的硬币5元,此时剩余需要找零6元;
再次选择最大的面额的纸币5元,还需找零1元;
选择最小的纸币1元,已经满足需要找零总数,张数3张;
此时,所用硬币数最少,共使用3个钱币。
此时,贪心算法得到了最优解,但是并不是所有情况下的结果就一定是最优解。
1.3 算法实现
/**
* {acount} number 需要找零总数
* {list} number[] 可找零钱币面额可选范围
* */
function greedy1(acount, list) {
let s = 0;
const saveD = [];
const arr = list;
arr.sort((a,b) => a-b);
let n = list.length - 1;
while(s < acount && n > -1) {
if (s+arr[n] < acount) {
s = s+arr[n];
saveD.push(arr[n]);
} else if (s+arr[n] == acount) {
s = s+arr[n];
saveD.push(arr[n]);
break;
} else {
n = n - 1;
}
}
return saveD;
}
console.log(greedy1(41,[1,5,10,20]));
2、栗子--排队打水问题
2.1 题目
有n个人排队到r个水龙头前去打水,他们装满水桶的时间分别是t1,t2,t3,...,tn,打水时间均为整数且各不相等,应该如何安排他们的打水时间才能使他们花费的总时间最少?
每个人的打水时间=排队时间+装满水桶的时间(假设排队切换等其他过程均不消耗时间)
2.2 思路
使用贪心算法,目标是所有人打水的总时间最少,那每个人打水的时间要当前最少,装满水桶的时间是不变的,那么就需要当前选择的水龙头排队的时间是最少的,那么如何保证当前这个人排队时间是当前最短的?
首先,第一轮打水的人要是装满水桶时间最少的那批人,才能保证后面的人等待的时间最短,所以要对所有人的装满水桶时间进行排序,得到一个数组。然后从这个数组里,直接安排r个人去进行第一轮打水,此时第二批人已经按照下面的站位进行等待,这样能保证每个人的等待时间是对他而言最短的,第r+1个人的总打水时间就是第一个人的装满水桶时间加上第r+1个人装满水桶的时间,以此类推,得到下面这张图。最后总的打水时间为
2.3 算法实现
/**
* {tapNum} number 水龙头个数
* {peopleTime} number[] 每个人打水所需时间组成的数组
* */
function greedy(tapNum, peopleTime) {
let s = 0;
const arr = peopleTime;
arr.sort((a,b) => a-b);
for (let i = 0; i < peopleTime.length; i++) {
if (i >= tapNum) {
arr[i] = arr[i] + arr[i-tapNum];
}
s = s + arr[i];
}
return s;
}
console.log(greedy(3,[5,2,6,1,8,3,6]));
三、贪心算法的特点
1、贪心算法的特点
优点:
1、简单易实现:贪心算法在每一步只考虑当前最优选择,不需要预测未来状态,因此实现起来较为简单。
2、时间复杂度低,因为只需要对当前情况进行一次计算,在不考虑排序的前提下,时间复杂度是O(n)。
缺点:
1、不能保证最后解是最优解,因为每次选择可能会对后续的选择产生影响,而贪心算法无法考虑这些影响。
2、难以判断某一问题是否适用贪心算法
常见应用场景:如最小生成树和最短路径算法。
对于它的一些缺点,有个与之相对应的概念--动态规划通常可以解决。
2、贪心算法与动态规划的区别
2.1 解决问题的类型不同
贪心算法是没有远视的一种算法,一般用于在做决策时,通过选择当前最优的选项来直接确定后续决策的问题,它关注的是在每个阶段做出最优选择,从而构造出整体最优解。与之相对的动态规划,通常用于解决一个复杂问题,该问题可以分解为多个子问题,这些问题通常在某种程度上是相互重叠的子单元模块,动态规划通过存储和重用子问题的最优解来避免重复计算,从而找到整个问题的最优解。
2.2 能否获得全局最优解不同
贪心算法通常能找到全局最优解,但并不一定,动态规划一定能找到全局最优解,但是过程可能复杂度比较高
如下:
贪婪算法以达到最大和为目标,在每一步都会选择看起来是最优的即时选择,所以在第二步会选择12而不是3,不会得到包含99的最优解。
2.3 算法复杂度不同
贪心算法通常比较简单,其时间复杂度可以是线性级别,而空间复杂度一般较低。动态规划的时间复杂度和空间复杂度通常相对较高,因为它需要存储和重用子问题的解。
2.4 实现方式不同
贪心算法从问题的顶层开始,逐步向下进行优化,每次选择都是基于当前状态下最佳的选择。动态规划则是从问题的底层开始,逐步向上构建最优解,它需要自底向上计算,并保存子问题的最优解以避免重复计算。
四、总结
1、贪心算法是一种思想和策略选择
2、与贪心算法相对的概念,动态规划,后面会单独分享
3、贪心算法的常见案例