贪心算法
概念分析
贪心算法又称贪婪算法,无论是哪种称呼,都离不开一个贪字
所谓“贪”,就是要在有限的资源竞争中获取最大利益,下面来看一看官方的定义
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择,就能得到问题的答案。
每次都找出局部最优的选择,细心的读者应该发现了,这和同样是解决子问题的动态规划思想有什么不同吗?
贪心贪下来的一定每一步都是最优的结果,每一个局部最优构成了整体最优
而动态规划更注重整体,每一个子问题的解决不一定是当下的最优选择,但整体一定是最优的
因为贪心和动态规划的高度相似性,很多问题两种方法都可以解决,选哪个就视情况而定了,今天只讨论贪心
贪心的概念很简单,难的是怎么将这种思想用到题里面,下面让我们一起进入实战环节把
实战演练
455. 分发饼干 - 力扣(LeetCode)
-
贪心没有固定的模板,在最终答案的体现也仅仅是一种思想,这意味着它往往需要和别的算法进行结合使用
-
像这道题,如果你上来就“贪”,你会发现只能每次都做一次遍历,以此来找到局部的最优解
如果采用这种策略,不就变成了暴力的两个循环求解了吗
根据题目的特性,我们知道“贪”的是尽量满足孩子的胃口,小饼干给胃口小的孩子,大饼干给胃口大的孩子,从小到大依次给出饼干
-
从小到大?什么算法能实现这种需求,排序,不仅饼干的大小要排,孩子的胃口也要排,虽然用了排序后时间复杂度依然挺高,但至少
O(nlogn)
比O(n^2)
要好吧 -
数列有序后,下来要考虑怎么进行具体的分发,要满足孩子,那就让每块饼干从小到大排队去试一试能不能满足,不行再换大一点的饼干,这又是什么思想,熟悉的朋友应该能反应过来,双指针,由于孩子的胃口此时也有序了,自然不用每次都回到最小的饼干去遍历,直接让指针去指向下一个能用的饼干即可
思路理清,来看一看代码
int cmp(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize) {
qsort(g, gSize, sizeof(int), cmp);
qsort(s, sSize, sizeof(int), cmp);
int left = 0, right = 0;
int num = 0;
while (right < sSize) {
if (left >= gSize) {
break;
}
if (g[left] <= s[right]) {
left++;
num++;
}
right++;
}
return num;
}
往后的题就不像这道题一样要多个算法思想同时用了,只在思路上加难度
122. 买卖股票的最佳时机 II - 力扣(LeetCode)
- 贪心的一大特点就是思路难想代码简单
下来我们来正式分析一下这道题
- 这道题的关键就在手里只能同时有一张股票,那就好办了,不漏过任何一个可以赚钱的机会,手里没有股票时先买一张,在写的时候体现为用临时变量先存储,第二天时如果可以盈利就卖出去,如果不能盈利就更改临时变量的值,继续假设买的股票是第二天这张,相当于昨天没有买股票,依次循环即可
int maxProfit(int* prices, int pricesSize) {
int ans = 0;
int tep = prices[0];
for (int i = 1; i < pricesSize; ++i) {
if (tep < prices[i]) {
ans += prices[i] - tep;
}
tep = prices[i];
}
return ans;
}
如果不能抓住题目中的关键字眼,那贪心的题目将会无从下手,这倒是像在考察语文的阅读理解
134. 加油站 - 力扣(LeetCode)
- 贪心解法从有些时候来看也是暴力解法,这道题就很好的体现了
本题最纯的暴力解法很好想,外层循环控制开始站的位置,内层从该位置开始遍历数组判断燃料是否足够即可,当然超时也是毋庸置疑的,就算在过程中一遇到燃料耗尽就立马开始下一轮判断,这样的适度贪心也逃不过超时的命运
有了上一题的经验,想要“贪”的恰如其分就要注意题目的关键字,题目保证解是唯一的,换个角度理解,只要有解只会有一个解,再想,什么时候会没解,不管你从哪一个站点出发都会耗尽燃料,说明本身给的燃料数量就是小于需要消耗的燃料数的
如果在进行是否有解的判断后得到有解的答案,那么解只存在一个,假设我们每次都从第一站开始模拟,遇到燃料耗尽的情况后就只需要从下一站开始即可,因为无论你从这之前的哪一站开始,最终在该站前燃料都会耗尽,除非你在该站后开始能积累足够的燃料,那就从它后面作为起点,循环一圈积累盈余的燃料后再判断,在前面已经排除了无解的情况,所以能完美遍历一圈的开始点就是唯一的解
下面上代码
int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
int i;
int cursum = 0, totalsum = 0;
int flag = 0;
for (i = 0; i < gasSize; i++ ) {
cursum += gas[i] - cost[i];
totalsum += gas[i] - cost[i];
if (cursum < 0) {
cursum = 0;
flag = i + 1;
}
}
if (totalsum < 0) {
return -1;
}
return flag;
}
这里是先算出了每一站能剩下的燃料,当然也可以用较好理解的每一站去加减燃料的方法,cursum
即是当前剩下的燃料,只要不小于零就可以继续出发,而判断燃料是否足够也可以单独用一个循环进行判断,放在一个循环里也不影响,最终先判断燃料是否足够即可
采用贪心后仍是暴力的思路,将一些不可能的情况全部排除了,效率自然提升
135. 分发糖果 - 力扣(LeetCode)
- 看了三道题了,全是情景题,贪心的题目多依附于情景,下面这道题也不例外
此题最大的坑:得分高的孩子能得到更多的糖果≠
得分相同的孩子可以得到相同的糖果,在题目中给的第二个例子就可以很好的证明这一点,毕竟我们现在是贪心的奸商,能少给糖果就少给
那么就很明朗了,一个循环过去即可,判断后面的孩子是否比前面的还是分高,只有高的时候给他多一个糖果,同分的时候让第二个孩子委屈一下,依然只给一个
这样子只做到了瞻前,而没有顾后,怎么去顾后,就是此题最大的难点了,如果我们依然采用正向循环再遍历一遍,用前一个孩子的分数去与下一个孩子的分数进行比较,这是会重复运算的,导致最终结果产生偏差
仔细思考后,你会发现正向的循环怎么做都是错,你在第一次正向遍历比较的是后面的孩子比前面,加糖果是作用在后面的孩子身上
而现在要如果前面的孩子比后面的孩子分高,要作用在前面的孩子身上,理所应当的要采用反向循环,把第一个循环稍作修改贴好交上去,很不幸还是错的,因为奸商的特质,每次只多给一个糖果,万一这样做后仍然无法满足第二条,所以要对前一个孩子的糖果数和后一个孩子的糖果加一后的结果进行比较,来决定给多少糖果
决定好了每一个孩子的糖果数,加起来就是总共的糖果数了
int candy(int* ratings, int ratingsSize) {
int n = ratingsSize;
int sum = 0;
int tep;
int num[n];
for (int i = 0; i < n; i++) {
num[i] = 1;
}
for (int i = 1; i < n; i++ ) {
if (ratings[i] > ratings[i - 1]) {
num[i] += num[i - 1];
}
}
for (int i = n - 1; i > 0; i--) {
if (ratings[i - 1] > ratings[i]) {
num[i - 1] = fmax(num[i - 1], 1 + num[i]);
}
}
for (int i = 0; i < n; i++ ) {
sum += num[i];
}
return sum;
}
总结
贪心类问题的解决没有固定的模板,我们能做的就是把自己代入具体的情境中,找到对最有利的结果,同时需要找到题目中的题眼,往往是突破问题的关键