一、贪心算法理论基础
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
贪心的套路:无固定的套路而言,关键在于如何通过局部最优达到全局最优?如果找不到合适的反例去反驳,就可以用贪心试试,但不需要进行理论的证明。
面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了。刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
贪心一般解题步骤:
贪心算法一般分为如下四步:
将问题分解为若干个子问题。
找出适合的贪心策略。
求解每一个子问题的最优解。
将局部最优解堆叠成全局最优解。
做题的时候,只要想清楚局部最优是什么,如果推导出全局最优,其实就够了。
二、分发饼干
链接:力扣
描述:假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
思路如下:
按照贪心的思路去理解,需要先将两个数组进行排序,大体积的饼干先满足给大胃口的孩子吃,这就是局部最优,找不出明显的反例,就可以推出全局最优,利用一个count来进行计数即可。
代码如下:
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s)
{
int count=0;//记录可以满足的孩子数
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int i = g.size()-1;//遍历孩子数组
int j = s.size() - 1;//遍历饼干数组
for (i; i >=0; i--)
{
if(j >= 0 && g[i] <= s[j])
{
count++;
j--;
}
}
return count;
}
};
运行如下:
三、摆动序列
链接:力扣
描述:如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
思路如下:
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度),这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点
在计算是否有峰值的时候,大家知道遍历的下标 i ,计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i]),如果prediff < 0 && curdiff > 0
或者 prediff > 0 && curdiff < 0
此时就有波动就需要统计。
本题要考虑三种情况:
- 情况一:上下坡中有平坡
- 情况二:数组首尾两端
- 情况三:单调坡中有平坡
对于情况一:
序列长度应该是3,而不是2。
对于情况二,数组首尾两端
本题统计峰值的时候,数组最左面和最右面如何统计呢?
题目中说了,如果只有两个不同的元素,那摆动序列也是 2。
例如序列[2,5],如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。
因为在计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i])的时候,至少需要三个数字才能计算,而数组只有两个数字。如何和我们的判断规则结合在一起呢?
可以假设,数组最前面还有一个数字,那这个数字应该是该数组第一个数字相同。
对于情况三,单调坡度有平坡。
更新 prediff 需要注意:
我们只需要在这个坡度摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。
代码如下:
class Solution {
public:
int wiggleMaxLength(vector<int>& nums)
{
if (nums.size() <= 1)
{
return nums.size();
}
int prediff = 0;//计算前一个坡度
int nextdiff = 0;//计算后一个坡度
int result = 1;//默认最右边有一个坡度
for (int i = 0; i < nums.size()-1; i++)
{
nextdiff = nums[i + 1] - nums[i];
if (prediff <= 0 && nextdiff > 0 || prediff >= 0 && nextdiff < 0)
{
result++;
prediff = nextdiff;
}
}
return result;
}
};
运行如下:
四、最大子数组和
链接:力扣
描述:给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
思路如下:
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
全局最优:选取最大“连续和”
局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
从代码角度上来讲:遍历 nums,从头开始用 sum累积,如果 sum一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 sum了,因为已经变为负数的 sum,只会拖累总和。需要用一个变量result来记录最大的和。
在使用贪心算法求解本题,是遇到负数就选择起始位置,还是连续和为负选择起始位置呢?
是后者的情况才去调整。
其实并不会,因为还有一个变量result一直在更新最大的连续和,只要有更大的连续和出现,result
就更新了。
代码如下:
class Solution {
public:
int maxSubArray(vector<int>& nums)
{
int sum = 0;//记录序列的和
int result = INT32_MIN;//随时更新和最大的子序列
for (int i = 0; i < nums.size(); i++)
{
sum += nums[i];
result = sum > result ? sum : result;
if (sum < 0)
{
sum = 0;//相当于是出现了负数和,直接从下一个数重新开始计数
}
}
return result;
}
};
运行如下: