代码随想录算法训练营第三十一天 | 贪心算法理论基础、455.分发饼干、376. 摆动序列、53. 最大子序和
贪心算法理论基础
讲解:贪心算法理论基础
题目分类:
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。贪心算法并没有固定的套路,
唯一的难点就是如何通过局部最优推出整体最优。在刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。贪心的解题步骤
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
简而言之:常识性推导 + 举反例
455.分发饼干
贪心算法
思路:本题局部最优就是大饼干喂给胃口大的(也可以反过来,充分利用小饼干),充分利用大饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩,如下图所示:
注意:必须先遍历胃口,再遍历饼干,因为外面的 for 循环的 i 是固定移动的,而 if 里面的下标 index 是符合条件才移动的。如果 for 控制的是饼干, if 控制胃口,则会出现如下情况 :
上图表示:if 里的 index 指向胃口 10, for 里的 i 指向饼干 9,因为饼干 9 满足不了胃口 10,所以 i 持续向前移动,而 index 走不到 s[index] >= g[i] 的逻辑,所以 index 不会移动,那么当 i 持续向前移动,最终导致所有的饼干都匹配不上。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int count = 0;
int index = s.length - 1;
// 注意下方代码:先遍历胃口数组,再对饼干进行判断(代码随想录视频有误)
//
for (int i = g.length - 1; i >= 0; i--) {
if (index >= 0 && s[index] >= g[i]) {
count += 1;
index -= 1;
}
}
return count;
}
}
376. 摆动序列
贪心算法
思路:本题细节较多,详见注释。主要思路为从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。根据题意可将差值抽象为坡度,如下图所示:
如图可知,本题的局部最优为删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值 ,而全局最优则是整个序列有最多的局部峰值,从而达到最长摆动序列。本题要考虑如下三种情况:
- 上下坡中有平坡,对应代码中 if 判断中的 =。
- 数组首尾两端,对应代码中假设的与第一个元素相同的“虚拟头元素”。
- (易忽略) 单调坡中有平坡,如 [1,2,2,2,3,4],对应代码中仅在坡度变化时,更新prediff 的值(代表坡度的方向),如此 prediff 在 单调区间有平坡的时候就不会发生变化。
class Solution {
public int wiggleMaxLength(int[] nums) {
// 对应题目中一个元素的情况
if (nums.length <= 1) {
return nums.length;
}
// 当前元素与前一元素的差值,必须初始为 0,
// 相当于在第一个元素前添加了一个与第一个元素相同的元素,用于适应 for 循环中判断的条件
int prediff = 0;
int curdiff = 0; // 当前差值(可初始化为任意值)
int result = 1; // 默认将最后一个元素视为一个摆动元素
// i 从 1 开始,即先计算第二个元素与第一个元素的差值
// 因为上方对 prediff 的定义包含假设的与第一个元素相同的“虚拟头元素”
for (int i = 1; i < nums.length; i++) {
curdiff = nums[i] - nums[i - 1];
// 下方 < 或 > 代表前后差值为一正一负的情况,而 = 代表平坡情况,如/一一一\
if ((prediff <= 0 && curdiff > 0) || (prediff >= 0 && curdiff < 0)) {
result++;
// 仅在坡度变化时,prediff 会跟随curdiff 变化,否则会忽略连续平坡情况,
// 如 [1,2,2,2,3,4]
prediff = curdiff;
}
}
return result;
}
}
53. 最大子序和
贪心算法
思路:本题贪心点在于——若目前的子数组和小于 0,即若下一个元素为正数,则当前的和会使该正数变小,所以可以抛弃当前负数和,而将下一个正数作为新和的起点。
- 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 ,“连续和”只会越来越小。
- 全局最优:选取最大“连续和”。
class Solution {
public int maxSubArray(int[] nums) {
if (nums.length == 1){
return nums[0];
}
int sum = Integer.MIN_VALUE;
int count = 0;
for (int i = 0; i < nums.length; i++){
count += nums[i];
// // 取区间累计的最大值(相当于不断确定最大子序终止位置)
sum = Math.max(sum, count);
if (count <= 0){
count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
}
}
return sum;
}
}