贪心->算法实现

综述

分发饼干:leetcode455
摆动序列:leetcode376
最大子数组和:leetcode53
买卖股票的最佳时机 II:leetcode122
跳跃游戏:leetcode55
跳跃游戏 II:leetcode45
K 次取反后最大化的数组和:leetcode1005
加油站:leetcode134
分发糖果:leetcode135
柠檬水找零:leetcode860
根据身高重建队列:leetcode406
用最少数量的箭引爆气球:leetcode452
无重叠区间:leetcode435
划分字母区间:leetcode763
合并区间:leetcode56
单调递增的数字:leetcode738
监控二叉树:leetcode968

引言

贪心的本质是选择每一阶段的局部最优,从而达到全局最优
贪心算法并没有固定的套路,所以建议直接刷题
刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。如果用不了贪心,就考虑动态规划

贪心算法一般分为如下四步:
将问题分解为若干个子问题
找出适合的贪心策略
求解每一个子问题的最优解
将局部最优解堆叠成全局最优解
这四部有点理论,刷题中有时候不会根据这四步来,所以直接刷题,刷题时只需要考虑局部最优是什么,全局最优是什么即可

分发饼干

题目

leetcode455
在这里插入图片描述

题解

一眼贪心算法,局部最优:给大胃口先分配大饼干,外层遍历胃口g,内层遍历饼干s(这里的内循环并不是真的 for 循环,而是满足一定条件进行++或- -),如下图
在这里插入图片描述

如果是小胃口先分配小饼干,外层遍历胃口g,内层遍历饼干s,那么会出错,如下图:
在这里插入图片描述

或者 局部最优:将小饼干分配给小胃口,外层遍历饼干s,内层遍历胃口g
在这里插入图片描述

同样的,局部最优:将大饼干分配给大胃口,外层遍历饼干s,内层遍历胃口g,就是错的

第一种情况的代码:

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(s.begin(), s.end());//先排序
        sort(g.begin(), g.end());//先排序
        int j = s.size() - 1;
        int res = 0;
        for (int i = g.size() - 1; i >= 0; i--) {//外层胃口,倒序
            if (j >= 0 && s[j] >= g[i]) {//每层饼干,倒序
                res++;
                j--;
            }
        }
        return res;
    }
};

摆动序列

题目

leetcode376
在这里插入图片描述

题解

贪心

这道题就是贪心直接上,尽量有更多的波峰和波谷,(数组的两端也可以称之为波峰和波谷,比如[1,3,2,4] 中1是波峰,4是波谷)
但是需要注意两个情况:

  1. 波动段有平台,需要在前面加等号
    在这里插入图片描述
  2. 过渡段有平台,需要在波动时才更新 prediff
    在这里插入图片描述

代码:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() == 1) return 1;
        int prediff = nums[1] - nums[0];
        int curdiff = 0;
        int res = (prediff == 0 ? 1 : 2);

        for (int i = 2; i < nums.size(); i++) {
            curdiff = nums[i] - nums[i - 1];
            if ((prediff <= 0 && curdiff > 0) || (prediff >= 0 && curdiff < 0)) {//加等号是因为波动段有平台
                res++;
                prediff = curdiff;//只有波动时才更新prediff,防止过度段有平台
            }
        }

        return res;
    }
};

动态规划

这道题用贪心很容易想到,但是很难一下子就做出来,这时候其实可以想想用动态规划
其实本题就是求 波峰的数量 和 波谷的数量 的最大值。(数组的两端也可以称之为波峰和波谷,比如[1,3,2,4] 中1是波峰,4是波谷)
记 上升摆动子序列为 整体是摆动的,但是最后一段是上升的
记 下降摆动子序列为 整体是摆动的,但是最后一段是下降的
在这里插入图片描述

那么定义一个二维数组 dp,第一行是上升摆动子序列的长度,第二行是下降摆动子序列的长度
所以 dp[i][0] 表示 以 nums[0] ~ nums[i] 的上升摆动子序列的长度
dp[i][1] 表示 以 nums[0] ~ nums[i] 的下降摆动子序列的长度

所以递推公式:
nums[i] > nums[i - 1] 时,
上升摆动子序列 的长度会是 下降摆动子序列的长度+1,并且和之前的上升摆动子序列的长度对比
下降摆动子序列 的长度不变
nums[i] < nums[i - 1] 时,
上升摆动子序列 的长度不变
下降摆动子序列 的长度是 上升摆动子序列的长度+1,并且和和之前的下降摆动子序列的长度对比
nums[i] = nums[i - 1] 时,
上升摆动子序列 的长度不变
下降摆动子序列 的长度不变
因此递推公式:

if (nums[i] > nums[i - 1]) {
    dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + 1);
    dp[i][1] = dp[i - 1][1];
} else if (nums[i] < nums[i - 1]) {
    dp[i][0] = dp[i - 1][0];
    dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + 1);
} else {
    dp[i][0] = dp[i - 1][0];
    dp[i][1] = dp[i - 1][1];
}

初始化:dp[0][0] = 1; dp[0][1] = 1;,指的是当只有一个元素时,上升摆动子序列 和 下降摆动子序列 长度都是1

代码:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        //dp数组第一行是上升摆动子序列,第二行是下降摆动子序列
        vector<vector<int>> dp(nums.size(), vector<int>(2, 1));
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] > nums[i - 1]) {
                dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + 1);
                dp[i][1] = dp[i - 1][1];
            } else if (nums[i] < nums[i - 1]) {
                dp[i][0] = dp[i - 1][0];
                dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + 1);
            } else {
                dp[i][0] = dp[i - 1][0];
                dp[i][1] = dp[i - 1][1];
            }
        }

        return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]);
    }
};

最大子数组和

题目

leetcode53
在这里插入图片描述

题解

一眼贪心,只要之前的和是正数,那么加上 nums[i] 可能会更大
只要之前的和是负数,那么加上 nums[i] 一定更小,所以直接从 nums[i] 开始算

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int add = 0;
        int res = INT_MIN;
        for (int i = 0; i < nums.size(); i++) {
            if (add < 0) add = 0;
            add += nums[i];
            res = max(res, add);
        }
        return res;
    }
};

买卖股票的最佳时机 II

题目

leetcode122
在这里插入图片描述

题解

贪心,前一天价格 比 后一天少,就可以前一天买,后一天卖
否则就不操作

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int res = 0;
        for (int i = 0; i < prices.size() - 1; i++) {
            if (prices[i] < prices[i + 1]) res += (prices[i + 1] - prices[i]);
        }
        return res;
    }
};

跳跃游戏

题目

leetcode55
在这里插入图片描述

题解

别管怎么跳,只要跳跃覆盖范围可以覆盖到终点即可

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;
        for (int i = 0; i <= cover; i++) {
            cover = max(cover, i + nums[i]);
            if (cover >= nums.size() - 1) return true;//覆盖最后一个元素 nums.size() - 1
        }
        return false;
    }
};

跳跃游戏 II

题目

leetcode45
在这里插入图片描述

题解

在这里插入图片描述
第一步到达的范围是 curDistance
第二步到达的范围是 nextDistacne,需要一直更新 nextDistacne
如果 i 遍历到了 curDistance,说明需要再加一步了,再加一步之后的 curDistance 就是 nextDistacne

class Solution {
public:
    int jump(vector<int>& nums) {
        if (nums.size() == 1) return 0;
        int curDistance = 0;
        int nextDistance = 0;
        int res = 0;
        for (int i = 0; i < nums.size(); i++) {
            nextDistance = max(nextDistance, i + nums[i]);
            if (i == curDistance) {
                res++;
                curDistance = nextDistance;
                if (nextDistance >= nums.size() - 1) return res;//由于上来在curDistacne=0时res就+1了,所以返回时不用再+1了
            }
        }
        return 0;
    }
};

更简单的是用动态规划
dp[i] 表示 到达 i 所需的最小步数
那么遍历到 i 时,从 i 到 i + nums[i] 中的步数都可以由 dp[i] + 1 得到,但是需要取最小,所以递推公式:for (int j = i; j <= i + nums[i] && j < nums.size(); j++) dp[j] = min(dp[j], dp[i] + 1);

class Solution {
public:
    int jump(vector<int>& nums) {
        vector<int> dp(nums.size(), INT_MAX);
        dp[0] = 0;
        for (int i = 0; i < nums.size(); i++) {
        	//只要是覆盖范围内的步数都可以由dp[i]+1得到,但是要取最小
            for (int j = i; j <= i + nums[i] && j < nums.size(); j++) dp[j] = min(dp[j], dp[i] + 1);
        }
        return dp[nums.size() - 1];
    }
};

动态规划简单易懂,但是时间复杂度是 o(n^2),而贪心算法时间复杂度是 o(n)

加油站

题目

leetcode134
在这里插入图片描述

题解

每个加油站的剩余量rest[i]为gas[i] - cost[i]
i 从0开始累加 rest[i],和记为 curSum,一旦 curSum 小于零,说明 [0, i] 区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从 i+1 算起,再从0计算curSum
在这里插入图片描述

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curSum = 0;
        int totalSum = 0;
        int start = 0;
        for (int i = 0; i < gas.size(); i++) {
            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];
            if (curSum < 0) {   // 当前累加rest[i]和 curSum一旦小于0
                start = i + 1;  // 起始位置更新为i+1
                curSum = 0;     // curSum从0开始
            }
        }
        if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了
        return start;
    }
};

分发糖果

题目

leetcode135
在这里插入图片描述

题解

一定要先确定一遍再确定另一边,如果同时考虑,会顾此失彼
一次是从左到右遍历,只比较右边孩子评分比左边大的情况,遇到则比左边的大1个,这样保证了右边比左边大的情况是成立的
一次是从右到左遍历,只比较左边孩子评分比右边大的情况,遇到则比右边的大1个,这样保证了左边比右边大的情况是成立的
那么既符合 左边 > 右边 又符合 右边 > 左边,那么应该去两者的最大值

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> leftRate(ratings.size(), 1);
        vector<int> rightRate(ratings.size(), 1);

        //从前向后,找到右边比左边大的,然后=左边的+1
        for (int i = 1; i < ratings.size(); i++) {
            if (ratings[i] > ratings[i - 1]) rightRate[i] = rightRate[i - 1] + 1;
        }
        
        //从后向前,找到左边比右边大的,然后=右边+1
        for (int i = ratings.size() - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) leftRate[i] = leftRate[i + 1] + 1;
            leftRate[i] = max(leftRate[i], rightRate[i]);//两边要同时满足,需要取max
        }

        //上一个从后向前遍历时未遍历最后一个元素,因此最后一个元素需要取max
        leftRate[ratings.size() - 1] = max(leftRate[ratings.size() - 1], rightRate[ratings.size() - 1]);

        int sum = 0;
        for (int ele : leftRate) sum += ele;
        return sum;
    }
};

根据身高重建队列

题目

leetcode406
在这里插入图片描述

题解

这道题也有两个维度,一个是 h,一个是 k,所以应该先处理一个维度,再处理另一个维度
先对身高进行排序,从大到小排序,这样身高的排序维度就处理完了
然后根据 k 进行插入,对于每一个 people[i], 它的 ki 就是前面的比它高的数量,因此,插入到第 ki 的位置即可。
因为本来就是身高从大到小排列的,那么小的身高插入到大身高前面,也不会影响大身高前面的比他身高大的数量,也就是说和大身高的 ki 仍然相等
在这里插入图片描述

代码:

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);
        vector<vector<int>> que;
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1];
            que.insert(que.begin() + position, people[i]);
        }
        return que;
    }
};

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

这种的话是 vector 频繁的插入,十分影响效率(因为底部扩容)。对于插入频繁的情况,用 list 会更高效:

class Solution {
public:
    // 身高从大到小排(身高相同k小的站前面)
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1]; // 插入到下标为position的位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());
    }
};

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

虽然两个的时间复杂度是一样的,但是执行时间差别很大

用最少数量的箭引爆气球

题目

leetcode452
在这里插入图片描述

题解

从此题开始,接下来的三道题都是和 “重复区间” 相关

局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少
为了让气球尽可能的重叠,需要对数组进行排序
其实按照左边界排序和有边界排序都可以
如果面试时发现代码不能通过,尝试换一下排序的方式或许可以(比如从左边界排序改为有边界排序)

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end(), pointSort);
        int res = 1;
        int arrow = points[0][1];
        for (int i = 1; i < points.size(); i++) {
            //认为箭就是区间的end,由于points经过了Xend的排序,因此每次射出去如果在相应气球的区间,则能穿破,不在区间则再申请一支箭
            if (arrow < points[i][0] || arrow > points[i][1]) {
                arrow = points[i][1];
                res++;
            }
        }

        return res;
    }


private:
    //根据Xend从小到大排序
    static bool pointSort(vector<int>& p1, vector<int>& p2) {
        if (p1[1] == p2[1]) return p1[0] < p2[0];
        return p1[1] < p2[1];
    }
};

这是按照右边界从小到大排序的,如果右边界相等,按照左边界从小到大排序

时间复杂度:O(nlog n),因为有一个快排
空间复杂度:O(1),有一个快排,最差情况(倒序)时,需要n次递归调用。如果加上系统栈空间的话需要O(n)的栈空间

无重叠区间

题目

leetcode435
在这里插入图片描述

题解

“重复区间” 问题,需要排序
此处按照右边界进行排序
在这里插入图片描述

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), intervalSort);

        int border = intervals[0][1];//记录边界
        int res = 0;

        for (int i = 1; i < intervals.size(); i++) {
            if (border > intervals[i][0]) res++;//如果边界 > 新区间的起始位置,说明区间重叠,那么将此区间删除
            else border = intervals[i][1];//如果边界 <= 新区间的起始位置,说明不重叠,更新边界
        }

        return res;
    }


private:
    static bool intervalSort(vector<int>& p1, vector<int>& p2) {
        if (p1[1] == p2[1]) return p1[0] < p2[0];
        return p1[1] < p2[1];
    }
};

时间复杂度:O(nlog n) ,有一个快排
空间复杂度:O(n)

合并区间

题目

leetcode56
在这里插入图片描述

题解

“重复区间” 问题,需要排序
此处按照左边界进行排序

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), mergeSort);

        int start = intervals[0][0];
        int end = intervals[0][1];
        vector<vector<int>> res;

        for (int i = 1; i < intervals.size(); i++) {
            if (intervals[i][0] > end) {//说明不重叠,那么就将结果加入res中,并更新start和end
                res.push_back({start, end});
                start = intervals[i][0];
                end = intervals[i][1];
            } else {//说明重叠,更新start和end
                start = start;//这个可以不要
                end = max(end, intervals[i][1]);
            }
        }

        //看最后一个start 和 end 是否被插入进去了
        if (res.empty() || end != res[res.size() - 1][1]) res.push_back({start, end});

        return res;
    }


private:
    static bool mergeSort(vector<int>& a, vector<int>& b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] < b[0];
    }
};

时间复杂度: O(nlogn)
空间复杂度:O(n)

划分字母区间

题目

leetcode763
在这里插入图片描述

题解

如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了
所以需要
统计每一个字符最后出现的位置
从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

class Solution {
public:
    vector<int> partitionLabels(string s) {
        unordered_map<char, int> umap;
        
        //找每个字符的最远位置下标
        for (int i = 0; i < s.size(); i++) {
            umap[s[i]] = i;
        }
        
        vector<int> res;
        int startIndex = 0;//子串的开始位置
        int endIndex = 0;//遍历的子串中出现的字符的最远位置下标
        
        for (int i = 0; i < s.size(); i++) {
            endIndex = max(endIndex, umap[s[i]]);
            if (i == endIndex) {
                res.push_back(endIndex - startIndex + 1);
                startIndex = endIndex + 1;
            }
        }

        return res;
    }
};

时间复杂度:O(n)
空间复杂度:O(n)

当然可以把unordered_map 换成 int hash[27];
每个字母可以这样取:hash[s[i] - 'a'] = ...;
这样的话空间复杂度是 o(1)

单调递增的数字

题目

leetcode738
在这里插入图片描述

题解

这道题是贪心算法题,但是中间有很多操作很技巧,非常值得一做
对于数字876,需要从后往前遍历,发现后一位“6”小于前一位“7”,那么后一位变成9,前一位减1,即869
继续遍历,发现后一位“6”小于前一位“8”,那么后一位变成9,前一位减1,即799
所以整体过程是从后往前遍历,找到最前面的 “后一位小于前一位” 的那个下标,然后将其后面的所有数字换成9即可

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        //将n转换成string,方便对位数操作
        string nums = to_string(n);

        //贪心算法,遇到高位比地位小的,那低位变9,高位--。一直找到最高位,赋值给flag,将flag之后的全部置为9
        int flag = nums.size();
        for (int i = nums.size() - 1; i > 0; i--) {
            if (nums[i] < nums[i - 1]) {
                flag = i;
                nums[i - 1]--;
            }
        }

        //通过nums中flag之后的数字置为9      
        for (int i = flag; i < nums.size(); i++) {
            nums[i] = '9';
        }

        return stoi(nums);
    }
};

时间复杂度:O(n)
空间复杂度:O(n)

这道题有个很技巧的操作就是将 n 转成 字符串string,这样操作很方便,后续可以再将其转成int

监控二叉树

题目

leetcode968
在这里插入图片描述

题解

每个节点三种状态,0是未覆盖,1是覆盖,2是装摄像头
尽量不往叶子节点放摄像头,而是在叶子节点的父节点放摄像头,所以需要后续遍历,直接跳过叶子节点,给父节点放摄像头。如果是前序遍历是控制不了的
由于叶子节点不放摄像头,叶子节点的父节点放摄像头,所以空节点认为是覆盖状态,因为:
空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。

class Solution {
public:
    //0是未覆盖,1是覆盖,2是装摄像头
    int minCameraCover(TreeNode* root) {
        if (traversal(root) == 0) res++;
        return res;
    }


private:
    int res = 0;

    int traversal(TreeNode* root) {
        if (root == nullptr) return 1;//空间点认为是覆盖了,这样就是叶子节点不会装摄像头,而是从叶子节点的父节点开始装摄像头
        int leftSate = traversal(root->left);
        int rightSate = traversal(root->right);

        //几种状态,可以动手画画
        if (leftSate == 0 || rightSate == 0) {
            res++;
            return 2;
        } else if (leftSate == 2 || rightSate == 2) {
            return 1;
        } else return 0;
    }
};

时间复杂度: O(n),需要遍历二叉树上的每个节点
空间复杂度: O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值