Day 23
贪心算法其实就是没有什么规律可言,所以大家了解贪心算法就了解它没有规律的本质就够了
不用花心思去研究其规律, 没有思路就立刻看题解
基本贪心的题目 有两个极端,要不就是特简单,要不就是死活想不出来
学完贪心之后再去看动态规划,就会了解贪心和动规的区别
理论基础
文章讲解:https://programmercarl.com/贪心算法理论基础.html
视频讲解:https://www.bilibili.com/video/BV1WK4y1R71x/?vd_source=822e86b53dab98632ef279a46d2536db
什么是贪心
例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?
指定每次拿最大的,最终结果就是拿走最大数额的钱。
每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。
贪心的本质
(1)贪心的本质是选择每一阶段的局部最优,从而达到全局最优
(2)贪心算法,没有套路 ❗❗❗
解题步骤
(1)将问题分解为若干个子问题
(2)找出适合的贪心策略
(3)求解每一个子问题的最优解
(4)将局部最优解堆叠成全局最优解
这个四步其实过于理论化了,我们平时在做贪心类的题目时,如果按照这四步去思考,真是有点“鸡肋”。
做题的时候,只要想清楚局部最优是什么,如果推导出全局最优,其实就够了
455.分发饼干
题目链接:https://leetcode.cn/problems/assign-cookies
文章讲解:https://programmercarl.com/0455.分发饼干.html
视频讲解:https://www.bilibili.com/video/BV1MM411b7cq
思路分析
(1)大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。
(2)这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
(3)为了更好对应大饼干喂给胃口大的小孩,先对数组排序,从数组的末尾开始遍历,外层循环遍历小孩,内层循环遍历饼干(只有满足条件才继续遍历,使用 while 循环来控制条件)
题解
java
class Solution {
public int findContentChildren(int[] g, int[] s) {
// 避免饼干浪费,大饼干先喂饱大胃口
Arrays.sort(g);
Arrays.sort(s);
int count = 0;
int start = s.length - 1;
// 遍历胃口
for (int index = g.length - 1; index >= 0; index--) {
if(start >= 0 && g[index] <= s[start]) {
start--;
count++;
}
}
return count;
}
}376. 摆动序列 ⚠️
题目链接:https://leetcode.cn/problems/wiggle-subsequence
文章讲解:https://programmercarl.com/0376.摆动序列.html
视频讲解:https://www.bilibili.com/video/BV17M411b7NS
思路分析
使用 prediff 和 curdiff 来记录当前数与其前后数的差值,根据差值是否是一正一负来判断是否符合摆动序列的条件

局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列
符合摆动的条件:(preDiff < 0 && curDiff > 0) || (preDiff > 0 && curDiff < 0)
上下坡有平坡

这种情况中,我们需要删除平坡中重复的元素,保留最后一个元素即可,即:prediff = 0 && curdiff < 0
结合之前摆动的条件,结合起来可以得到如下摆动条件:
(preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)
数组首尾两端

初始化 result = 1
具体解释
(1)为了使用先前(preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)这个条件,必须需要有三个数,这里自己添加一个平坡,结果刚好满足条件 preDiff = 0
(2)使用以上条件的逻辑是,只要满足条件,结果集就会自增一
(3)但是题目说了,当只有两个元素的时候,结果为 2,为了使得满足条件后自增一(计算了左边的峰值)的结果等于 2,所以这里初始化时,默认右边的坡度为 1,即 result = 1(默认最右端有一个峰值),遍历元素时,只遍历到倒数第二个元素即可
单调坡度有平坡

(1)prediff 和 curdiff 是在遍历过程中逐渐变化的,且 prediff 在 curdiff 之前,在这种情况中,何时更新 prediff 就决定了结果集是否正确的问题
(2)单调中的平坡不能算峰值(即摆动),如果结果集错误(prediff 时时跟着 curdiff 的变化而变化),即多计算了平坡中最后那个一元素
(3)上图情况中,实际上只有两个摆动,第一个元素和最后一个元素(对应数组首尾两端这种情况)
(3)解决方案:只有当满足条件时,才更新 prediff 的位置,prediff 只记录当坡度变化的时候,下一个坡的初始坡度,然后 prediff 就不改变了,直到遇到下一个摆动(坡度方向又改变了)再去改变 prediff,这样的好处就是不会记录平坡这种情况
题解
java
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length <= 1) {
return nums.length;
}
// 当前元素后面的那个坡度
int curDiff = 0;
// 当前元素前面的那个坡度
int preDiff = 0;
// 平坡情况时,默认最右端有一个峰值(坡度)
int count = 1;
// 最右端已经记录了一个摆动(平坡情况),遍历到倒数第二个元素
for (int i = 0; i < nums.length - 1; i++) {
// 计算当前差值
curDiff = nums[i + 1] - nums[i];
// 一正一负才能算摆动(下坡上坡 || 上坡下坡)
if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
count++;
// 只有当坡度变化时,才更新 preDiff,避免错误记录平坡的情况
preDiff = curDiff;
}
}
return count;
}
}53. 最大子序和
题目链接:https://leetcode.cn/problems/maximum-subarray
文章讲解:https://programmercarl.com/0053.最大子序和.html
视频讲解:https://www.bilibili.com/video/BV1aY4y1Z7ya
思路分析
局部最优
(1)当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小
(2)只要连续和还是正数就会对后面的元素起到增大总和的作用,所以只要连续和为正数我们就保留
(3)这相当于是暴力解法中的不断调整最大子序和区间的起始位置
举例:如果 -2 和 1 在一起,计算起点的时候,一定是从 1 开始计算,因为负数只会拉低总和,这就是贪心贪的地方
题解
java
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];
// 找到了更大的子序列和
if (count > sum) {
sum = count; // 取区间累计的最大值(相当于不断确定最大子序终止位置)
}
// 如果发现和更小了,就更新贪心的起点(跳过当前遍历的数)
if (count < 0) {
count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
}
}
return sum;
}
}输出子数组
java
class Solution {
public int maxSubArray(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
int sum = Integer.MIN_VALUE; // 存放最大子数组和
int count = 0; // 当前子数组的累加和
int start = 0; // 当前子数组的起始索引
int end = 0; // 当前子数组的结束索引
int tempStart = 0; // 临时记录子数组的起始位置
for (int i = 0; i < nums.length; i++) {
count += nums[i];
// 如果找到更大的子数组和
if (count > sum) {
sum = count;
start = tempStart; // 更新最大子数组的起始位置
end = i; // 更新最大子数组的结束位置
}
// 如果当前子数组的和变小,重置子数组的起始位置
if (count < 0) {
count = 0;
tempStart = i + 1; // 从下一个元素重新开始
}
}
// 输出最大子序列
System.out.print("Max Subarray: ");
for (int i = start; i <= end; i++) {
System.out.print(nums[i] + " ");
}
System.out.println();
return sum;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
int result = solution.maxSubArray(nums);
System.out.println("Max Sum: " + result);
}
}