力扣贪心算法专题(三)力扣题 452、435、763、56、738、968、714 思路及C++实现

news2024/11/19 6:39:58

文章目录

  • 贪心算法
  • 452. 用最少数量的箭引爆气球
  • 435. 无重叠区间
    • 做法1 右边界排序 不重叠区间
    • 做法2 右边界排序 不重叠区间
    • 做法3 左边界排序 重叠区间
  • 763.划分字母区间
    • 做法1
    • 做法2
  • 56. 合并区间
  • 738.单调递增的数字
    • 暴力解法
    • 贪心算法
  • 968.监控二叉树
  • 714.买卖股票的最佳时机含手续费

贪心算法

  1. 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。如何通过局部最优,推出整体最优。
  2. 贪心算法的套路就是常识性推导加上举反例
  3. 贪心算法解题思路:想清楚局部最优是什么,如果推导出全局最优,就够了。

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

在这里插入图片描述

思路:

  • 只射重叠最多的气球,用的弓箭一定最少。
  • 贪心算法局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。

做法:

  • 把气球排序,从前到后(从后向前)遍历气球,仅跳过被射过的气球,记录箭的数量。
  • 按照气球的起始位置排序,从前向后遍历气球数组,靠左尽可能让气球重复。如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。
  • 例如,[[10,16],[2,8],[1,6],[7,12]]为例。排序后,[[1,6], [2,8], [7,12], [10,16]]。首先第一组重叠气球[1,6]和[2,8],一定需要一支箭;[7,12]气球3的左边界大于了 第一组重叠气球[1,6]的最小右边界,所以还需要一支箭来射气球3。

代码:

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b)
    {
        return a[0]<b[0];
    }
    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++)//注意从第二个气球开始
        {
            //如果气球不重叠 当前气球的头 在 上一个气球的尾后面 需要一只箭
            //不取= 是因为题目中满足xstart ≤ x ≤ xend,则该气球会被引爆。
            //那么说明两个气球挨在一起不重叠也可以一起射爆
            if(points[i][0] > points[i-1][1]) result++;
            //如果气球重叠  更新重叠气球最小右边界
            else points[i][1] = min(points[i-1][1], points[i][1]);
        }
        return result;
    }
};

435. 无重叠区间

做法1 右边界排序 不重叠区间

思路: 这和452.用最少数量的箭引爆气球非常像,弓箭数相当于是非交叉区间的数量。不同的是,题目中认为[0,1]和[1,2]不是相邻区间,注意判断条件的修改,然后用总区间数减去弓箭数量 就是要移除的区间数量了。

代码:

class Solution {
public:
    static bool cmp_r(const vector<int>& a, const vector<int>& b)
    {
        return a[1] < b[1];
    }
    static bool cmp_l(const vector<int>& a, vector<int>& b)
    {
        return a[0] < b[0];
    }

    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.size()==0) return 0;
       
        //做法1 右边界排序 重叠区间
        sort(intervals.begin(), intervals.end(), cmp_r);
        int count = 1;//不重叠区间数
        for(int i=1; i<intervals.size(); i++)
        {
            //注意 [0,1]和[1,2]不是相邻区间
            if(intervals[i][0] >= intervals[i-1][1]) count++;
            else intervals[i][1] = min(intervals[i][1], intervals[i-1][1]);
        }
        return intervals.size() - count;
    }
};

做法2 右边界排序 不重叠区间

思路: 让区间尽可能重叠,首先右边界按照从小到大排序,从左向右遍历记录非交叉区间的个数,最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。

代码:

class Solution {
public:
    static bool cmp_r(const vector<int>& a, const vector<int>& b)
    {
        return a[1] < b[1];
    }
    static bool cmp_l(const vector<int>& a, vector<int>& b)
    {
        return a[0] < b[0];
    }

    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.size()==0) return 0;

        //做法2 右边界排序 不重叠区间
        sort(intervals.begin(), intervals.end(), cmp_r);
        int count = 1;//记录非交叉区间的个数
        int end = intervals[0][1];//分割点
        for(int i=1; i<intervals.size(); i++)
        {
            //如果当前区间的尾巴 在 上一个区间的头前面 说明不重叠
            if(end <= intervals[i][0])
            {
                count++;
                end = intervals[i][1];//更新区间尾巴
            }
        }
        return intervals.size() - count;
    }
};

做法3 左边界排序 重叠区间

代码:

class Solution {
public:
    static bool cmp_r(const vector<int>& a, const vector<int>& b)
    {
        return a[1] < b[1];
    }
    static bool cmp_l(const vector<int>& a, vector<int>& b)
    {
        return a[0] < b[0];
    }

    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.size()==0) return 0;

        //做法3 左边界排序 重叠区间
        sort(intervals.begin(), intervals.end(), cmp_l);
        int count = 0;//记录重叠区间
        for(int i=1; i<intervals.size(); i++)
        {
            //如果当前区间的头 在 上一个区间的尾巴前面 说明重叠
            //注意 [0,1]和[1,2]不是相邻区间
            if(intervals[i][0] < intervals[i-1][1])
            {
                intervals[i][1] = min(intervals[i][1], intervals[i-1][1]);//更新最小右边界
                count++; 
            }
        }
        return count;
    }
};

763.划分字母区间

在这里插入图片描述

做法1

思路:
用最远出现距离模拟了圈字符的行为,要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了,可以分为如下两步:

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

代码:

class Solution {
public:
    vector<int> partitionLabels(string s) {
        int hash[27] = {0};//保存字母最后出现的位置 
        //统计字母最后出现的位置
        //i为字符,hash[i]为字符出现的最后位置
        for(int i=0; i<s.size(); i++)
        {
            hash[s[i] - 'a'] = i;
        }
        vector<int> result;
        int left = 0;
        int 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;//left要更新 跳到i+1位置开始新的划分起点
            }
        }
        return result;
    }
};

做法2

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

代码:

class Solution {
public:
    //做法2
    //首先获得区间输入
    vector<vector<int>> countLabels(string s)
    {
        vector<vector<int>> hash(26, vector<int>(2, INT_MIN));//记录区间 默认26哥字母
        vector<vector<int>> hash_filter;
        //记录每个字母出现的起始位置
        for(int i=0; i<hash.size(); ++i)
        {
            if(hash[s[i] - 'a'][0] == INT_MIN) hash[s[i] - 'a'][0] = i;//最开始位置
            hash[s[i] - 'a'][1] = i;//最末尾位置
        }
        //去掉s中没有出现的字母的区间
        for(int i=0; i<s.size(); ++i)
        {
            if(hash[i][0] != INT_MIN) hash_filter.push_back(hash[i]);//存放出现字母对应区间
        }
        return hash_filter;
    }

    //排序 左边界排序
    static bool cmp(vector<int>& a, vector<int>& b)
    {
        return a[0] < b[0];
    }
    //找出重叠区间的长度个数
    vector<int> partitionLabels(string s)
    {
        vector<int> result;//记录区间分割点
        //获得s各字母的起始区间
        vector<vector<int>> hash = countLabels(s);
        sort(hash.begin(), hash.end(), cmp);//按照左边界排序
        int right = hash[0][1];// 记录最大右边界
        int left = 0;
        //找到分割点
        for(int i=1; i<hash.size(); ++i)
        {
            //当前区间的头 在 上一区间的尾部 前面
            if(hash[i][0] > right)
            {
                result.push_back(right - left + 1);//存放长度
                left = hash[i][0];//left左边界更新 找下一个重叠区间长度
            }
            right = max(right, hash[i][1]);//right最远右边界更新 找下一个重叠区间长度
        }
        result.push_back(right - left + 1);//最右端
        return result;
    }
};

56. 合并区间

在这里插入图片描述

思路:
和435.无重叠区间题目思路类似,找重叠区间,不同的是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。

代码:

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> result;
        //lambda表达式 左边界排序
        sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){return a[0] < b[0];});
        //左边界排序后 result.back()的左边界一定是最小值 只需要跟新右区间
        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;
    }
};

738.单调递增的数字

在这里插入图片描述

暴力解法

依次取位,从个位开始向高位依次判断,数字是否递增;从大到小遍历

代码:

class Solution {
private:
    bool isup(int num)
    {
        int max = 10, temp = 0;//个位最大取9
        while(num)
        {
            temp = num % 10;
            if(max >= temp) max = temp;
            else return false;
            num = num / 10;//前进一位
        }
        return true;
    }
public:
    int monotoneIncreasingDigits(int n) {
        for(int i=n; i>=0; i--)//从大到小逐个判断
        {
            if(isup(i)) return i;
        }
        return 0;
    }
};

贪心算法

一个两位数xy,找小于等于xy的最大单调递增整数,如果x>y,让x–,y=9,即(x-1)9;如果x≤y,不变。从后向前遍历数字。
不可以从前往后遍历,如果按照上述操作,对于三位数等多位数有可能会导致结果不对,例如543,从前往后遍历就变成了439,百位大于十位,不是单调递增数字。从后往前遍历,就是543→539→499

代码:

class Solution {
//贪心算法
public:
    int monotoneIncreasingDigits(int n) {
        //先转成字符串
        string strnum = to_string(n);
        //标记赋值9从哪里开始,设置默认值为strnum.size()
        //防止第二个for循环在flag没有被赋值的情况下执行
        int mark = strnum.size();
        for(int i = strnum.size()-1; i>0; i--)
        {
            if(strnum[i-1] > strnum[i]) 
            {
                mark = i;//标记
                strnum[i-1]--;
            }
        }
        //在标记位置赋值为9
        for(int i = mark; i<strnum.size(); i++)
        {
            strnum[i] = '9';
        }
        return stoi(strnum);
    }

};

968.监控二叉树

分析:

  1. 摄像头都没有在叶子节点上。如果放在叶子节点上,就会浪费一层覆盖;放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积,即摄像头可以覆盖上中下三层。
  2. 从下往上看。如果从上往下看,头结点不放置,可以省一个摄像头。

思路:
从下往上遍历,后序遍历,左右中顺序,回溯时先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。
局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少。

如何放置摄像头?
状态转移的公式,三个数字来表示:
0:该节点无覆盖
1:本节点有摄像头
2:本节点有覆盖
空节点的状态只能是有覆盖,无摄像头就是无覆盖或者有覆盖的状态

单层递归的四种情况:

  1. 情况1:左右节点都有覆盖,那么中间节点就是无覆盖了
  2. 情况2:左右节点至少有一个无覆盖的情况,则中间节点(父节点)应该放摄像头:
  • left == 0 && right == 0 左右节点无覆盖
  • left == 1 && right == 0 左节点有摄像头,右节点无覆盖
  • left == 0 && right == 1 左节点有无覆盖,右节点摄像头
  • left == 0 && right == 2 左节点无覆盖,右节点覆盖
  • left == 2 && right == 0 左节点覆盖,右节点无覆盖
    此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。
  1. 情况3:左右节点至少有一个有摄像头,那么其父节点就是覆盖的状态
  • left == 1 && right == 2 左节点有摄像头,右节点有覆盖
  • left == 2 && right == 1 左节点有覆盖,右节点有摄像头
  • left == 1 && right == 1 左右节点都有摄像头
  1. 头结点没有覆盖,递归结束之后,还要判断根节点,如果没有覆盖,result++

代码:

class Solution {
private:
    int result;
    /*
    0---无覆盖
    1---有摄像头
    2---有覆盖
    */
    int traversal(TreeNode* cur)
    {
        //空节点,该节点有覆盖
        if(cur == NULL) return 2;
        int left = traversal(cur->left);
        int right = traversal(cur->right);

        //1.左右都有节点 中间节点无覆盖
        if(left == 2 && right == 2) return 0;
        //2.左右节点至少有一个是覆盖  中间节点放摄像头
        if(left == 0 || right == 0)
        {
            result++;
            return 1;
        }
        //3.左右节点至少有一个有摄像头 中间节点有覆盖
        if(left == 1 || right == 1) return 2;

          return -1;
    }

public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        //4.判断头结点无覆盖 +1
        if(traversal(root) == 0) result++;
        return result;
    }
};

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

在这里插入图片描述
思路:
涉及手续费,要考虑什么时候买卖股票,因为有可能买卖利润不足以支付手续费。
最低价买股票,即买入日期,此时股价最小值。
扣除手续费的最高价卖股票,即卖出日期。只要当前价格大于最低价格+手续费,就可以收获利润。而最终的卖出日期是连续收获利润区间里的最后一天。
收获利润操作时的三种情况:
情况一:最低价时,买入股票
情况二:不买不卖,保持原有状态,买不便宜,卖亏本
情况三:当前价格大于最低价格+手续费时卖出股票,计算利润,不是真的卖出股票,同时记录最小价格,最后一次计算计算利润才是真的卖出股票

代码:

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++)
        {
            //低价买入 买入日期
            if(prices[i] < minprice) minprice = prices[i];

            //不买不卖 买不便宜 卖亏本
            if(prices[i] >= minprice && prices[i] <= minprice+fee) continue;

            //计算利润 最后一天计算利润才是真的卖出日期
            if(prices[i] > minprice+fee)
            {
                result += prices[i] - minprice - fee;
                minprice = prices[i] - fee;//每天要更新最低价格
            }
        }
        return result;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/569280.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSDN周赛55期 - 简单分析

上期周赛被判抄袭了。。。无语。我从第一期周赛一步步走来&#xff0c;所有题我都做过&#xff0c;我还需要抄袭&#xff1f; 虽然第一时间去申诉&#xff0c;但貌似并没什么用。算了&#xff0c;C站的审核也就这样了&#xff0c;失望寒心。 本期还是《计算之魂》主题赛——不得…

JavaScript 基础 DOM (四)

正则表达式正则表达式 正则基本使用 定义规则 const reg /表达式/其中/ /是正则表达式字面量正则表达式也是对象 使用正则 test()方法 用来查看正则表达式与指定的字符串是否匹配 如果正则表达式与指定的字符串匹配 &#xff0c;返回true&#xff0c;否则false reg.test(…

eclipse环境配置

eclipse环境配置 0. 前言1. 下载eclipse2. 下载sdk3. 配置sdk环境变量4. 验证sdk其他 0. 前言 本节记录如何配置eclipse的sdk配置 操作系统&#xff1a;Windows10 专业版 开发环境&#xff1a;eclipse 1. 下载eclipse 来这边下载一下eclipse 菜鸟教程 完事之后打开eclips…

回归测试概念和4种回归测试策略——你想知道的都在这里啦!

前言&#xff1a; 回归测试是指修改了旧代码后&#xff0c;重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。自动回归测试将大幅降低系统测试、维护升级等阶段的成本。回归测试作为软件生命周期的一个组成部分&#xff0c;在整个软件测试过程中占有很大的工作…

Tomcat源码:Acceptor与Poller、PollerEvent

参考资料&#xff1a; 《Tomcat源码解析系列&#xff08;十一&#xff09;ProtocolHandler》 《Tomcat源码解析系列&#xff08;十二&#xff09;NioEndpoint》 前文&#xff1a; 《Tomcat源码&#xff1a;启动类Bootstrap与Catalina的加载》 《Tomcat源码&#xff1a;容器…

绝对不能错过的7个零基础免费的ChatGPT镜像网站

还在为打不开openai官网烦心&#xff1f;本文帮你实现ChatGPTMidJourney自由(&#xffe3;∇&#xffe3;)/ &#x1f4d2;收集了一些截至目前(2023年5月25日午12:00)可以免费访问&#xff0c;并且零基础也能正常使用的镜像网站&#xff0c;后续将持续维护更新(&#xff61;&a…

(2)lordPE脱壳

1.寻找OEP 1.1第一种情况 1.修改OD选项&#xff0c;调试设置"事件"为系统断点&#xff0c;直接打开"查看"->"内存",设置00400000下F2断点&#xff0c;单步F8找到0040****开头的OEP例如&#xff1a;00401528 1.2第二种情况 进入od后如果直…

Github疯传!200本经典计算机书籍!

好书在精不在多&#xff0c;每一本经典书籍都值得反复翻阅&#xff0c;温故而知新&#xff01; 下面分享几本计算机经典书籍&#xff0c;都是我自己看过的。 重构 改善既有代码的设计 就像豆瓣评论所说的&#xff0c;看后有种醍醐灌顶、欲罢不能的感觉。无论你是初学者&#…

数据结构与算法02:数组和链表

目录 【数组】 为什么Go语言的切片是成倍扩容&#xff1f; 【链表】 单链表 循环链表 双向链表 双向循环链表 数组和链表如何选择&#xff1f; 如何使用链表实现 LRU 缓存淘汰算法&#xff1f; 链表的一些操作 【每日一练】 【数组】 数组&#xff08;Array&#…

JMeter参数化四种实现方式

1 参数化释义 什么是参数化&#xff1f;从字面上去理解的话&#xff0c;就是事先准备好数据&#xff08;广义上来说&#xff0c;可以是具体的数据值&#xff0c;也可以是数据生成规则&#xff09;&#xff0c;而非在脚本中写死&#xff0c;脚本执行时从准备好的数据中取值。 参…

Sentinel热点key

1.基本介绍 官方文档 何为热点&#xff1f;热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据&#xff0c;并对其访问进行限制。比如&#xff1a; 商品 ID 为参数&#xff0c;统计一段时间内最常购买的商品 ID 并进行限制用户 ID 为参数&…

HyDE、UDAPDR(LLM大模型用于信息检索)

本篇博文继续整理LLM在搜索推荐领域的应用&#xff0c;往期文章请往博主主页查看更多。 Precise Zero-Shot Dense Retrieval without Relevance Labels 这篇文章主要做zero-shot场景下的稠密检索&#xff0c;通过借助LLM的力量不需要Relevance Labels&#xff0c;开箱即用。作…

【数据结构】---几分钟简单几步学会手撕链式二叉树(上)

文章目录 前言&#x1f31f;一、二叉树链式结构的实现&#x1f30f;1.1 前置说明&#x1f4ab;快速创建一棵简单的二叉树 &#x1f30f;1.2 二叉树的遍历的时间、空间复杂度&#x1f30f;1.3 二叉树的遍历&#x1f4ab;1.3.1 前序、中序以及后序遍历&#xff1a;&#x1f4ab;1…

深入理解hashmap底层实现原理

目录 总体介绍 HashMap元素的存储 在hashmap中添加元素 HashMap的扩容机制 HashMap的线程安全性 1.添加和删除元素时存在不安全性 2.进行扩容操作时存在不安全性 3.哈希冲突存在不安全性 4.线程之间的不可见性导致安全问题 总体介绍 HashMap是我们用于元素映射使用频率最…

MySQL——子查询

来一篇 MySQL-子查询 记录一下这个美好的时光,学习记录篇,下文中所有SQL 语句 均可在 MySQL DB 学习Demo 此处下载SQL语句执行,有相关DB 与 表。 1. 需求分析与问题解决 1.1 实际问题 现有解决方式一: SELECT salary FROM employees WHERE last_name = Abel SELECT last…

【算法】【算法杂谈】旋转数组的二分法查找

aTOC 前言 当前所有算法都使用测试用例运行过&#xff0c;但是不保证100%的测试用例&#xff0c;如果存在问题务必联系批评指正~ 在此感谢左大神让我对算法有了新的感悟认识&#xff01; 问题介绍 原问题 给定一个从小到大有序的数组&#xff0c;该数组存在重复的数&#xf…

【数据安全-02】AI打假利器数字水印,及java+opencv实现

AIGC 的火爆引燃了数字水印&#xff0c;说实话数字水印并不是一项新的技术&#xff0c;但是这时候某些公司拿出来宣传一下特别应景&#xff0c;相应股票蹭蹭地涨。数字水印是什么呢&#xff0c;顾名思义&#xff0c;和我们在pdf中打的水印作用差不多&#xff0c;起到明确版权、…

【拒绝爆零】C++编程考试常见栽区

前言 在OI赛制中&#xff0c;我们可能会因为一些细节原因导致题目爆零。下面&#xff0c;是我列举的一些常见的坑&#xff1a; 1.极值未赋值 这个错误在运行时就能检查出来&#xff0c;但还是会浪费一定的时间&#xff0c;所以我们还是避开这些小插曲为好。 2.定义变量遇到…

利用无代码工具开发一款小程序

目录 无代码工具开发小程序的流程需求分析阶段模型设计阶段页面搭建阶段创建项目创建数据表组件搭建 预览发布总结 日常我们开发小程序的时候都是要从写代码开始&#xff0c;但是写代码这个事只有专业开发才可以干&#xff0c;那作为普通人&#xff0c;如果也希望开发小程序&am…

前端小工具:批量修改图片信息

前端小工具一&#xff1a;批量修改文件夹里面的图片名称 步骤&#xff1a; 1.安装nodejs。 2.根据需要修改editFileName(filePath, formatName)函数的参数&#xff0c;也可以不改&#xff0c;直接将renameFile.js和img文件夹放在同一个目录下。 3.在renameFile.js目录下开启…