代码随想录算法训练营第27天 | LeetCode455.分发饼干、LeetCode376.摆动序列、LeetCode53.最大子数组和

目录

LeetCode455.分发饼干

LeetCode376.摆动序列

1. 贪心算法

2. 动态规划

LeetCode53.最大子数组和

1. 贪心算法

2. 动态规划


LeetCode455.分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

思路:这里的分发饼干用到了贪心算法思想。

涉及到贪心,那就需要先搞明白,如何能够局部最优如何通过局部最优找到整体最优

这里的局部最优可以看为大尺寸饼干分给大胃口孩子,小尺寸饼干分给小胃口孩子,这样能够保证局部最优,依据这个思路继续往下分,最后就能使得尽可能多的满足越多数量的孩子。

当然了,这里同样有细节需要注意,for循环里面的变量究竟是用饼干还是孩子胃口尺寸呢?

这可得分清楚,并不是都可以,要依据具体情况具体分析。

如果是首先使得大尺寸饼干满足大胃口孩子,那么就是for循环中变量为孩子的胃口大小,饼干尺寸和孩子胃口从后往前遍历,当然了,首先得排序

    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());//将孩子胃口进行排序
        sort(s.begin(), s.end());//将饼干尺寸大小进行排序
        int count = 0;//记录满足孩子的个数
        int sIndex = s.size() - 1; //指向饼干的最大值
        for(int i = g.size() - 1; i >= 0; i --){
            if(sIndex >= 0 && s[sIndex] >= g[i]){//首先应该判断下标是否大于等于0
                count ++;
                sIndex --;
            }
        }
        return count;
    }

当首先满足小尺寸饼干先满足小胃口孩子时,for循环变量就是饼干的尺寸了,从前往后依次判断增加。

    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());//将孩子胃口进行排序
        sort(s.begin(), s.end());//将饼干尺寸大小进行排序
        int gIndex = 0; //指向饼干的最小值
        for(int i = 0; i < s.size(); i ++){//注意这里是循环的饼干尺寸
            if(gIndex < g.size() && s[i] >= g[gIndex]){//首先应该判断下标是否小于s的最大下标
                gIndex ++;
            }
        }
        return gIndex;
    }

有人会问,为什么不能反过来呢?

其实可以手动模拟一下,如果是先满足大尺寸饼干满足大胃口孩子,让饼干最为for循环变量,就会看到,可能存在这样的情况,也就是说饼干最大的尺寸没有办法使得最大胃口的孩子满足,于是for循环不断跳过,但是本身饼干的尺寸已经排好序了,越往前只会越来越小,根本不可能满足,结果就会造成一个也满足不了,但是其实饼干尺寸是能够满足一些孩子的胃口的,所以这样就出错了。比如饼干尺寸排序后为[5,6,7,8],孩子胃口大小为[6,7,8,9]。

小尺寸饼干满足小胃口孩子也是同样的道理,所以这里可得小心,到底是使用哪种思路做。

如果不明白的话,建议使用上面例子在纸上画一画就明了了。

时间复杂度:O(nlogn)

空间复杂度:O(1)

LeetCode376.摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

  • 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 最长子序列的长度

思路:求摆动序列,贪心可以做,动态规划也可以做。

1. 贪心算法

首先我们要明白最后求的是最长的摆动序列,求的是长度

于是这里有三种情况需要考虑:

1、上下坡有平坡;

2、首尾结点处理;

3、单调序列有平坡

首先第一种,上下坡有平坡,也就是说从一个点开始,先往上走,然后平着走,最后往下走,可以想象有多个点,比如[1,2,2,2,1]这样的;如果进行统计的话,所求长度就是为3,那如何求呢?

首先可以看到2-1>0,2-2=0,这是前面三个数的情况;

最后的时候有2-2=0,2-1>0,这是最后三个数的情况。

前面的设为prediff,后面的设为curdiff(都是差值的意思),这样当prediff>=0,cur<0或者pre<=0,cur>0的时候,可以知道能够加1,这是第一种情况;

对于第二种情况,首尾结点,比如[1,2],我们可以直观看到,最后结果为2,但是如何代码实现呢?

我们假设在1前面还有一个1,所以prediff=0,cur>0,这样就可以加1了,但是最终结果为2,所以我们初始的时候就设置result为1,这样最终结果的时候就能得到最终结果。

现在将第一种情况和第二种情况结合来看,就知道为什么需要prediff=0了,因为本身存在prediff>0,curdiff<0或者prediff<0,curdiff>0就记录一个点,但是这里是prediff>=0,cur<0或者pre<=0,cur>0的时候开始统计,目的就是为了方便计数,进行统计。

当然了,只有上面两种情况仍然是不够的,还有一种情况,也就是单调递增的时候有平坡,比如[1,2,2,2,2,3,5],如果按照上面的情况,在统计完后,将prediff更新,就会发现统计的个数会多1,就是因为不管满不满足统计的条件,跳出了判断后,都会将prediff更新,这会导致增减态势没有发生变化,但是还是加了1,所以这里我们只在增减的态势发生了变化后再更新pre,如果没有发生改变,那么就不增加,也不更新prediff。

这样局部求序列长度,最后将整体序列遍历完成后,就能得到最终的最长摆动序列大小。

    int wiggleMaxLength(vector<int>& nums) {
        if(nums.size() == 1 ) return 1;//这里是元素只有一个的时候
        int prediff = 0;//记录前一个差值
        int curdiff = 0;//记录目前的差值
        int result = 1;//记录最终结果
        for(int i = 0; i < nums.size() - 1; i ++){
            curdiff = nums[i + 1] - nums[i];

            if((prediff >= 0 && curdiff < 0) || (prediff <= 0 && curdiff > 0)){
                result ++;
                prediff = curdiff;//这里只能在坡度有变化的时候才能更新prediff,否则会统计失误
            }
        }
        return result;
    }

时间复杂度:O(n)

空间复杂度:O(1)

2. 动态规划

当然,这里还可以使用动态规划的方法,使用额外的数组dp[i][1]表示前i个元素,使得第i个元素作为谷底的情况;dp[i][0]表示前i个元素,使得第i个元素作为谷峰的情况(这里的谷底谷峰指的是变化趋势,比如在纸上画出[1,3,2],1、2为谷底,3为谷峰)。

对于每一个元素来说,它们既可以自己作为一个统计所需要的谷底谷峰,又可以接在其他元素之后,所以其初始值dp[i][0],dp[i][1]都为1,都有机会统计到最终结果。

这里使用了两层循环,第一层循环是为了遍历每一个元素,第二层循环就是为了判断这个元素作为谷底还是谷峰所能够贡献的最大值,当循环结束后,取dp[nums.size()-1][0],dp[nums.size()-1][1]两数中的最大值,就是最终结果,因为dp[nums.size()-1]已经将前面以及自身的元素的情况统计完毕了,要找的就是最大的那种情况。

    int dp[1005][2];
    int wiggleMaxLength(vector<int>& nums) {
        memset(dp, 0, sizeof(dp));//将dp数组初始化为0
        dp[0][0] = dp[0][1] = 1;
        for(int i = 1; i < nums.size(); i ++){
            dp[i][0] = dp[i][1] = 1;//每个结点作为山峰和山顶都可能成为一个峰值
            for(int j = 0; j < i; j ++){
                if(nums[j] > nums[i]) dp[i][1] = max(dp[i][1], dp[j][0] + 1);//当前面的比i大时,i会作为山谷
            }
            for(int j = 0; j < i; j ++){
                if(nums[j] < nums[i]) dp[i][0] = max(dp[i][0], dp[j][1] + 1);//当前面的比i小时,i会作为山顶
            }
        }
        return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]);
        //这里返回的是最后一个元素作为山谷或山顶时能够取到的最大值
        //经过前面的更新,最后一个元素所存储的元素中最大值即为最终结果
    }

时间复杂度:O(n^2)

空间复杂度:O(n)

LeetCode53.最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。 

思路:这里同样是可以选择贪心算法以及动态规划。

1. 贪心算法

首先需要明白,这里的子数组是连续的,也就是说,从一个元素出发,依次遍历。

于是结束条件是什么呢?

当累加元素和小于0时,也就是说前面加的序列的和没办法做到最大了,于是就跳过,从下一个元素开始,累加值同时归0,开始重新累加,每次记录到最大值,都会更新最终结果。

这样局部求最大,当全局遍历完成后,也就是求到了整体最大。

    int maxSubArray(vector<int>& nums) {
        int result = INT32_MIN;
        int count = 0;
        for(int i = 0; i < nums.size(); i ++){
            count += nums[i];
            if(count > result) result = count;//更新最大值
            if(count < 0) count = 0;
            //当count小于了0,说明当前的nums[i]加上后使得前面的值小于了0,
            //所以开始从nums[i+1]开始,count等于0重新开始计数
        }
        return result;
    }

时间复杂度:O(n)

空间复杂度:O(1)

2. 动态规划

这里的动态规划的状态转移方程是取dp[i-1]+nums[i],与nums[i]中的最大一个,dp[i]就是前面i个元素的最大子数组和的大小,如果说,加上了nums[i]开始小于了nums[i]的大小,那么前i个元素中,能取到的最大和就是nums[i]的大小。 当然,每次做完后需要更新一下最终结果,方便结束之后返回。

于是这样遍历到最后,result中就记录了整个序列能够取到的最大子数组和,返回即可。

    int maxSubArray(vector<int>& nums) {
        if(nums.size() == 0) return 0;
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        int result = dp[0];
        for(int i = 1; i < nums.size(); i ++){
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);//状态转移方程
            if(dp[i] > result) result = dp[i];//更新最大值
        }
        return result;
    }

时间复杂度:O(n)

空间复杂度:O(n)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值