当前位置: 首页 > news >正文

代码随想录Day 31|leetcode题目:56.合并区间、738.单调递增的数字、968.监控二叉树

提示:DDU,供自己复习使用。欢迎大家前来讨论~

文章目录

  • 贪心算法Part05
  • 题目
    • 题目一:56. 合并区间
      • 解题思路
    • 题目二:738.单调递增的数字
      • 解题思路:
        • 暴力解法:结果超时
        • 贪心算法
    • 题目三: 968.监控二叉树
      • 解题思路
      • 确定遍历顺序
      • 如何隔两个节点放一个摄像头
  • 贪心章节总结


贪心算法Part05

回溯算法开始

题目

题目一:56. 合并区间

56. 合并区间

解题思路

判断区间重叠,区别就是判断区间重叠后的逻辑,本题是判断区间重叠后要进行区间合并。

先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同。

按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1] 即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重贴,所以是<=)

这么说有点抽象,看图:(注意图中区间都是按照左边界排序之后了

56.合并区间

如何去模拟合并区间呢?

用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。

完整的C++代码如下:

sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){return a[0] < b[0];});

作用是对intervals这个容器中的元素按照每个元素的第一个值进行升序排序。在很多情况下,这种排序用于处理区间问题,其中每个区间由一对整数表示,第一个整数是区间的开始,第二个整数是区间的结束。通过这样的排序,可以方便地处理重叠区间等问题。

class Solution {
public:vector<vector<int>> merge(vector<vector<int>>& intervals) {vector<vector<int>> result;if (intervals.size() == 0) return result; // 区间集合为空直接返回// 排序的参数使用了lambda表达式sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){return a[0] < b[0];});// 第一个区间就可以放进结果集里,后面如果重叠,在result上直接合并result.push_back(intervals[0]); for (int i = 1; i < intervals.size(); i++) {if (result.back()[1] >= intervals[i][0]) { // 发现重叠区间// 合并区间,只更新右边界就好,因为result.back()的左边界一定是最小值,因为我们按照左边界排序的result.back()[1] = max(result.back()[1], intervals[i][1]); } else {result.push_back(intervals[i]); // 区间不重叠 }}return result;}
};
  • 时间复杂度: O(nlogn)
  • 空间复杂度: O(logn),排序需要的空间开销

题目二:738.单调递增的数字

738. 单调递增的数字

解题思路:

暴力解法:结果超时
class Solution {
private:// 判断一个数字的各位上是否是递增bool checkNum(int num) {int max = 10;while (num) {int t = num % 10;if (max >= t) max = t;else return false;num = num / 10;}return true;}
public:int monotoneIncreasingDigits(int N) {for (int i = N; i > 0; i--) { // 从大到小遍历if (checkNum(i)) return i;}return 0;}
};
  • 时间复杂度:O(n × m) m为n的数字长度
  • 空间复杂度:O(1)
贪心算法

题目要求小于等于N的最大单调递增的整数,那么拿一个两位的数字来举例。

例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]–,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。

后面要考虑是从前向后遍历还是从后向前遍历呢?

从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。

这么说有点抽象,举个例子,数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。

那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299

确定了遍历顺序之后,那么此时局部最优就可以推出全局,找不出反例,试试贪心。

完整的C++代码如下:

class Solution {
public:int monotoneIncreasingDigits(int N) {string strNum = to_string(N);// flag用来标记赋值9从哪里开始// 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行int flag = strNum.size();for (int i = strNum.size() - 1; i > 0; i--) {if (strNum[i - 1] > strNum[i] ) {flag = i;strNum[i - 1]--;}}for (int i = flag; i < strNum.size(); i++) {strNum[i] = '9';}return stoi(strNum);//string 转换为 int}
};
  • 时间复杂度:O(n),n 为数字长度
  • 空间复杂度:O(n),需要一个字符串,转化为字符串操作更方便

小结:

  1. 理解特殊情况:考虑一个数字序列,如98,当出现一个数比它右边的数小(即非单调递增)时,我们希望减少左边的数并使右边的数变为9,从而得到一个更小的数(例如89)。
  2. 贪心策略:通过这个特殊情况,我们可以推导出一个贪心算法,即在遇到非单调递增的情况时,总是尝试减少较大的数并增加较小的数,以获得更小的整数。
  3. 遍历顺序:为了有效地应用这个贪心策略,我们需要从序列的末尾开始向前遍历。这样做可以确保我们能够利用之前比较的结果,避免重复计算。
  4. 代码实现技巧:在实现算法时,可以使用一个标志(flag)来记录从哪个位置开始需要将数字赋值为9。

题目三: 968.监控二叉树

968. 监控二叉树

解题思路

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

局部最优推出全局最优,找不出反例,那么就按照贪心来!

大体思路就是从底到上,先给叶子节点的父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。

此时这道题目还有两个难点:

  1. 二叉树的遍历
  2. 如何隔两个节点放一个摄像头

确定遍历顺序

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

//后序遍历的代码
int traversal(TreeNode* cur) {// 空节点,该节点有覆盖if (终止条件) return ;int left = traversal(cur->left);    // 左int right = traversal(cur->right);  // 右逻辑处理                            // 中return ;
}

如何隔两个节点放一个摄像头

来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:

有如下三种:

  • 该节点无覆盖
  • 本节点有摄像头
  • 本节点有覆盖

我们分别有三个数字来表示:

  • 0:该节点无覆盖
  • 1:本节点有摄像头
  • 2:本节点有覆盖

大家应该找不出第四个节点的状态了。

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

递归的函数,以及终止条件已经确定了,再来看单层逻辑处理。

主要有如下四类情况:

  • 情况1:左右节点都有覆盖

左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。

如图:

968.监控二叉树2
// 左右节点都有覆盖
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 左节点覆盖,右节点无覆盖

有一个孩子没有覆盖,父节点就应该放摄像头。

此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。

本题是多个集合求组合,所以在回溯的搜索过程中,都有一些细节需要注意的。

if (left == 0 || right == 0) {result++;return 1;
}
  • 情况3:左右节点至少有一个有摄像头
if (left == 1 || right == 1) return 2;
  • 情况4:头结点没有覆盖(这个是最后遍历完所有的 才能知道 )

以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图:

968.监控二叉树3

所以递归结束之后,还要判断根节点,如果没有覆盖,result++,代码如下:

int minCameraCover(TreeNode* root) {result = 0;if (traversal(root) == 0) { // root 无覆盖result++;}return result;
}

C++代码如下:

// 版本一
class Solution {
private:int result;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// 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) {result++;return 1;}// 情况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;}public:int minCameraCover(TreeNode* root) {result = 0;// 情况4if (traversal(root) == 0) { // root 无覆盖result++;}return result;}
};

在以上代码的基础上,再进行精简,代码如下:

// 版本二
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;else if (left == 0 || right == 0) {result++;return 1;} else return 2;}
public:int minCameraCover(TreeNode* root) {result = 0;if (traversal(root) == 0) { // root 无覆盖result++;}return result;}
};
  • 时间复杂度: O(n),需要遍历二叉树上的每个节点
  • 空间复杂度: O(n)

贪心章节总结

  1. 究竟什么题目是贪心呢?

Carl个人认为:如果找出局部最优并可以推出全局最优,就是贪心,如果局部最优都没找出来,就不是贪心,可能是单纯的模拟。(并不是权威解读,一家之辞哈)

  1. 贪心题目分析

简单题目,就是靠常识,但我都具体分析了局部最优是什么,全局最优是什么,贪心也要贪的有理有据!

贪心中等题,靠常识可能就有点想不出来了。开始初现贪心算法的难度与巧妙之处。

贪心解决股票问题,股票系列问题是动规的专长,其实用贪心也可以解决

贪心难题,如果没有接触过,其实是很难想到的,甚至接触过,也一时想不出来,所以题目不要做一遍,要多练

  1. 两个维度权衡问题

在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【网络原理】从0开始学习计算机网络常识,中学生看了都能学会
  • 倒计时1天!每日一题,零基础入门FPGA
  • 【时间盒子】-【2.准备】HarmonyOS 开发前需要准备什么?
  • Mysql8 主从复制主从切换(超详细)
  • 如何在 CentOS 6 上安装 Nagios
  • 面试(九)
  • 今日算法:蓝桥杯基础题之“星期一”
  • 【Nest 学习笔记】AOP切片编程
  • python执行安装包文件时报错
  • 微信卸载后重新安装聊天记录怎么恢复?4招轻松找回微信数据
  • 最大交换
  • 哈希基础概念即使用(C++)
  • 华为云征文 | 华为云Flexus云服务器X实例全面使用操作指南
  • Hadoop环境搭建
  • 解析淘宝商品详情API返回值中的特殊属性
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • 「前端早读君006」移动开发必备:那些玩转H5的小技巧
  • Android组件 - 收藏集 - 掘金
  • Angular6错误 Service: No provider for Renderer2
  • IDEA 插件开发入门教程
  • javascript从右向左截取指定位数字符的3种方法
  • Js基础知识(四) - js运行原理与机制
  • js学习笔记
  • leetcode46 Permutation 排列组合
  • Nodejs和JavaWeb协助开发
  • Python语法速览与机器学习开发环境搭建
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • vue--为什么data属性必须是一个函数
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 半理解系列--Promise的进化史
  • 从0实现一个tiny react(三)生命周期
  • 浮现式设计
  • 基于MaxCompute打造轻盈的人人车移动端数据平台
  • 聊聊spring cloud的LoadBalancerAutoConfiguration
  • 每天一个设计模式之命令模式
  • 七牛云 DV OV EV SSL 证书上线,限时折扣低至 6.75 折!
  • 世界编程语言排行榜2008年06月(ActionScript 挺进20强)
  • 一个完整Java Web项目背后的密码
  • 一些基于React、Vue、Node.js、MongoDB技术栈的实践项目
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • 摩拜创始人胡玮炜也彻底离开了,共享单车行业还有未来吗? ...
  • ​用户画像从0到100的构建思路
  • ​总结MySQL 的一些知识点:MySQL 选择数据库​
  • (20)docke容器
  • (70min)字节暑假实习二面(已挂)
  • (第9篇)大数据的的超级应用——数据挖掘-推荐系统
  • (附源码)计算机毕业设计ssm基于Internet快递柜管理系统
  • (附源码)计算机毕业设计SSM基于java的云顶博客系统
  • (牛客腾讯思维编程题)编码编码分组打印下标(java 版本+ C版本)
  • (四)进入MySQL 【事务】
  • (转)jdk与jre的区别
  • (转)LINQ之路
  • (转)利用PHP的debug_backtrace函数,实现PHP文件权限管理、动态加载 【反射】...
  • .Net Core缓存组件(MemoryCache)源码解析
  • .NET Framework 服务实现监控可观测性最佳实践