27.贪心(1) | 分发饼干、摆动序列、最大子序和(h)

        今天是贪心算法的第1天,但题目做的却比较艰难。学到的贪心算法的套路就是没有套路,每一道题都可能是新的套路。过于详细的理论推导没有必要,举例验证即可。贪心题目大多是会就会,不会就不会的,所以在做的时候也不用太纠结,不会的话去看题解就行。


        第1题(455. 分发饼干)相对容易,自己实现的贪心策略是从优先用小饼干满足小胃口,分发时从剩余饼干中挑尽可能小的。所以对应的实现方法是对胃口和饼干都进行排序,之后用两个指针分别遍历胃口和饼干,不断分发并统计个数,直至其中一个遍历结束。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int indG = 0, indS = 0;
        int ans = 0;
        while (indG < g.size() && indS < s.size()) {
            if (s[indS] >= g[indG]) {
                ++indG, ++indS;
                ++ans;
            }
            else {
                ++indS;
            }
        }
        return ans;
    }
};

        从题解中了解到,也可以优先分发大饼干给大胃口。这种方式中,主体不再是胃口,而是饼干。当前饼干满足当前胃口时,两者下标都右移一位;否则,饼干下标右移一位。所以饼干下标无论如何都要右移,只有遇到前者的情况胃口下标才右移。最后,胃口下标右移的个数也就是得到饼干的人数。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int indG = 0;
        for (int indS = 0; indS < s.size(); ++indS) {
            if (indG < g.size() && s[indS] >= g[indG]) {
                ++indG;
            }
        }
        return indG;
    }
};

        但其实以胃口为主体也可以的。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int indS = 0;
        int ans = 0;
        for (int indG = 0; indG < g.size(); ++indG) {
            while (indS < s.size() && s[indS] < g[indG]) {
                ++indS;
            }
            if (indS < s.size() && s[indS] >= g[indG]) {
                ++ans;
                ++indS;
            }
        }
        return ans;
    }
};

        第2题(376. 摆动序列)贪心的思路是去掉非山峰/山谷点,但实际上只需要统计下山峰/山谷点个数。贪心策略比较简单,但实现并不容易。自己就陷入了误区,认为需要设立前继点pre,用来保存上一个山峰/山谷。然后当遇到下一个山峰(上一个为山谷时),或下一个山谷(上一个为山峰时)时,更新pre为山峰/山谷。首先,自己判断山峰的依据是当前数字大于pre,判断山谷的依据是当前数字小于pre。然而,如果上一个转折点是山谷,谷点数字被保存为pre,那么遇到比pre大的数字并不能说明就是山峰,只能说明比山谷大,它之后可能紧跟更大的数字;反之,如果上一个转折点是山峰,峰点数字被保存为pre,那么遇到比pre小的数字也不能说明就是山谷,只能说明比山峰小,它之后可能紧跟更小的数字。所以这样的思路是错误的。

        正确的思路应该是pre就用于保存与当前节点相邻的上一个节点,通过与当前节点比大小而确定pre是山峰还是山谷。还需要一个bool变量用于保存之前的转折点是山峰还是山谷,用来确定当前遇到山峰还是山谷时停止。此外,还有一个坑点在于数组一开始可能是一段相同的数字,所以要将这一段首先遍历结束。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() == 1) {
            return 1;
        }
        // 处理开头的多个相等数字
        bool inc;
        int ind = 0;
        while (ind < nums.size() && nums[ind] == nums[0]) {
            ind++;
        }
        // 如果全部相等
        if (ind >= nums.size()) {
            return 1;
        }
        // 初始化
        int ans = 1;
        if (nums[ind] > nums[0]) {
            inc = true;
            ans++;
        }
        else {
            inc = false;
            ans++;
        }
        int pre = nums[ind];
        ind++;
        // 非开头相等部分
        while (ind < nums.size()) {
            if (nums[ind] > pre && !inc) {
                ++ans;
                inc = true;
            }
            else if (nums[ind] < pre && inc) {
                ++ans;
                inc = false;
            }
            pre = nums[ind]; // 重点
            ind++;
        }
        return ans;
    }
};

        题解的思路要简洁很多,贪心策略同样是只统计转折点数量。利用当前元素与上一个元素数值的差值,即当前差值curDiff,以及上一个差值preDiff来判断是否到达山峰/山谷。具体来说,上一个差值为负数或0,当前差值为正数的话就说明当前点是山峰;上一个差值为正数或0,当前差值为负数的话就说明当前点是山谷。重点有2个操作:

  • 数组的首尾不好处理。按照题目要求,2个不同数字也算是长度为2的摆动序列。所以为了统一计算,将上一个差值初始化为0,即相当于在第一个数字前添加了与其相等的数字。这样一来,无论刚开始是上升还是下降,都会判断为转折点。然后,又因为最后一个点的curDiff无法计算且总是要判定为一个转折点,所以不对其进行计算,并将答案初始化为1;
  • 仅在遇到山峰或山谷时,才更新上一个差值;而当前差值在每个点都要计算。因为只有遇到山峰/山谷时,下一个转折点的判定条件才发生变化,因为像一些平路之后上升/下降趋势保持不变的数组,比如[1,2,2,3],如果每次都更新上一个差值,那么在平路的下一个点处,由于上一个差值被更新为0,那么就会对结果就会进行原本不该的+1。而如果只在转折处才更新上一个差值,就不会被更新为0,就避免了多余的+1。
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int preDiff = 0, curDiff;
        int ans = 1;
        for (int i = 0; i < nums.size() - 1; ++i) {
            curDiff = nums[i + 1] - nums[i];
            if (curDiff > 0 && preDiff <= 0 || 
                curDiff < 0 && preDiff >= 0) {
                ans++;
                preDiff = curDiff;
            }
        }
        return ans;
    }
};

        题解针对这道题还有DP解法。dp矩阵的行数与nums数组长度一致,列数为2。dp[i][0]代表对于nums的前i个数字,将第i个数字认定为山谷的答案(即最长摆动子序列的长度);dp[i][1]则对应将第i个数字认定为山峰的答案。而对于一些在山腰的数字,虽然在最优解下是被认定为山腰的,但这并不妨碍它可以被认定为山峰/山谷,只不过得到的不是最优解,最坏情况下,它被认定为山峰/山谷后得到的答案是1。

        dp的第i行无法直接根据第i - 1行而更新,而是要从第0行遍历到第i - 1行,取其中与“当前数字被认定为山峰/山谷”这一行为所匹配的数字对应的dp数值 + 1,作为dp第i行的取值。具体来说,如果nums[j](j < i)比nums[i]大,那么dp[i][0]就可以更新为max(dp[i][0], dp[j][1] + 1)。

        而初始化方面,因为题目说一个数字也是1个摆动序列,所以对应的最小值为1,所以就需将dp的第0行2个数字都初始化为1,并在遍历nums时,每次都将nums[i]的各数字也都初始化为1。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int dp[nums.size()][2]; // 第0列代表认为当前行对应数字是山谷,第2列代表···山峰
        dp[0][0] = 1, dp[0][1] = 1;
        for (int i = 1; i < nums.size(); ++i) {
            dp[i][0] = 1, dp[i][1] = 1;
            for (int j = 0; j < i; ++j) {
                if (nums[i] < nums[j]) {
                    dp[i][0] = max(dp[i][0], dp[j][1] + 1);
                }
                if (nums[i] > nums[j]) {
                    dp[i][1] = max(dp[i][1], dp[j][0] + 1);
                }
            }
        }
        return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]);
    }
};

         这样做的时间复杂度是O(n²),效率较低,不如贪心解法。

        二刷:用dire表示前面是上升还是下降序列(-1下降,0不变,1上升),起始由于不论前面是山峰还是山谷,一旦遇到第一个与num[0]不相等的数字,都可将ans + 1,所以将dire初始化为0。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int ans = 1, dire = 0;
        for (int i = 1; i < nums.size(); ++i) {
            if (nums[i] > nums[i - 1] && dire <= 0) {
                dire = 1;
                ans++;
            }
            else if (nums[i] < nums[i - 1] && dire >= 0) {
                dire = -1;
                ans++;
            }
        }
        return ans;
    }
};

        第3题(53. 最大子序和)属实不会了,直接看了题解。贪心的思路在于一旦sum小于0,就抛弃掉之前的序列并重新开始计数,因为如果不抛弃的话,前面sum已经为负数的序列就会成为累赘,不如直接从下一个数重新开始计数,相当于之前的sum从负数变为了0。

        一个问题是如果前面序列的sum为负数,但其中后半部分的sum是为正的,那么岂不是会错误地丢掉后面的这一部分?在这种情况下,当遍历到前半部分结束时,sum就已经变为负数,前面部分就已经被丢弃掉了。而后面的部分因为sum大于0,所以也会被保留。所以最终不会出现上面的情况。

        循环中操作的顺序也很重要,应该首先计算sum,再更新ans,最后再更新sum为0(或不更新)。这样一来,即便nums中全是负数,也会得到正确的答案。

        另外,ans的初始化也很重要。可以初始化为INT_MIN,或者nums[0],否则如果nums中全是负数的话,还会出错。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int sum = 0, ans = nums[0]; // nums[0]也可以初始化为INT_MIN
        for (int n : nums) {
            sum += n;
            ans = max(ans, sum);
            if (sum < 0) {
                sum = 0;
            }
        }
        return ans;
    }
};

        这道题同样有DP解法。dp数组的长度与nums一致,dp[i]的含义是,以第i个数作为末尾的序列中,最大的子数组和。所以答案并不是dp的最后一位,而是dp所有数字中最大的一个。已知了将前一位作为末尾的最大子数组和,想要得到以当前位作为末尾的最大子数组和,只有2种选择,即保留前一位的最大子数组并加上当前数字,或者抛弃掉前一位的最大子数组,将当前数字当作新的最大子数组。所以dp更新方式为dp[i] = max(dp[i - 1] + nums[i], nums[i])。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int dp[nums.size()];
        dp[0] = nums[0];
        int ans = nums[0]; // nums[0]也可以初始化为INT_MIN
        for (int i = 1; i < nums.size(); ++i) {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

        二刷:忘记方法;应该先进行sum += x,再更新ans,最后再进行sum = max(sum, 0)。如果先将sum跟0比较并更新,再更新ans的话,会导致答案应该为负数时,输出为0。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值