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

本文章代码以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补

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

zookeeper介绍、zookeeper的安装与配置

1、zookeeper介绍 1.1 官网说明 官方地址&#xff1a;http://zookeeper.apache.org/ 它是拿来管理 Hadoop、Hive、Pig的管理员&#xff0c; Apache Hbase和Apache Solr以及阿里的Dubbo等项目中都采用到了Zookeeper。 一句话&#xff1a;ZooKeeper是一个分布式协调技术、高性…

MySQL基础入门

推荐查看 数据库相关概念 MySQL百度百科 MySQL是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一&#xff0c;在 WEB 应用方面&#xff0c;MySQL是最好的 RDBMS (Relational Databa…

学习pytorch7 神经网络的基本骨架--nn,module的使用

神经网络的基本骨架--nn,module的使用 官网Module介绍Python父类子类继承关系前向神经网络pycharm快捷键重写类方法codedebug B站小土堆视频学习笔记 官网Module介绍 https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module Python父类子类继承关系…

自动化运维:Ansible脚本之playbook剧本

目录 一、理论 1.playbooks 2.YAML 3.使用ansible批量安装apache服务 4.定义、引用变量 5.指定远程主机sudo切换用户 6.when条件判断 7.迭代 8.Templates 模块 9.tags 模块 10.Roles 模块 二、实验 1.使用ansible批量安装apache服务 2.定义、引用变量…

对称加密 非对称加密 AC认证 https原理

文章目录 对称加密及漏洞非对称加密及漏洞什么是数据摘要&#xff08;也称数据指纹&#xff09;什么是CA认证CA证书签发过程https通信方案 对称加密及漏洞 对称加密是一种加密算法&#xff0c;使用相同的密钥&#xff08;也称为对称密钥&#xff09;用于加密和解密数据。在对称…

c++ lambda

Lambda Lambda 表达式一般用于定义匿名函数&#xff0c;使得代码更加灵活简洁&#xff0c;优点&#xff1a; 声明式编程风格&#xff1a;就地匿名定义目标函数或函数对象&#xff0c;不需要额外写一个命名函数或者函数对象。以更直接的方式去写程序&#xff0c;好的可读性和可…

Spring与Mybatis集成且Aop整合

目录 一、集成 1.1 集成的概述 1.2 集成的优点 1.3 代码示例 二、整合 2.1 整合概述 2.2 整合进行分页 一、集成 1.1 集成的概述 集成是指将不同的组件、部分或系统组合在一起&#xff0c;以形成一个整体功能完整的解决方案。它是通过连接、交互和协调组件之间的关系来实…

python的异步编程async

一、介绍 在Python 3.5中引入了async和await关键字&#xff0c;用于异步编程。async关键字用于定义一个协程&#xff08;coroutine&#xff09;&#xff0c;而await关键字则用于等待一个协程完成。 注&#xff1a;协程&#xff08;coroutine&#xff09;是是一种轻量级的线程…

LeetCode——回溯篇(三)

刷题顺序及思路来源于代码随想录&#xff0c;网站地址&#xff1a;https://programmercarl.com 目录 46. 全排列 47. 全排列 II 332. 重新安排行程 51. N 皇后 37. 解数独 46. 全排列 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任…

Linux命令查看CPU、内存、IO使用情况简单介绍

文章目录 1. CPU相关介绍1.1 物理CPU1.2 物理CPU内核1.3 逻辑CPU1.4 几核几线程1.5 CPU设计图 2. top 查看系统负载、CPU使用情况2.1 系统整体的统计信息2.2 进程信息2.3 top命令使用 3. lscpu 显示有关 CPU 架构的信息4. free 查看内存信息5. iostat 查看io信息 1. CPU相关介绍…

雅思写作 三小时浓缩学习顾家北 笔记总结(一)

目录 饥饿网翻译100个句子记录 There are some economically deprived communities in large cities. there is no clear link between grouping student by ability and their levels of attainment. young people without tertiary education qualification normally hav…

python-数据可视化-下载数据-CSV文件格式

数据以两种常见格式存储&#xff1a;CSV和JSON CSV文件格式 comma-separated values import csv filename sitka_weather_07-2018_simple.csv with open(filename) as f:reader csv.reader(f)header_row next(reader)print(header_row) # [USW00025333, SITKA AIRPORT, A…

高等职业学校物联网实训室建设方案

一、概述 1.1专业背景 物联网&#xff08;Internet of Things&#xff09;被称为继计算机、互联网之后世界信息产业第三次浪潮&#xff0c;它并非一个全新的技术领域&#xff0c;而是现代信息技术发展到一定阶段后出现的一种聚合性应用与技术提升&#xff0c;是随着传感网、通…

解决博客不能解析PHP直接下载源码问题

背景&#xff1a; 在网站设置反向代理后&#xff0c;网站突然不能正常访问&#xff0c;而是会直接下载访问文件的PHP源码 解决办法&#xff1a; 由于在搞完反向代理之后&#xff0c;PHP版本变成了纯静态&#xff0c;所以网站不能正常解析&#xff1b;只需要把PHP版本恢复正常…

SQL注入漏洞复现(CVE-2017-8917)

文章目录 搭建环境启动环境漏洞复现报错注入使用sqlmap 前提条件&#xff1a; 1.安装docker docker pull medicean/vulapps:j_joomla_22.安装docker-compose docker run -d -p 8000:80 medicean/vulapps:j_joomla_23.下载vulhub Docker Compose是 docker 提供的一个命令行工具&…

springboot docker

在Spring Boot中使用Docker可以帮助你将应用程序与其依赖的容器化&#xff0c;并简化部署和管理过程。 当你在Spring Boot中使用Docker时&#xff0c;你的代码不需要特殊的更改。你可以按照通常的方式编写Spring Boot应用程序。 java示例代码&#xff0c;展示了如何编写一个基…

直播预告!生鲜与零售商品识别系统产业实践与部署详解

生鲜零售作为民生消费的重要一环&#xff0c;在促进行业新消费升级的进程中有着至关重要的作用。在超市等无人零售场景中&#xff0c;目前结算方式主要有以下几种&#xff1a; 但是以上几种方法存在如下缺点&#xff1a; 条形码方式&#xff1a;对于成品包装的商品较为成熟&a…

WPF_布局基础

布局容器 Grid 定义由列和行组成的灵活的网格区域。 行 <Grid.RowDefinitions><RowDefinition/><RowDefinition/></Grid.RowDefinitions> 列 <Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition/></Grid.ColumnDe…

指针与空间按钮的交互

文章目录 原理案例&#xff1a;“直线指针”和“点击按钮”的交互1、效果2、步骤 原理 指针不能直接和空间按钮交互&#xff0c;得借助一个中间层——分发器——它分发指针的进入、退出、选择事件&#xff0c;空间按钮自动监听这些事件 案例&#xff1a;“直线指针”和“点击…

HTTPS安全通信和SSL Pinning

随着互联网的迅速发展&#xff0c;网络通信安全问题日益凸显。在这一背景下&#xff0c;HTTPS作为一种加密通信协议得到了广泛应用&#xff0c;以保障用户的数据隐私和信息安全。本文将介绍HTTPS的基本原理、发展历程&#xff0c;以及与之相关的中间人攻击和防护方法。 1. HTT…