贪心算法理论基础
什么是贪心
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
贪心的套路(什么时候用贪心)
贪心算法并没有固定的套路。
唯一的难点就是如何通过局部最优,推出整体最优。
那么如何能看出局部最优是否能推出整体最优呢?有没有什么固定策略或者套路呢?
不好意思,也没有! 靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。
有同学问了如何验证可不可以用贪心算法呢?
最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
贪心一般解题步骤
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。
做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
总结
本篇给出了什么是贪心以及大家关心的贪心算法固定套路。
不好意思了,贪心没有套路,说白了就是常识性推导加上举反例。
最后给出贪心的一般解题步骤,大家可以发现这个解题步骤也是比较抽象的,不像是二叉树,回溯算法,给出了那么具体的解题套路和模板。
455. 分发饼干
文章讲解:代码随想录
重点:
1. 局部最优:充分利用小饼干,小饼干先喂饱小胃口
思路:
1. 先把胃口和饼干从小到大排序
Arrays.sort(g); Arrays.sort(s);
2. 从小到大遍历饼干,如果当前饼干满足当前的最小胃口,则childIndex+1,移动到满足下一个小孩
int childIndex = 0; for (int i = 0; i < s.length; i++) { if (childIndex < g.length && g[childIndex] <= s[i]) { childIndex++; } }
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int childIndex = 0;
for (int i = 0; i < s.length; i++) {
if (childIndex < g.length && g[childIndex] <= s[i]) {
childIndex++;
}
}
return childIndex;
}
376. 摆动序列
文章讲解:代码随想录
重点:
1. 局部最优:删掉单调坡度上的中间节点,让峰值尽可能的保持峰值
2. 整体最优: 整个序列有多个局部峰值,从而达到最长摆动序列
3. 三种特殊情况:上下坡中有平坡,首尾元素,单调坡中有平坡
普通情况:
上下坡中有平坡:
首尾元素:
if (nums.length == 1) { return 1; }
可以假设,数组最前面还有一个数字,那这个数字应该是什么呢?
之前我们在 讨论 情况一:相同数字连续 的时候, prediff = 0 ,curdiff < 0 或者 >0 也记为波谷。
那么为了规则统一,针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即 preDiff = 0,如图:
单调坡中有平坡:
思路:
贪心算法:
1. 数组长度只为1时,直接返回1
2. prediff = 0,默认最前面多加一个相同的元素,result = 1,默认最后一个元素为摆动序列的值
// 上一个差值 int prediff = 0; // 当前差值 int curdiff = 0; // 最终摆动序列长度,默认包括最后一个元素 int result = 1; for (int i = 0; i < nums.length - 1; i++) { // 计算当前差值 curdiff = nums[i + 1] - nums[i]; // 如果当前差值和上一个差值为一正一负 // 等于0的情况表示首元素 if ((prediff >= 0 && curdiff < 0) || (prediff <= 0 && curdiff > 0)) { result++; // prediff放在if里面更新是为了防止单调坡中的平坡也被计算了 prediff = curdiff; } }
public int wiggleMaxLength(int[] nums) {
if (nums.length == 1) {
return 1;
}
// 上一个差值
int prediff = 0;
// 当前差值
int curdiff = 0;
// 最终摆动序列长度,默认包括最后一个元素
int result = 1;
for (int i = 0; i < nums.length - 1; i++) {
// 计算当前差值
curdiff = nums[i + 1] - nums[i];
// 如果当前差值和上一个差值为一正一负
// 等于0的情况表示首元素
if ((prediff >= 0 && curdiff < 0) || (prediff <= 0 && curdiff > 0)) {
result++;
// prediff放在if里面更新是为了防止单调坡中的平坡也被计算了
prediff = curdiff;
}
}
return result;
}
53. 最大子序和
文章讲解:代码随想录
重点:
贪心算法:
1. 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
2. 全局最优:选取最大“连续和”
局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
常见误区:
1. 不少同学认为 如果输入用例都是-1,或者 都是负数,这个贪心算法跑出来的结果是 0, 这是又一次证明脑洞模拟不靠谱的经典案例,建议大家把代码运行一下试一试,就知道了,也会理解 为什么 result 要初始化为最小负数了。
思路:
贪心算法:
1. 初始化记录最大值的result和当前连续和curSum
int result = Integer.MIN_VALUE; int curSum = 0;
2. for循环遍历nums,当前连续和大于之前记录的最大值,更新result。当当前连续和为负数时,会拖累后面的累加和,直接舍弃
for (int num : nums) { curSum += num; // 取区间累计的最大值(相当于不断确定最大子序终止位置) if (curSum > result) { result = curSum; } // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和 if (curSum < 0) { curSum = 0; } }