算法学习【贪心算法】(困难)


无重叠区间

题目

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

示例 1:

  • 输入: [ [1,2], [2,3], [3,4], [1,3] ]
  • 输出: 1
  • 解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

  • 输入: [ [1,2], [1,2], [1,2] ]
  • 输出: 2
  • 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

分析

究竟是按照右边界排序,还是按照左边界排序呢?

这其实是一个难点!

按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。

按照左边界排序,就要从右向左遍历,因为左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。

如果按照左边界排序,还从左向右遍历的话,其实也可以,逻辑会有所不同。

一些同学做这道题目可能真的去模拟去重复区间的行为,这是比较麻烦的,还要去删除区间。

题目只是要求移除区间的个数,没有必要去真实的模拟删除区间!

我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了

此时问题就是要求非交叉区间的最大个数。

右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。

局部最优推出全局最优,试试贪心!

代码

class Solution {
private:
    static bool cmp(const vector<int>& a, const vector<int>& b) &#123;
        if(a[0]==b[0]) return a[1]<b[1];
        return a[0] < b[0];
    &#125;
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) &#123;
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int result = 1; // points 不为空至少需要一支箭
        for (int i = 1; i < intervals.size(); i++) &#123;
            if (intervals[i][0] >= intervals[i - 1][1]) &#123;  // 气球i和气球i-1不挨着,注意这里不是>=
                // 需要一支箭
                result++;
            &#125;
            else &#123;  // 气球i和气球i-1挨着
                intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界
            &#125;
        &#125;
        return intervals.size()-result;
    &#125;
&#125;;

划分字母区间

题目

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

  • 输入:S = “ababcbacadefegdehijhklij”
  • 输出:[9,7,8] 解释: 划分结果为 “ababcbaca”, “defegde”, “hijhklij”。 每个字母最多出现在一个片段中。 像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。

提示:

  • S的长度在[1, 500]之间。
  • S只包含小写字母 ‘a’ 到 ‘z’ 。

思路

在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。

可以分为如下两步:

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

代码

class Solution &#123;
public:
    vector<int> partitionLabels(string S) &#123;
        int hash[27] = &#123;0&#125;; // i为字符,hash[i]为字符出现的最后位置
        for (int i = 0; i < S.size(); i++) &#123; // 统计每一个字符最后出现的位置
            hash[S[i] - 'a'] = i;
        &#125;
        vector<int> result;
        int left = 0;
        int right = 0;
        for (int i = 0; i < S.size(); i++) &#123;
            right = max(right, hash[S[i] - 'a']); // 找到字符出现的最远边界
            if (i == right) &#123;
                result.push_back(right - left + 1);
                left = i + 1;
            &#125;
        &#125;
        return result;
    &#125;
&#125;;

合并区间

题目

给出一个区间的集合,请合并所有重叠的区间。

示例 1:

  • 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
  • 输出: [[1,6],[8,10],[15,18]]
  • 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

  • 输入: intervals = [[1,4],[4,5]]
  • 输出: [[1,5]]
  • 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
  • 注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。

代码

class Solution &#123;
private:
    static bool cmp(const vector<int>&a,vector<int>&b)&#123;
        if(a[0]==b[0]) return a[1]<b[1];
        return a[0]<b[0];
    &#125;
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) &#123;
        vector<vector<int>> result;
        if(intervals.size()==0) return intervals;
        sort(intervals.begin(),intervals.end(),cmp);
        for(int i=1;i<intervals.size();i++)&#123;
            if(intervals[i][0]>intervals[i-1][1])&#123;
                result.push_back(intervals[i-1]);
            &#125;
            else&#123;
                intervals[i][0] = min(intervals[i][0],intervals[i-1][0]);
                intervals[i][1] = max(intervals[i][1],intervals[i-1][1]);          
            &#125;
        &#125;
        result.push_back(intervals[intervals.size()-1]);
        
        return result;
    &#125;
&#125;;

单调递增的数字

题目

给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。

(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)

示例 1:

  • 输入: N = 10
  • 输出: 9

示例 2:

  • 输入: N = 1234
  • 输出: 1234

示例 3:

  • 输入: N = 332
  • 输出: 299

说明: N 是在 [0, 10^9] 范围内的一个整数。

代码

class Solution &#123;
public:
    int monotoneIncreasingDigits(int n) &#123;
        string num=to_string(n);
        int flag=num.length();
        for(int i=num.length()-1;i>=1;i--)&#123;
            if(num[i]<num[i-1])&#123;
                flag=i;
                num[i-1]--;
            &#125;
        &#125;
        for(int i=0;i<num.length();i++)&#123;
            if(i>=flag)&#123;
                num[i]='9';
            &#125;
        &#125;
        return stoi(num);
    &#125;
&#125;;

买卖股票的最佳时机含手续费

题目

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1: 输入: prices = [1, 3, 2, 8, 4, 9], fee = 2 输出: 8

解释: 能够达到的最大利润: 在此处买入 prices[0] = 1 在此处卖出 prices[3] = 8 在此处买入 prices[4] = 4 在此处卖出 prices[5] = 9 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

注意:

  • 0 < prices.length <= 50000.
  • 0 < prices[i] < 50000.
  • 0 <= fee < 50000.

思路

买卖股票的最佳时机II中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。

而本题有了手续费,就要关系什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以手续费的情况。

如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。

此时无非就是要找到两个点,买入日期,和卖出日期。

  • 买入日期:其实很好想,遇到更低点就记录一下。
  • 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。

所以我们在做收获利润操作的时候其实有三种情况:

  • 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
  • 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
  • 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)

从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!

代码

class Solution &#123;
public:
    int maxProfit(vector<int>& prices, int fee) &#123;
        int price=0;
        int minprice=prices[0];
        for(int i=0;i<prices.size();i++)&#123;
            if(prices[i]<minprice)&#123;
                minprice=prices[i];
            &#125;
            if(prices[i]>=minprice&&prices[i]<=minprice+fee)&#123;
                continue;
            &#125;
            if(prices[i]>minprice+fee)&#123;
                price+=prices[i]-minprice-fee;
                minprice=prices[i]-fee;
            &#125;
        &#125;
        return price;
    &#125;
&#125;;

监控二叉树

一道值得寻味的题

class Solution &#123;
private:
    int result;
    int traversal(TreeNode* cur) &#123;

        // 空节点,该节点有覆盖
        if (cur == NULL) return 2;

        int left = traversal(cur->left);    // 左
        int right = traversal(cur->right);  // 右

        // 情况1
        // 左右节点都有覆盖
        if (left == 2 && right == 2) return 0;

        // 情况2
        // left == 0 && right == 0 左右节点无覆盖
        // left == 1 && right == 0 左节点有摄像头,右节点无覆盖
        // left == 0 && right == 1 左节点有无覆盖,右节点摄像头
        // left == 0 && right == 2 左节点无覆盖,右节点覆盖
        // left == 2 && right == 0 左节点覆盖,右节点无覆盖
        if (left == 0 || right == 0) &#123;
            result++;
            return 1;
        &#125;

        // 情况3
        // left == 1 && right == 2 左节点有摄像头,右节点有覆盖
        // left == 2 && right == 1 左节点有覆盖,右节点有摄像头
        // left == 1 && right == 1 左右节点都有摄像头
        // 其他情况前段代码均已覆盖
        if (left == 1 || right == 1) return 2;

        // 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解
        // 这个 return -1 逻辑不会走到这里。
        return -1;
    &#125;

public:
    int minCameraCover(TreeNode* root) &#123;
        result = 0;
        // 情况4
        if (traversal(root) == 0) &#123; // root 无覆盖
            result++;
        &#125;
        return result;
    &#125;
&#125;;

文章作者: Turboost Chen
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Turboost Chen !
  目录