1. 贪心解题步骤
一般四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
贪心没有套路,说白了就是常识性推导加上举反例。
2. 455. 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
2.1 思路
-
将饼干和胃口数组排序
-
从后向前遍历胃口,用大饼干优先满足大胃口,并统计满足胃口数量
-
也可以小饼干先喂饱小胃口
2.2 先考虑胃口
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int result=0;
int start=s.length-1;//饼干数组的下标
//遍历胃口
for (int index = g.length-1; index >=0 ; index--) {
//饼干≥胃口就输出
if (start>=0&&g[index]<=s[start]) {//饼干
start--;
result++;
}
}
return result;
}
}
2.3 先考虑饼干
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int result=0;
int start=0;
//遍历饼干
for (int i = 0; i < s.length && start < g.length; i++) {
if (s[i]>=g[start]) {
start++;
result++;
}
}
return result;
}
}
3. 376. 摆动序列
3.1 思路
本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
相信这么一说吓退不少同学,这要求最大摆动序列又可以修改数组,这得如何修改呢?来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢?用示例二来举例,如图所示:
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
局部最优推出全局最优,并举不出反例,那么试试贪心!
(为方便表述,以下说的峰值都是指局部峰值)
实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了,(相当于是删除单一坡度上的节点,然后统计长度)这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点。
实现技巧
本题代码实现中,还有一些技巧,例如统计峰值的时候,数组最左面和最右面是最不好统计的。
例如序列[2,5],它的峰值数量是2,如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。
所以可以针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即preDiff = 0
,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tru4Kiyo-1688046832784)(E:\系列笔记\算法笔记\随想录训练营\image\贪心算法_part01_峰值.jpg)]
针对以上情形,
- result初始为1(默认最右面有一个峰值),此时
curDiff > 0 && preDiff <= 0
,那么result++(计算了左面的峰值),最后得到的result就是2(峰值个数为2即摆动序列长度为2;
本题要考虑三种情况:
-
上下坡中有平坡
-
数组首尾两端
-
单调坡中有平坡(很难想到)
很容易忽略了一种情况,即 如果在一个单调坡度上有平坡,例如[1,2,2,2,3,4],如图:
图中,我们可以看出,版本一的代码在三个地方记录峰值,但其实结果因为是 2,因为 单调中的平坡 不能算峰值(即摆动)。
之所以版本一会出问题,是因为我们实时更新了 prediff。
那么我们应该什么时候更新 prediff 呢?
只需要在 这个坡度 摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。
for (int i = 0; i < nums.length - 1; i++) { cur=nums[i+1]-nums[i]; if ((pre>=0&&cur<0)||(pre<=0&&cur>0)) { result++; pre=cur;//写到if里面,坡度 摆动变化的时候,更新 pre 就行 } }
3.2 代码实现
具体实现思路:
首先如果nums数组的长度为0或者为1,就直接返回该数组的长度就可以了;
nums数组的长度≥2的前提下,进入以下逻辑:
- 首先定义初始值
- preDiff = 0, 相当于在数组nums最前面,加上了一个与nums[0]大小相同的元素;
- curDiff也初始化为0,
- result=1,默认序列最右边有一个峰值;
- 然后开始遍历数组,pre和cur相乘为负数,就说明他们异号,满足序列的要求;
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length<=1) {
return nums.length;
}
int pre=0;//初始前一对差值为0
int cur=0;//当前差值
int result=1;//记录峰值个数,序列默认序列最右边有一个峰值
//因为默认最右边有一个峰值,所以遍历只遍历length-1个
for (int i = 0; i < nums.length - 1; i++) {
cur=nums[i+1]-nums[i];
if ((pre>=0&&cur<0)||(pre<=0&&cur>0)) {
result++;
pre=cur;//写到if里面,坡度 摆动变化的时候,更新 pre 就行
}
}
return result;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(1),该函数使用了3个额外的整型变量,它们的空间复杂度都是O(1)
4. 53. 最大子数组和
4.1 方法1:暴力求解
class Solution {
public int maxSubArray(int[] nums) {
int result = Integer.MIN_VALUE;
int count = 0;
for (int i = 0; i < nums.length; i++) {
count = 0;
for (int j = i; j < nums.length; j++) {
count += nums[j];
result = count > result ? count : result;
}
}
return result;
}
}
4.2 方法2:贪心算法
4.2.1 思路
贪心贪在哪?
如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方!
局部最优推出全局最优?
- 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
- 全局最优:选取最大“连续和”
- 局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
在代码中要如何实现?
从代码角度上来讲:
- 遍历nums,从头开始用count累积
- 如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积count了
- 因为已经变为负数的count,只会拖累总和。
- 这相当于是暴力解法中的不断调整最大子序和区间的起始位置;
- 每次取count为正数的时候,开始一个区间的统计
区间终止位置不用调整么? 如何才能得到最大“连续和”呢?
如果count取到最大值了,及时记录下来!这样相当于是用 result 记录最大子序和区间和(变相的算是调整了终止位置)。
if (count > result) result = count;
4.2.2 代码实现
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;
}
}
- 时间复杂度:O(N); 需要遍历数组一遍
- 空间复杂度:O(1);常数个变量记录
for循环里还可以用以下代码:
class Solution {
public int maxSubArray(int[] nums) {
int maxSum = Integer.MIN_VALUE;
int curSum = 0;
for (int num : nums) {
curSum += num;
maxSum = Math.max(maxSum, curSum);//更新最大和
curSum = Math.max(curSum, 0);//将负值变为0,重新开始计算子序和
}
return maxSum;
}
}
4.2.3 总结
-
最大和maxSum初始值设为
Integer.MIN_VALUE
,是为了保证计算出的子序和curSum一定大于等于maxSum所设置的值,从而保证得到正确的结果。考虑一下,如果maxSum的初始值为0,而数组中所有的元素都是负数,那么遍历数组后maxSum的值仍然为0,结果就不正确了。如果将maxSum的初始值设为负无穷,或者比数组中所有元素的最小值还小一些的值,可以保证curSum在遍历过程中取到正数时,可以正确地更新maxSum。
-
要注意起始位置和终止位置如何确定
- 起始位置:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”
- 终止位置:如果count取到最大值了,及时记录下来!这样相当于是用 result 记录最大子序和区间和(变相的算是调整了终止位置)。
考虑一下,如果maxSum的初始值为0,而数组中所有的元素都是负数,那么遍历数组后maxSum的值仍然为0,结果就不正确了。如果将maxSum的初始值设为负无穷,或者比数组中所有元素的最小值还小一些的值,可以保证curSum在遍历过程中取到正数时,可以正确地更新maxSum。
- 要注意起始位置和终止位置如何确定
- 起始位置:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”
- 终止位置:如果count取到最大值了,及时记录下来!这样相当于是用 result 记录最大子序和区间和(变相的算是调整了终止位置)。