[日记]LeetCode算法·十三——贪心①

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值