代码随想录1刷—贪心算法篇(二)

452. 用最少数量的箭引爆气球

局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。

如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。

但仔细思考一下就发现:为了让气球尽可能的重叠,需要对数组进行排序。如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remote气球,只要记录一下箭的数量就可以了。

如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭

注意题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆,、所以代码中 if (points[i][0] > points[i - 1][1]) 不能是>=

452.用最少数量的箭引爆气球
class Solution {
private:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        return a[0] < b[0];
    }
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if(points.size() == 0)
            return 0;
        sort(points.begin(), points.end(), cmp);
        int result = 1;
        for(int i = 1; i < points.size(); i++){
            if(points[i][0] > points[i-1][1]){   //气球i和i-1不挨着
                result++;
            }else{
                points[i][1] = min(points[i-1][1],points[i][1]);    //更新重叠气球的最小右边界
            }
        }
        return result;
    }
};

435. 无重叠区间

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

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

一些同学做这道题目可能真的去模拟去重复区间的行为,这是比较麻烦的,还要去删除区间。题目只是要求移除区间的个数,没有必要去真实的模拟删除区间!按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。此时问题就是要求非交叉区间的最大个数。

435.无重叠区间
class Solution {
private:
    static bool cmp(vector<int>& a,vector<int>& b){
        return a[1] < b[1]; //按右边界从小到大排序
    }
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.size() == 0)
            return 0;
        sort(intervals.begin(),intervals.end(),cmp);
        int count = 1;  //记录非交叉区间的个数
        int end = intervals[0][1];  //记录区间的分割点
        for(int i = 1;i< intervals.size();i++){
            if(end <= intervals[i][0]){
                end = intervals[i][1];
                count++;
            }
        }
        return intervals.size() - count;
    }
};
//用452的思路也是一样的,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了

class Solution {
public:
    // 按照区间右边界排序
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[1] < b[1];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);

        int result = 1; // points 不为空至少需要一支箭
        for (int i = 1; i < intervals.size(); i++) {
            if (intervals[i][0] >= intervals[i - 1][1]) {
                result++; // 需要一支箭
            }
            else {  // 气球i和气球i-1挨着
                intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); 
                						// 更新重叠气球最小右边界
            }
        }
        return intervals.size() - result;
    }
};

763. 划分字母区间

方法一

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

可以分为如下两步:

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

763.划分字母区间

class Solution {
public:
    vector<int> partitionLabels(string s) {
        int hash[26] = {0};
            //hash每个格子代表一个字母
            //注意,申请hash[26]那数组的下标就是0-25,a对应下标0的位置
            //为了防止数组越界 一般会多开一格作为保护 所以开hash[27]比较合适
        for(int i = 0;i < s.size();i++){
            hash[s[i]-'a'] = i;
        }
        vector<int> result;
        int left = 0,right = 0;
        for(int i = 0;i < s.size();i++){
            right = max(right,hash[s[i]-'a']);
            if(i == right){
                result.push_back(right-left+1);
                    //返回一个表示每个字符串片段的长度的列表
                left = i+1;
            }
        }
        return result;
    }
};
方法二

提供一种与 452.用最少数量的箭引爆气球 、435.无重叠区间 相同的思路。

统计字符串中所有字符的起始和结束位置,记录这些区间(实际上也就是 435.无重叠区间 题目里的输入),将区间按左边界从小到大排序,找到边界将区间划分成组,互不重叠。找到的边界就是答案。

class Solution {
public:
    static bool cmp(vector<int> &a, vector<int> &b) {
        return a[0] < b[0];
    }
    // 记录每个字母出现的区间
    vector<vector<int>> countLabels(string s) {
        vector<vector<int>> hash(26, vector<int>(2, INT_MIN));
        vector<vector<int>> hash_filter;
        for (int i = 0; i < s.size(); ++i) {
            if (hash[s[i] - 'a'][0] == INT_MIN) {
                hash[s[i] - 'a'][0] = i;
            }
            hash[s[i] - 'a'][1] = i;
        }
        // 去除字符串中未出现的字母所占用区间
        for (int i = 0; i < hash.size(); ++i) {
            if (hash[i][0] != INT_MIN) {
                hash_filter.push_back(hash[i]);
            }
        }
        return hash_filter;
    }
    vector<int> partitionLabels(string s) {
        vector<int> res;
        // 这一步得到的 hash 即为无重叠区间题意中的输入样例格式:区间列表
        // 只不过现在我们要求的是区间分割点
        vector<vector<int>> hash = countLabels(s);
        // 按照左边界从小到大排序
        sort(hash.begin(), hash.end(), cmp);
        // 记录最大右边界
        int rightBoard = hash[0][1];
        int leftBoard = 0;
        for (int i = 1; i < hash.size(); ++i) {
            // 由于字符串一定能分割,因此,
            // 一旦下一区间左边界大于当前右边界,即可认为出现分割点
            if (hash[i][0] > rightBoard) {
                res.push_back(rightBoard - leftBoard + 1);
                leftBoard = hash[i][0];
            }
            rightBoard = max(rightBoard, hash[i][1]);
        }
        // 最右端
        res.push_back(rightBoard - leftBoard + 1);
        return res;
    }
};

56. 合并区间

按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了;

整体最优:合并所有重叠的区间。

56.合并区间
class Solution {
public:
    // 按照区间左边界从小到大排序
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0];
    }
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> result;
        if (intervals.size() == 0) return result;
        sort(intervals.begin(), intervals.end(), cmp);
        
        bool flag = false; // 标记最后一个区间有没有合并
        int length = intervals.size();

        for (int i = 1; i < length; i++) {
            int start = intervals[i - 1][0];    // 初始为i-1区间的左边界
            int end = intervals[i - 1][1];      // 初始i-1区间的右边界
            while (i < length && intervals[i][0] <= end) { // 合并区间
                end = max(end, intervals[i][1]);    // 不断更新右区间
                if (i == length - 1) flag = true;   // 最后一个区间也合并了
                i++;                                // 继续合并下一个区间
                	//因为此处需要继续合并后面的 用了i++ 所以while里边界条件一定要处理好
             		//不写i < length会越界访问
            }
            result.push_back({start, end});
        }
        // 如果最后一个区间没有合并,将其加入result
        if (flag == false) {
            result.push_back({intervals[length - 1][0], intervals[length - 1][1]});
        }
        return result;
    }
};
//简洁版~

class Solution {
public:
    // 按照区间左边界从小到大排序
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0];
    }
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> result;
        if (intervals.size() == 0) return result;
        sort(intervals.begin(), intervals.end(), cmp);
        result.push_back(intervals[0]);
        for (int i = 1; i < intervals.size(); i++) {
            if (result.back()[1] >= intervals[i][0]) { // 合并区间
                result.back()[1] = max(result.back()[1], intervals[i][1]);
            } else {
                result.push_back(intervals[i]);
            }
        }
        return result;
    }
};
拓展:lambda表达式

lambda 表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda 表达式的语法形式可简单归纳为: [ capture ] ( params ) opt -> ret { body; };

其中 capture 是捕获列表,params 是参数表,opt 是函数选项,ret 是返回值类型, body 是函数体。另外,lambda 表达式在没有参数列表时,参数列表是可以省略的。

[](const vector<int>& a, const vector<int>& b){return a[0] < b[0];}

//表示的就是:
    
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0];
    }

在这里插入图片描述

738. 单调递增的数字

暴力解法

直接从n到0一个个试,试出最大的合法数字。

class Solution {
private:
    bool checkNum(int num) {
        int max = 10;
        while (num) {
            int t = num % 10;
            if (max >= t) max = t;  //测是不是每一位都大于后一位,然后更新
            else return false;      //不大于则不合法,直接返回false
            num = num / 10;
        }
        return true;
    }
public:
    int monotoneIncreasingDigits(int N) {
        for (int i = N; i > 0; i--) {
            if (checkNum(i)) return i;  //从n到0一个个试是不是合法……
        }
        return 0;
    }
};
贪心算法

局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数全局最优:得到小于等于N的最大单调递增的整数

数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。所以从前后向遍历会改变已经遍历过的结果!那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string strNumber = to_string(n);    //将int类型数字转换为字符串数组~
        int flag = strNumber.size();    //flag用于标记9的赋值从哪一位开始进行
        //设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
        for(int i = strNumber.size() - 1; i > 0; i--){
            if(strNumber[i - 1] > strNumber[i]){  //相邻位数上的数字违背单调递增
                flag = i;   //标记9的赋值从哪一位开始进行
                strNumber[i - 1]--;   //赋值的前一位进行-1操作。
            }
        }
        for(int i = flag; i < strNumber.size(); i++){
            strNumber[i] = '9';
        }
        //如果没有出现相邻位数上的数字违背单调递增的原则,则在第一个循环内不做操作,不进入第二个循环,直接返回原数字
        return stoi(strNumber); //注意返回值要恢复为int类型 所以字符串转int stoi函数
    }
};

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

贪心算法

在 122.买卖股票的最佳时机II 中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后就是最大利润了。而本题有了手续费,就关系到什么时候买卖了,因为计算所获得利润,需要考虑到出现买卖利润不足以手续费的情况。

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

  • 买入日期:其实很好想,遇到更低点就记录一下。
  • 卖出日期:当前价格大于(最低价格+手续费),就可以收获利润,卖出日期就是连续收获利润区间里的最后一天。

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

  • 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
  • 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
  • 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int result = 0;
        int minPrice = prices[0];   //记录初始最低价格
        for(int i = 1;i < prices.size();i++){
            //情况2 买入
            if(prices[i] < minPrice){
                minPrice = prices[i];
            }
            //情况3 不买不卖
            if(prices[i] >= minPrice && prices[i] <= minPrice + fee){
                continue;
            }
            //算利润
            if(prices[i] > minPrice + fee){
                result += prices[i] - minPrice - fee;
                minPrice = prices[i] - fee; //情况1 
  //比如[1,3,7,5,10,3] fee=3情况下:
  //先算7-1-3=3,然后更新当天的最低价格,和第二天的最低价格进行比较
  //如果第二天低则是情况2(这一天是真的卖出了,最低价格会被更新为第二天的价格,也就是第8行)
  //但7-3=4<5属于今天低的情况,今天低则是情况1
  //意味着7这一天并不是本次购入股票的利润最高点,所以更新7这天的价格-减去手续费 = 4
  //这样就相当于当天没有进行卖出,因为后续卖出的result利润中 10-4-3= 3而不再是10-7-3
  //也就是说实际上手续费只算了一次,第二次扣除部分已经和最低价格的更新进行了抵消,也就相当于是最后一天才卖的了
            }
        }
        return result;
    }
};
动态规划

相对于 122.买卖股票的最佳时机II 的动态规划解法中,只需要在计算卖出操作的时候减去手续费就可以了。

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        //dp[i][0]  表示第 i 天交易完后手里没有股票的最大利润
        //dp[i][1]  表示第 i 天交易完后手里持有股票的最大利润
        int n = prices.size();
        vector<vector<int>> dp(n,vector<int>(2,0));
        dp[0][0] = 0;
        dp[0][1] = - prices[0];
        for(int i = 1; i < n; i++){
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
                        // 前一天没有,今天也没买 or 前一天有 今天卖了 扣了手续费
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
                        // 前一天持有 or 前一天没有,今天购入
        }
        return dp[n - 1][0];
    }
};

//时间复杂度:O(n)
//空间复杂度:O(n)
优化

当然可以对空间进行优化,不申请dp,因为当前状态只是依赖前一个状态,直接定义int变量进行更新即可。

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        //表示第 i 天交易完后手里没有股票的最大利润 saleStock 
        //表示第 i 天交易完后手里持有股票的最大利润 holdStock 
        int n = prices.size();
        int saleStock = 0;
        int holdStock = - prices[0];
        for(int i = 1; i < n; i++){
            int temp = saleStock;
            saleStock = max(saleStock, holdStock + prices[i] - fee);
                        // 前一天没有,今天也没买 or 前一天有 今天卖了 扣了手续费
            holdStock = max(holdStock, temp - prices[i]);
                        // 前一天持有 or 前一天没有,今天购入
        }
        return saleStock;
    }
};

//时间复杂度:O(n)
//空间复杂度:优化至O(1)

在这里插入图片描述

968. 监控二叉树

从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!

从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。

可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。

每个节点可能的几种状态分别由数字来表示:

  • 0:该节点无覆盖
  • 1:本节点有摄像头
  • 2:本节点有覆盖
  • (无摄像头状态 就是 无覆盖还没遍历到 或者 有覆盖不需要摄像头的状态,所以不额外用3来表示了。)

那么空节点怎么办?

  • 不能是无覆盖的状态,这样叶子节点就要放摄像头了
  • 空节点也不能是有摄像头的状态,这样叶子节点的父节点就放不了摄像头了
  • 所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了
情况分析
  • 情况1:左右节点都有覆盖,此时中间节点应该为无覆盖的状态。
  • 情况2:左右节点至少有一个无覆盖,则中间节点(父节点)放摄像头。
  • 情况3:左右节点至少有一个有摄像头,那么其父节点就是被覆盖的状态。
  • 情况4:处理完,递归结束后,头结点没有覆盖,放摄像头。
class Solution {
private:
    int result;
    int traversal(TreeNode* cur){
        if(cur == NULL) return 2;   //空结点是有覆盖状态
        int left = traversal(cur->left);
        int right = traversal(cur->right);

        if(left == 2 && right == 2) return 0;    // 情况1
        else if(left == 0 || right == 0){ // 情况2
            result++;
            return 1;
        } 
        else return 2;    // 情况3  if(left == 1 || right == 1) 
    }
public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        if(traversal(root) == 0){   // 情况4
            result++;
        }
        return result;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

97Marcus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值