贪心算法理论基础
- 贪心没有套路,说白了就是常识性推导加上举反例。
- 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
- 面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了。刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
455. 分发饼干
一、做题感受&第一想法
贪心:把小饼干分给小孩子,大饼干分给大孩子
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int num = 0;
int i = 0,j = 0;
while(i < g.size() && j < s.size()){
if(s[j] >= g[i]){ //分发
num ++;
i++;
j++;
}
else if(s[j] < g[i]){
j++; //看下一个小饼干
}
}
return num;
}
};
二、学习文章后收获
1.其他代码
大饼干给大胃口,让大胃口先吃。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int index = s.size() - 1; // 饼干数组的下标
int result = 0;
for (int i = g.size() - 1; i >= 0; i--) { // 遍历胃口
if (index >= 0 && s[index] >= g[i]) { // 遍历饼干
result++;
index--;
}
}
return result;
}
};
2.一个可能犯的错
- 从小往大遍历 时,饼干和胃口怎么遍历都可以。
- 从大往小遍历时,不能for循环遍历饼干,index控制胃口。否则可能被大胃口卡住!
376. 摆动序列
一、做题感受&第一想法
- 错误思路:从第一个元素开始,寻找后续数组中首个大于第一个元素的元素,并把它记录为cur;之后寻找后续数组中首个小于cur的元素……统计得到的序列长度
- 错误原因:会错过很多“小波动”(可以想象一下,如果是[1,2,5,4,5,4,5,4],第二个元素2是数组倒数第二小元素,上述方法就会一直等待一个比2小的元素来形成摆动,但是这样的话后续的所有波动就都记录不了了)
- 所以,不能单纯地把“第一个出现波动的元素”作为判断基准!
记录一下错误代码(仅记录,这代码是错的!)
//仅记录,这是错误代码!!
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int result = 0,cur = nums[0],totalSize = 1;
int i = 1;
while(i < nums.size()){
while(i < nums.size() && cur <= nums[i]){ //找到第一个比cur小的元素
cout << "1" <<endl;
i++;
}
if(i < nums.size() && cur > nums[i]){
cur = nums[i];
cout << nums[i] <<endl;
totalSize++;
i++;
}
while(i < nums.size() && cur >= nums[i]){ //找到第一个比cur大的元素
i++;
}
if(i < nums.size() && cur < nums[i]){
cur = nums[i];
cout << nums[i] <<endl;
totalSize++;
i++;
}
}
result = max(result,totalSize);
cout << "total:" << totalSize <<endl;
cur = nums[0];totalSize = 1;i = 1;
while(i < nums.size()){
while(i < nums.size() && cur >= nums[i]){ //找到第一个比cur大的元素
// cout << "2" <<endl;
i++;
}
// cout << "2.05" <<endl;
if(i < nums.size() && cur < nums[i]){
// cout << "2.1" <<endl;
cur = nums[i];
cout << nums[i] <<endl;
totalSize++;
i++;
}
while(i < nums.size() && cur <= nums[i]){ //找到第一个比cur小的元素
i++;
}
if(i < nums.size() && cur > nums[i]){
cur = nums[i];
cout << nums[i] <<endl;
totalSize++;
i++;
}
}
cout << "total:" << totalSize <<endl;
result = max(result,totalSize);
return result;
}
};
二、学习文章后收获
1.贪心:思路分析
-
“折线图”思想:可以把求“摆动序列”看成求折线图的转折点。
- 只要出现了一个转折点,就出现了一个“子摆动序列”!
- 而没有转折的地方(上坡、下坡),不可能出现“子摆动序列”
-
关于“折线法”需要设定的(边界)条件
- 针对转折点:找到一个“子摆动序列”,计数器++,相当于把坡顶点(局部最大值/最小值)加入了摆动序列
- 针对开头元素:开头第一个“非平坡元素”,它天然地是摆动序列的元素([1,1,2,4,3]中的第二个1)
- 针对结尾元素: 结尾元素天然地是摆动序列的元素([1,1,2,4,3]中的3)
-
参数说明:
- curDiff:nums[i+1] - nums[i],也就是后一个元素减去该元素。
- preDiff:nums[i] - nums[i-1],也就是本元素减前一个元素。
- 初值设定:preDiff = 0(可认为开头元素前有一个等值的虚拟元素,也就是一个虚拟的平坡,这样程序正好会把开头第一个“非平坡元素”识别成摆动序列的一部分。preDiff只有在最一开始为0),curDiff = 0(这个无所谓)
- 参数变化:每个结点都计算自己的curDiff。只有当curDiff不为0且和preDiff一正一负(或者preDiff为0而curDiff不为0时,也就是刚经过开头)时,才把curDiff赋值给preDiff,更新坡度变化。(其实我们只关注preDiff的正负,因为它标明了当前/平坡之前,坡度是上坡还是下坡。)
-
关于“折线法”需要考虑清楚的三种情况
- 上坡->下坡:(如[3,4,2]中的4处)curDiff 和preDiff符号不同(一正一负)
- 上坡->平坡->下坡:(如[2,3,3,3,1]中的最后一个3处)平坡处curDiff为0。下坡来临时,curDiff 和preDiff符号不同(一正一负)
- 上坡->平坡->上坡:(如[1,3,3,3,4])无转折点!!不加入摆动序列。
2.贪心:代码
- 卡哥的代码:
class Solution {
public:
int wiggleMaxLength(vector<int>& nums){
int result = 0;
int preDiff = 0, curDiff = 0;
for(int i = 0;i < nums.size() - 1;i++){
curDiff = nums[i+1] - nums[i];
if((preDiff >= 0 && curDiff < 0) || (preDiff <= 0 && curDiff > 0)){
preDiff = curDiff;
result++;
}
}
result++;
return result;
}
};
- 另:我从评论区看到的方法:用数组记录前后的差值(也就是用数组表示两个元素之间是上坡下坡还是平坡,其实和卡哥的思路是一样的,只是实现上可以一次处理完整个数组)
3.动态规划(还没学,后续补上)
53. 最大子数组和
一、做题感受&第一想法
没有好想法。
二、学习文章后收获
1.“最大子数组和”问题思路
- 贪心:如果已经收集的元素和sum是负的,那“不如不加”!直接重新从新元素开始收集!
- 参数含义:
- sum:元素和
- result:已经找到的元素和的最大值,初值为INT_MIN
2.代码
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT_MIN;
int sum = 0;
for(int i = 0;i < nums.size();i++){
if(sum < 0){
sum = nums[i];
}
else{
sum += nums[i];
}
result = max(result,sum);
}
return result;
}
};
3.动态规划
可以用动态规划,但是此处不写,后续博客会再练习到这题。
4.我个人的一些小思考
当sum为负数时,就从新收集元素,这难道不怕前一个元素是正数吗?
答:不会出现这样的情况。如果前一个元素是正数,而加了这个正数之后sum为负,说明在前一个元素(正数)时sum就已经为负了,于是会重新收集,矛盾。
所以“前一个元素是正数”的假设不成立!
三、过程中遇到的问题
1.INT_MIN的头文件
#include <limits.h>