代码随想录算法训练营第31天 | 贪心算法 part01
455.分发饼干
● 376. 摆动序列
● 53. 最大子序和
题目一 455.分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
- 输入: g = [1,2,3], s = [1,1]
- 输出: 1 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以你应该输出 1。
注意贪心的想法是局部最优,推出整体最优。
因此为了满足更多的小孩,就不要造成饼干尺寸的浪费。
大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
可以尝试使用贪心策略,先将饼干数组和小孩数组排序。
然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
本题的关键是让饼干等人,外层遍历人,而只让饼干在该移动的时候移动。
比如饼干是9,那么9先不动,匹配人的10,不成功则移动人,移动到7则满足,然后再移动饼干和人。
因此外层循环应当是人(从最大开始遍历),内层循环为饼干,在特定情况下才移动。
public int findContentChildren(int[] g, int[] s)
{
Arrays.sort(g);
Arrays.sort(s);
int glen = g.length - 1;//人
int slen = s.length - 1;//饼干
int ans = 0;//喂饱多少个人
for(int i= glen; i>=0; i--)
{
if(slen >= 0 && s[slen] >= g[i])//满足条件匹配成功
{
slen--;
ans++;
}
}
return ans;
}
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
那么可不可以 先外层遍历饼干,再内层遍历胃口(人)呢?其实不可以的。
外面的 for 是里的下标 i 是固定移动的,而 if 里面的下标 index 是符合条件才移动的。
如果 for 控制的是饼干, if 控制胃口,就是出现如下情况 :
if 里的 index 指向 胃口 10, for 里的 i 指向饼干 9,因为 饼干 9 满足不了 胃口 10,所以 i 持续向前移动,而 index 走不到s[index] >= g[i] 的逻辑,所以 index 不会移动,那么当 i 持续向前移动,最后所有的饼干都匹配不上。
所以 一定要 for 控制 胃口,里面的 if 控制饼干。
也可以换一个思路,小饼干先喂饱小胃口.注意前提是满足尽可能多的孩子,因此这种方法也成立。
// 思路1:优先考虑饼干,小饼干先喂饱小胃口
public int findContentChildren(int[] g, int[] s)
{
Arrays.sort(g);
Arrays.sort(s);
int start = 0;
int count = 0;
for (int i = 0; i < s.length && start < g.length; i++) {
if (s[i] >= g[start]) {
start++;
count++;
}
}
return count;
}
题目二 376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。
通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
- 输入: [1,7,4,9,2,5]
- 输出: 6
- 解释: 整个序列均为摆动序列。
将整个数组的值抽象为一个折线图,那么我们的全局最优是去掉折线中间上升和下降的部分元素,只留下峰值和峰谷。
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
局部最优推出全局最优,并举不出反例,那么试试贪心!
这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点
对于本题的每一个节点,我们都需要获取它左右相邻的节点,才能得到差值,进而判断差值正负性。
题目中说了,如果只有两个不同的元素,那摆动序列也是 2。
例如序列[2,5],如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。
在计算 prediff 和 curdiff的时候,至少需要三个数字才能计算,而数组只有两个数字。
这里可以写死,就是 如果只有两个元素,且元素不同,那么结果为 2。
在计算是否有峰值的时候,大家知道遍历的下标 i ,然后后减去前,
计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i]),
如果prediff < 0 && curdiff > 0
(先下后上)或者 prediff > 0 && curdiff < 0
(先上后下),此时就有波动就需要统计。
然后就是对特殊情况进行讨论。
情况一:上下坡中有平坡
针对中间平坡的情况,可以约定只留下左边或只留下右边。相当于对平坡两端的特殊情况,有一端可以开个口子,保留一种情况。
情况二:数组首尾两端的判定
如果只有俩个元素,直接判定为2;
对于最左/右的元素直接在两端再加上一个相同的元素,变成平坡,重复情况一的判断。
针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了,
前面一端是平的,即默认 preDiff = 0,
针对以上情形,result 初始为 1(默认最右面有一个峰值),
此时 curDiff > 0 && preDiff <= 0,那么 result++(计算了左面的峰值),
最后得到的 result 就是 2(峰值个数为 2 即摆动序列长度为 2)
情况三:单调坡中有平坡
正常的平坡我们需要保存一个值,但是如果平坡在一个单调区间内,那么保存的这个点也得删掉。
代码实现时注意遍历时只用遍历到倒数第二个就行(因为要考虑前后)。
随着遍历时i的移动,可以直接将curdiff赋值给prediff,同样能实现功能。
public int wiggleMaxLength(int[] nums)
{
if(nums.length <= 1)
return nums.length;
int len = nums.length;
int cur = 0, pre = 0;
int ans = 1;//默认值,右边算一个
for(int i=0 ;i<len - 1; i++)//遍历到倒数第二个
{
cur = nums[i+1] - nums[i];
if( (pre <= 0 && cur > 0) || (pre >= 0 && cur < 0))
{
ans++;
pre = cur;
}
}
return ans;
}
只需要在 坡度 摆动变化的时候,更新 prediff 就行,而不是每次循环都更新。
这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
题目三 53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
- 输入: [-2,1,-3,4,-1,2,1,-5,4]
- 输出: 6
- 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
可以用暴力,但是会超时
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
第一层 for 设置起始位置,第二层 for 循环从某一位置出发遍历数组后面的部分,不断相加,寻找最大值
public int maxSubArray(int[] nums)
{
int ans = -999999; //min
int count = 0;
int len = nums.length;
for(int i=0;i< len; i++)
{
count = 0;
for(int j=i; j<len; j++)
{
count += nums[j];
ans = count > ans ? count : ans ;
}
}
return ans;
}
使用贪心算法,
局部最优:只要正数的那部分,遇到负数直接去掉,然后接着继续.
当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
整体最优:最大连续和
从代码角度上来讲:遍历 nums,从头开始用 count 累积,
如果 count 一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 count 了,因为已经变为负数的 count,只会拖累总和。
这相当于是暴力解法中的不断调整最大子序和区间的起始位置。
某个区间的终止位置,其实就是如果 count 取到最大值了,及时用ans记录下来。之后不断更新各区间和之间的最大者,得到最终结果。
public int maxSubArray(int[] nums)
{
int ans = -999999; //最小,min
int count = 0;
int len = nums.length;
for(int i=0;i< len; i++)
{
count += nums[i];
if(count > ans)
{
ans = count;//某个区间的最大
}
if(count <= 0)
count = 0;//小了就reset
}
return ans;
}