【LeetCode题目详解】第八章 贪心算法 part06 738.单调递增的数字 968.监控二叉树 (day37补)

news2025/6/16 10:19:58

本文章代码以c++为例!

一、力扣第738题:单调递增的数字

题目:

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

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

示例 1:

输入: n = 10
输出: 9

示例 2:

输入: n = 1234
输出: 1234

示例 3:

输入: n = 332
输出: 299

提示:

  • 0 <= n <= 109

思路

# 暴力解法

题意很简单,那么首先想的就是暴力解法了,来我替大家暴力一波,结果自然是超时!

代码如下:

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);
    }
};

  • 时间复杂度:O(n),n 为数字长度
  • 空间复杂度:O(n),需要一个字符串,转化为字符串操作更方便

# 总结

本题只要想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。

想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。

最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。

二、力扣第968题:监控二叉树

题目:

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例 1:

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。


提示:

  1. 给定树的节点数的范围是 [1, 1000]
  2. 每个节点的值都是 0。

思路

这道题目首先要想,如何放置,才能让摄像头最小的呢?

从题目中示例,其实可以得到启发,我们发现题目示例中的摄像头都没有放在叶子节点上!

这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。

所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。

那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢?

因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。

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

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

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

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

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

# 确定遍历顺序

在二叉树中如何从低向上推导呢?

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

后序遍历代码如下:

int traversal(TreeNode* cur) {

    // 空节点,该节点有覆盖
    if (终止条件) return ;

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

    逻辑处理                            // 中
    return ;
}

注意在以上代码中我们取了左孩子的返回值,右孩子的返回值,即left 和 right, 以后推导中间节点的状态

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

此时需要状态转移的公式,大家不要和动态的状态转移公式混到一起,本题状态转移没有择优的过程,就是单纯的状态转移!

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

有如下三种:

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

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

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

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

一些同学可能会想有没有第四种状态:本节点无摄像头,其实无摄像头就是 无覆盖 或者 有覆盖的状态,所以一共还是三个状态。

因为在遍历树的过程中,就会遇到空节点,那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢?

回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。

那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。

所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了

接下来就是递推关系。

那么递归的终止条件应该是遇到了空节点,此时应该返回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:左右节点至少有一个有摄像头

如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态)

  • left == 1 && right == 2 左节点有摄像头,右节点有覆盖
  • left == 2 && right == 1 左节点有覆盖,右节点有摄像头
  • left == 1 && right == 1 左右节点都有摄像头

代码如下:

if (left == 1 || right == 1) return 2;

从这个代码中,可以看出,如果left == 1, right == 0 怎么办?其实这种条件在情况2中已经判断过了,如图:

968.监控二叉树1

这种情况也是大多数同学容易迷惑的情况。

  1. 情况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;
        // 情况4
        if (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)

大家可能会惊讶,居然可以这么简短,其实就是在版本一的基础上,使用else把一些情况直接覆盖掉了

在网上关于这道题解可以搜到很多这种神级别的代码,但都没讲不清楚,如果直接看代码的话,指定越看越晕,所以建议大家对着版本一的代码一步一步来,版本二中看不中用!

# 总结

本题的难点首先是要想到贪心的思路,然后就是遍历和状态推导。

在二叉树上进行状态推导,其实难度就上了一个台阶了,需要对二叉树的操作非常娴熟。

这道题目是名副其实的hard,大家感受感受。

day37补

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

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

相关文章

嵌入式技术

嵌入式技术 嵌入式微处理器体系结构嵌入式微处理器分类多核处理器嵌入式软件嵌入式系统的组成嵌入式系统的特性 嵌入式技术和计算机网络 超纲的内容很多 这个课件只包含一半的分 其他的分看真题 嵌入式微处理器体系结构 将指令存储器和数据存储器合并在一起的结构 处于同一个存…

搭建一个你的文件共享站

说起文件共享&#xff0c;类似文件站这样的功能。在很久之前我使用过用apache httpd这个方案&#xff0c;这个的话就是太过于简单了。当然是满足需求的&#xff08;又不是不能用&#xff09; 今天来分享一个开源的文件共享平台。Pingvin Share 作者的github地址&#xff1a;ht…

面试了38位Java候选人之后,我总结出了他们关于面试中的16条通病

都说现在Java面试卷&#xff0c;前段时间项目招人的时候&#xff0c;我刚好就作为面试官面试了一些人 在整个面试的过程中&#xff0c;我就发现了一些关于面试的通病 所以呢&#xff0c;趁着这次金&#xff08;铜&#xff09;九银&#xff08;铁&#xff09;十的机会&#xf…

C++用于算法题中简化代码的冷门函数与类模板集合(持续更新中~)

1.accumulate ①求和&#xff1a; 第三个参数作为初始值&#xff0c;将区间[first, end)的值相加&#xff0c;返回初始值加上区间总和的值。 需要注意的是&#xff0c;如果总和超出区间中数的类型范围&#xff0c;可以将第三个参数强转成64位的long long类型 #include <iost…

CentOS 8 安装 Code Igniter 4

在安装好LNMP运行环境基础上&#xff0c;将codeigniter4文件夹移动到/var/nginx/html根目录下&#xff0c;浏览器地址栏输入IP/codeigniter/pulbic 一直提示&#xff1a; Cache unable to write to "/var/nginx/html/codeigniter/writable/cache/". 找了好久&…

企业数字化转型有没有通用的路径和准则?

数字化转型是利用数字技术从根本上改变企业运营方式并向客户提供价值的过程。它涉及将数字技术集成到组织活动的各个方面&#xff0c;从客户交互到内部流程。虽然数字化转型的具体方法可能会根据组织的目标、行业和现有基础设施而有所不同&#xff0c;但有一些通用路径和指南可…

异常的捕获和处理

目录 一、异常 1.异常概述 1.1认识异常 1.2Java异常体系结构 2.Java异常处理机制 2.1异常处理 2.2捕获异常 2.2.1使用try-catch捕获异常 2.2.2使用try-catch-finally处理异常 2.2.3使用多重catch处理异常 2.3抛出异常 2.3.1使用throws声明抛出异常 2.3.2使用throw…

小企业需不需要内部知识库?为什么都在倡导内部知识沉淀?

有多种方法可以提高员工敬业度和员工工作效率&#xff0c;从给予信任到创造积极的工作环境。但一还有一个不为人知但十分有效的方式——为员工创建良好的内部知识库。所以小企业同样需要内部知识库&#xff0c;以下是为什么倡导内部知识沉淀的理由&#xff1a; 知识积累与传承…

Http 1.0 1.1 2.0 3.0 版本差别

Http 1.0 发布年份&#xff1a;1996 非官方标准 短链接&#xff1a;每一次请求都对应一次TCP的连接与释放 开销大&#xff1a;每次请求都要TCP的连接与释放队头阻塞&#xff1a;每次请求都必须等上一次请求获得响应之后&#xff0c;才可以发送&#xff1b;效率低下 缓存&…

理论转换实践之keepalived+nginx实现HA

背景&#xff1a; keepalivednginx实现ha是网站和应用服务器常用的方法&#xff0c;之前项目中单独用nginx实现过负载均衡和服务转发&#xff0c;keepalived一直停留在理论节点&#xff0c;加之最近工作编写的一个技术文档用到keepalived&#xff0c;于是便有了下文。 服务组件…

Linux系统编程:线程同步及生产与消费者模型

目录 一. 线程同步的概念及功能 二. 线程同步的实现方法 2.1 条件变量相关函数 2.2 线程同步demo代码 三. 生成与消费者模型 3.1 生产与消费者模型的概念 3.2 生产与消费者模型实现代码 四. 总结 一. 线程同步的概念及功能 为了了解线程同步的概念及实现的功能&#xf…

Revit SDK:PointCurveCreation 创建点来拟合曲线

前言 这个例子通过留个例子来展示如何通过点来拟合曲线或者曲面。 内容 PointsParabola 生成抛物线的核心逻辑&#xff1a; double yctr 0; XYZ xyz null; ReferencePoint rp null; double power 1.2; while (power < 1.5){double xctr 0;double zctr 0;while (…

数据集学习笔记(七):不同任务数据集的标签介绍(包含目标检测、图像分割、行为分析)

文章目录 一、目标检测1.1 TXT1.2 COCO1.3 XML 二、图像分割2.1 json2.1 TXT2.1.1 json转txt 三、行为分析3.1 TXT3.2 JSON 一、目标检测 1.1 TXT 每行表示&#xff08;类别&#xff0c;中心x相对坐标&#xff0c;中心y相对坐标&#xff0c;相对宽度、相对高度&#xff09; 1…

FFmpeg4.3.1+h264在windows下编译与VS2017项目集成

前言 在Android音视频开发中&#xff0c;网上知识点过于零碎&#xff0c;自学起来难度非常大&#xff0c;不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》&#xff0c;结合我自己的工作学习经历&#xff0c;我准备写一个音视频系列blog。本文是音视频系…

python spyder环境配置

首先安装python&#xff0c;配置环境变量等等 其次 pip install spyder 安装 spyder 最后启动 spyder&#xff0c;cmd下 执行 spyder&#xff0c;就打开了 调试下面的代码看看是否是系统的python import sys print(sys.executable) print(sys.path) 工具-偏好-python调试器 …

QT(8.30)常用类与组件,实现登录界面

1.作业&#xff1a; 完成一个登录界面(图片未附带): 头文件: #ifndef WIDGET_H #define WIDGET_H#include <QWidget>#include <QLineEdit>//行编辑器#include<QIcon>//图标#include<QLabel>//标签#include<QPushButton>//按钮#include<QIc…

【pyqt5界面化工具开发-13】QtDesigner功能择优使用

目录 0x00 前言&#xff1a; 一、完成基本的布局 二、其他功能的使用 三、在代码行开发 0x00 前言&#xff1a; QtDesigner工具的择优使用&#xff1a; 1、他的界面开发&#xff0c;是我们主要需要使用的功能 2、他的其他功能的使用&#xff0c;有需要就可使用&#xff…

报This application has no explicit mapping for /error 解决方法

Application启动类的位置不对&#xff0c;要将Application类放在最外侧&#xff0c;即包含所有子包。 没发现这个问题&#xff0c;卡了我老半天&#xff0c;兄弟们可以看一下你们是不是这个问题。

万人在线,一站式自动化运维 SysOM 3.0重磅发布!龙蜥社区系统运维 MeetUp 回顾来了

8 月 12 日&#xff0c;由龙蜥社区系统运维 SIG 主办&#xff0c;乘云数字协办的&#xff0c;主题为“观测&#xff0c;让运维更简单&#xff01;”的系统运维 MeetUp 于杭州圆满结束。来自乘云数字、谐云科技、乐维、云杉网络、擎创科技、观测云、阿里云以及浙江大学等众多厂商…

基于爬行动物算法优化的BP神经网络(预测应用) - 附代码

基于爬行动物算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于爬行动物算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.爬行动物优化BP神经网络2.1 BP神经网络参数设置2.2 爬行动物算法应用 4.测试结果&#xff1a;5…