1 贪心算法
贪心算法就是选择每一阶段的局部最优,从而达到全局最优。
2 分发饼干
LeetCode:分发饼干
做贪心算法的时候,往往只有一种感觉:理应如此,甚至没有意识到用了贪心算法。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
//因为满足胃口大的和胃口小都只算1,所以优先满足胃口小的
//优先用小饼干满足小胃口
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int g_index=0;
int s_index=0;
int count=0;
while(g_index<g.size() && s_index<s.size())
{
if(g[g_index]<=s[s_index])
{
++count;
++g_index;
++s_index;
}
else
{
++s_index;
}
}
return count;
}
};
3 摆动序列
LeetCode:摆动序列
摆动序列这道题可以看成一道求峰值与谷点的题目,需要考虑清楚平坡的情况,以下给出两种写法。
另外一种想法,a-b+b-c=a-c,所以删除一个数,等于摆动序列的相加,那么直接看发生多少次符号变化即可代表多大的长度。
1 峰值与谷点
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
//特殊情况
if(nums.size()==1 || (nums.size()==2 && nums[0]!=nums[1]))
return nums.size();
if(nums.size()==2 && nums[0]==nums[1])
return 1;
//利用峰谷思想,本题最终保留的是峰与谷的节点,这也是一种最长子序列
//现在需要考虑平坡的情况,因为长度为1和2的情况都已经考虑了,所以我们默认最右边的是一个峰值点/谷点
int result=1;
//平坡时我们保留最后一个点,这也就意味着我们在nums[0]前虚设一个nums[-1]==num[0]不会影响判断
//因为不保留nums[-1]
int preDiff=0;
int curDiff;
for(int i=0;i<nums.size()-1;++i)
{
curDiff=nums[i+1]-nums[i];
if((preDiff<=0 && curDiff>0) || (preDiff>=0 && curDiff<0))
{
//代表着nums[i]是一个峰点
++result;
//此时代表着前后的转向不一致了,修改preDiff
preDiff=curDiff;
}
//为什么不在此处修改preDiff?
//因为在此处修改,由于0的判断,容易对1 2 2 3这样的平坡造成误判
//我们并不需要精确的preDiff值,我们只需要记录他的符号即可,所以如果符号没有发生改变,就无需记录
}
return result;
}
};
2 删除的符号变化
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
//特殊情况
if(nums.size()==1 || (nums.size()==2 && nums[0]!=nums[1]))
return nums.size();
if(nums.size()==2 && nums[0]==nums[1])
return 1;
//现在是=>3的序列进行判断
//因为a-b+b-c=a-c
//所以删除一个数<=>差值数组相邻的数相加
//那么差值数组中所有符号相等的相邻项都应该合成为1个数
//此时就是其中一个最长的子序列
//那么意味着产生多少次的符号变化,代表着多大的长度
int start;
//找到第一个起点
for(start=1;start<nums.size();++start)
{
//找到起点
if(nums[start]!=nums[start-1])break;
}
//全部相等,返回1
if(start==nums.size())return 1;
//至少start和start-1不一样,最小长度为2
int length=2;
int pre=nums[start]-nums[start-1];
for(int i=start+1;i<nums.size();++i)
{
int cur=nums[i]-nums[i-1];
//等于0,直接略过,相当于把相等数删到只剩一个
if(cur==0)continue;
//两数不一样,代表着可以长度+1
if(cur*pre<0)
{
++length;
pre=cur;
}
}
return length;
}
};
4 最大子数组和
LeetCode:# 4 最大子数组和
好久以前就做过这道题,现在重新做了一遍,还是那句话,什么意识到自己哪里用了贪心。
这道题的精髓在于应该以当前数为主视角,看以此结尾的子数组,而不要想着维持一个区间去保证最大。
另外采用分而治之的想法,分解成小尺度的左、右以及从中间往左+从中间往右的最大子数组也很简单。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int i;
int max_val=nums[0];
int cur_sum=nums[0];
int max_sum=nums[0];
for(i=1;i<nums.size();i++)
{
//max_val用于全是负数的数组判断
max_val=max(max_val,nums[i]);
//cur_sum记录了[?,i-1]的子数组之和
//如果cur_sum<0代表着[?,i]不如nums[i]
if(cur_sum<0)
{
cur_sum=nums[i];
}
else
{
cur_sum+=nums[i];
max_sum=max(max_sum,cur_sum);
}
}
return max(max_val,max_sum);
}
};
5 买卖股票的最佳时机II
LeetCode:买卖股票的最佳时机II
更像是一道常识题,但做法可以精细化。
1 低进高出
class Solution {
public:
int maxProfit(vector<int>& prices) {
//每一次都在谷地买入,并在峰值卖出即可
//这样保证每一次的收益都最大
int result=0;
//in代表买入的时机,hold代表买入持仓
int in,start;
bool hold=false;
//找到第一次谷点,即第一次买入的时机
for(start=0;start<prices.size()-1;start++)
{
if(prices[start]<prices[start+1])
break;
}
//股票一直跌
if(start==prices.size()-1)
return 0;
//从start处开始买入
in=start;
hold=true;
for(int i=start+1;i<prices.size();i++)
{
//已经买入,寻找抛售点
if(hold)
{
//股票即将跌了,或者到最后一天了
if(i==prices.size()-1 || prices[i]>prices[i+1])
{
result+=prices[i]-prices[in];
//未持仓
hold=false;
}
}
//寻找买入点
else
{
//在明天比今天要涨的情况下,买入(最后一天不能买入)
if(i<prices.size()-1 && prices[i]<prices[i+1])
{
in=i;
hold=true;
}
}
}
return result;
}
};
2 利润拆分24h
class Solution {
public:
int maxProfit(vector<int>& prices) {
//每一次都在谷地买入,并在峰值卖出即可
//这样保证每一次的收益都最大
//同时可以一段利润可以拆分为每一天的利润累加
//那么我们只需把相邻两天是涨的值累加即可
int result=0;
for(int i=1;i<prices.size();i++)
{
//负数不加
result+=max(0,prices[i]-prices[i-1]);
}
return result;
}
};
6 跳跃游戏
LeetCode:跳跃游戏
两种思路:
1 从终点出发,判断每个点到达所需要的最小点数。如果满足,代表其他点可以通过这个点进行位移,将点数置零,从而减少最小点数需求,最后看起始位是否满足条件。
2 从起点出发,利用覆盖范围这一思想,看最终覆盖范围是否能够触及终点,如果不能,在遍历起跳点(同时更新覆盖范围)的同时,一定会出现起跳点超过覆盖范围的情况。
1 终点出发——最小点数
class Solution {
public:
bool canJump(vector<int>& nums) {
//计算到达最后一个点,每个位置所需要的最小值
int min_step=0;
for(int i=nums.size()-2;i>=0;--i)
{
++min_step;
//num[i]>=min_step,意味着可以从num[i]到末尾,那么在他之前的点只需要到num[i]即可
//能到达最后一个点的点,也一定能到达num[i]
//所以置零min_step
if(nums[i]>=min_step)
{
min_step=0;
}
}
return min_step<=nums[0];
}
};
2 起点出发——覆盖范围
class Solution {
public:
bool canJump(vector<int>& nums) {
if(nums.size()==1)return true;
//不断动态更新自己所能到达的范围,从而判断
//最开始只能到达0
int cover=0;
int i=0;
//动态更新起跳点i和覆盖范围cover
//如果cover到最后一个,true
//如果起跳点i超过从0开始的跳跃覆盖范围,说明覆盖范围已经达到最大了
while(i<=cover)
{
//更新最大覆盖范围
cover=max(cover,i+nums[i]);
if(cover>=nums.size()-1)return true;
++i;
}
//此时代表i>cover,失败了
return false;
}
};
7 跳跃游戏II
LeetCode:跳跃游戏II
还是一样的两种思路,终点出发类似于回溯,不断更新最小路径;起点出发则是利用覆盖范围,每次跨出覆盖范围意味着向前一步。
推荐使用覆盖范围。
覆盖范围
class Solution {
public:
int jump(vector<int>& nums) {
//特殊情况处理
if(nums.size()==1)return 0;
//cur_cover用于标记步数
//max_cover用于检测终点
int cur_cover=0;
int step=0;
int max_cover=nums[0];
for(int i=0;i<nums.size();++i)
{
//i>cur_cocer意味着这一步已经迈出上一步所能覆盖的范围,需要+1,即迈出了这还未到达终点的一步
//cur_cover必须用[0,i-1]的最大覆盖距离替代,而不是[0,i]的最大覆盖距离
//因为[0,i-1]的覆盖距离意味着到上一步到这一步所能覆盖的最大距离
if(i>cur_cover)
{
++step;
cur_cover=max_cover;
}
max_cover=max(max_cover,i+nums[i]);
if(max_cover>=nums.size()-1)
{
//迈出到终点的一步
++step;
break;
}
}
return step;
}
};
最小路径
class Solution {
public:
int jump(vector<int>& nums) {
if(nums.size()==0)
return 0;
//记录最短路径
vector<int> min_path;
vector<int> tmp;
min_path.push_back(nums.size()-1);
for(int i=nums.size()-2;i>=0;--i)
{
tmp.clear();
for(int j=0;j<min_path.size();++j)
{
tmp.push_back(min_path[j]);
if(min_path[j]-i<=nums[i])
{
tmp.push_back(i);
min_path=tmp;
break;
}
}
}
return min_path.size()-1;
}
};
8 K次取反后最大化的数组和
LeetCode:K次取反后最大化的数组和
思路上很简单,最小K个的负数变为正数,剩下的次数为偶数就不变,剩下的次数为奇数就变一个绝对值最小的。
在实现上有直接版与绝对值排序版,后者可以避免很多不必要的异常情况处理。
绝对值排序
class Solution {
static bool cmp(int a,int b)
{
return abs(a)>abs(b);
}
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
//按绝对值大小排序,从大到小排序
sort(nums.begin(),nums.end(),cmp);
int sum=0;
//前k个负数翻转
for(int i=0;i<nums.size();++i)
{
if(k!=0 && nums[i]<0)
{
nums[i]*=-1;
--k;
}
}
if(k%2==0)
for(int a:nums)sum+=a;
else
{
nums.back()*=-1;
for(int a:nums)sum+=a;
}
return sum;
}
};
直接版(考虑全正数与全负数的情况)
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
//把最小的负数变成正的
//如果k还有剩余,剩下的为偶数就没事,剩下的为奇数,将绝对值最小的变为负数
sort(nums.begin(),nums.end());
int count=0;
int sum=0;
//尽可能地把前k个负数变为正数
for(count=0;count<nums.size();count++)
{
if(count<k && nums[count]<0)
{
nums[count]*=-1;
}
else
{
break;
}
}
if(count==k)
{
for(int a:nums)sum+=a;
}
//偶数
else if((k-count)%2==0)
{
for(int a:nums)sum+=a;
}
//奇数
else
{
//count同时是负数的个数,如果全是负数
if(count==nums.size())
nums.back()*=-1;
//全是正数
else if(count==0)
{
nums.front()*=-1;
}
else
{
//有正有负,最小正在count,最大负count-1
if(nums[count]<nums[count-1])
nums[count]*=-1;
else
nums[count-1]*=-1;
}
for(int a:nums)sum+=a;
}
return sum;
}
};
9 总结
贪心实在是没什么规律可言,感觉就是一个正常的脑回路,难不成我的思想天生就是局部最优+贪心?
——2023.2.26