LeetCode 1376. Time Needed to Inform All Employees【自顶向下,自底向上(记忆化搜索+空间优化+迭代)】中等

news2025/1/10 21:00:35

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

公司里有 n 名员工,每个员工的 ID 都是独一无二的,编号从 0 到 n - 1。公司的总负责人通过 headID 进行标识。

在 manager 数组中,每个员工都有一个直属负责人,其中 manager[i] 是第 i 名员工的直属负责人。对于总负责人,manager[headID] = -1。题目保证从属关系可以用树结构显示。

公司总负责人想要向公司所有员工通告一条紧急消息。他将会首先通知他的直属下属们,然后由这些下属通知他们的下属,直到所有的员工都得知这条紧急消息。

第 i 名员工需要 informTime[i] 分钟来通知它的所有直属下属(也就是说在 informTime[i] 分钟后,他的所有直属下属都可以开始传播这一消息)。

返回通知所有员工这一紧急消息所需要的 分钟数 。

示例 1:

输入:n = 1, headID = 0, manager = [-1], informTime = [0]
输出:0
解释:公司总负责人是该公司的唯一一名员工。

示例 2:

输入:n = 6, headID = 2, manager = [2,2,-1,2,2,2], informTime = [0,0,1,0,0,0]
输出:1
解释:id = 2 的员工是公司的总负责人,也是其他所有员工的直属负责人,他需要 1 分钟来通知所有员工。
上图显示了公司员工的树结构。

提示:

  • 1 <= n <= 10^5
  • 0 <= headID < n
  • manager.length == n
  • 0 <= manager[i] < n
  • manager[headID] == -1
  • informTime.length == n
  • 0 <= informTime[i] <= 1000
  • 如果员工 i 没有下属,informTime[i] == 0 。
  • 题目 保证 所有员工都可以收到通知。

解法1 递归+自顶向下

和104. 二叉树的最大深度相似,只用把递归中的 1 1 1 替换成 informTime [ x ] \textit{informTime}[x] informTime[x] ,即 i n f o r m T i m e [ x ] informTime[x] informTime[x] 看做树中子树结点到儿子结点的边长。但相比二叉树,一般树需要先通过 manager \textit{manager} manager 数组把每个点的儿子预处理出来,存储在 g g g 数组中。然后在递归中遍历当前节点的儿子,向下递归

递归一般来说,有如下两种写法。有返回值的写法(自顶向下+归来时累加):

class Solution {
	public int numOfMinutes(int n, int headID, int[] manager, int[] informTime) {
		List<Integer> g[] = new ArrayList[n];
		Arrays.setAll(g, e -> new ArrayList<>());
		for (int i = 0; i < n; ++i)
			if (manager[i] >= 0) g[manager[i]].add(i); // 建树
		return dfs(g, informTime, headID); // 从根结点headID递归,有返回值
	}
	private int dfs(List<Integer>[] g, int[] informTime, int x) {
		int maxPathSum = 0;
		for (int y : g[x]) // 遍历x的儿子y(如果没有就不会进入循环)
			maxPathSum = Math.max(maxPathSum, dfs(g, informTime, y));
		return maxPathSum + informTime[x];  // 和104题代码中return max(lDepth, rDepth) + 1;是一个意思
	}
}

传参写法(自顶向下+递去时累加):

class Solution {
    public int numOfMinutes(int n, int headID, int[] manager, int[] informTime) {
	    List<Integer> g[] = new ArrayList[n];
	    Arrays.setAll(g, e -> new ArrayList<>());
        for (int i = 0; i < n; ++i)
            if (manager[i] >= 0)
                g[manager[i]].add(i); // 建树
        dfs(g, informTime, headID, 0); // 从根结点headID递归
        return ans;
    }
    private int ans;
    private void dfs(List<Integer>[] g, int[] informTime, int x, int pathSum) {
	    pathSum += informTime[x]; // 累加递归路径上的informTime[x]
	    ans = Math.max(ans, pathSum); // 更新答案的最大值
	    for (int y : g[x]) // 遍历x的儿子y(如果没有儿子就不会进入循环)
		    dfs(g, informTime, y, pathSum); // 继续递归
    }
}

复杂度分析:

  • 时间复杂度: O ( n ) \mathcal{O}(n) O(n) 。每个节点都恰好访问一次。
  • 空间复杂度: O ( n ) \mathcal{O}(n) O(n) 。最坏情况下,树退化成一条链,递归需要 O ( n ) \mathcal{O}(n) O(n) 的栈空间。

解法二 自底向上

由于 manager \textit{manager} manager 数组中保存了每个节点的父节点,无需建树,直接顺着父节点,一路向上,同时累加路径上的 informTime [ x ] \textit{informTime}[x] informTime[x]

如果暴力枚举每个点、分别自底向上,取所有累加值中的最大值作为答案,时间复杂度是 O ( n 2 ) \mathcal{O}(n^2) O(n2) 。如何优化?不难发现,使用 记忆化搜索 这一思想,把从 x x x 向上得到的累加值记录到一个 m e m o memo memo\textit{memo} memomemo 数组中,如果下次再递归到 x x x ,就直接返回 m e m o memo memo 数组中保存的累加值(Python 可以用 @cache 装饰器)。

class Solution {
    public int numOfMinutes(int n, int headID, int[] manager, int[] informTime) {
	    var memo = new int[n];
	    Arrays.fill(memo, -1); // -1表示还没有计算过
	    int ans = 0;
	    for (int i = 0; i < n; ++i)
		    ans = Math.max(ans, dfs(manager, informTime, memo, i));
		return ans;
    }
    private int dfs(int[] manager, int[] informTime, int[] memo, int x) {
		if (manager[x] < 0) return informTime[x]; // 是根
		if (memo[x] >= 0) return memo[x]; // 之前计算过了
		return memo[x] = dfs(manager, informTime, memo, manager[x]) + informTime[x]; 
    }
}

我们可以空间优化——把计算结果直接保存到 informTime \textit{informTime} informTime 中。如何判断之前是否计算过呢?利用 manager \textit{manager} manager 数组,如果 x x x 计算过,就把 manager [ x ] \textit{manager}[x] manager[x] 置为 − 1 -1 1

class Solution {
public:
    int numOfMinutes(int n, int headID, vector<int> &manager, vector<int> &informTime) {
	    int ans = 0;
	    function<int(int)> dfs = [&](int x) -> int {
		    if (manager[x] >= 0) {
			    informTime[x] += dfs(manager[x]);
			    manager[x] = -1; // 标记x计算过
		    }
		    return informTime[x];
	    };
	    for (int i = 0; i < n; ++i) ans = max(ans, dfs(i)); 
        return ans;
    }
};

进一步地,把上面的代码改成两次迭代:

  • 第一次迭代,仅累加,不更新,计算从当前节点 i i i 往上的 i n f o r m T i m e informTime informTime 的累加值 s s s
  • 第二次迭代,更新从当前节点 i i i 向上的每个未被计算的节点值的对应累加值。在向上移动之前,从 s s s 中减去当前节点的 i n f o r m T i m e informTime informTime 值,同时设置当前节点的 m a n a g e r manager manager 值为 − 1 −1 1

如果你学过并查集,可以试试利用这个技巧,写出 f i n d find find 函数的非递归版本。

class Solution {
public:
    int numOfMinutes(int n, int headID, vector<int> &manager, vector<int> &informTime) {
        int ans = 0;
        for (int i = 0; i < n; ++i) {
	        if (manager[i] < 0) continue;
	        // 计算从i向上的累加值
	        int s = 0, x = i;
	        for (; manager[x] >= 0; x = manager[x])
		        s += informTime[x];
		    // 此时x要么是headID,要么是一个计算过的结点
		    s += informTime[x];
		    ans = max(ans, s);
		    // 记录从i向上的每个未被计算的节点值的对应累加值
		    for (int x = i; manager[x] >= 0; ) {
			    int t = informTime[x];
			    informTime[x] = s;
			    s -= t;
			    int m = manager[x];
			    manager[x] = -1; // 标记已被访问
			    x = m; // 继续向上
		    }
        } 
        return ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) \mathcal{O}(n) O(n) 。没有建图,实际运行速度比方法一要快一些。
  • 空间复杂度: O ( 1 ) \mathcal{O}(1) O(1) 。仅用到若干额外变量。

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

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

相关文章

Java+springboot开发的医院HIS信息管理系统实现,系统部署于云端,支持多租户SaaS模式

一、项目技术框架 前端&#xff1a;AngularNginx 后台&#xff1a;JavaSpring&#xff0c;SpringBoot&#xff0c;SpringMVC&#xff0c;SpringSecurity&#xff0c;MyBatisPlus&#xff0c;等 数据库&#xff1a;MySQL MyCat 缓存&#xff1a;RedisJ2Cache 消息队列&…

JVM-类的加载机制

目录 一、类的生命周期二、类加载的过程三、类加载的时机四、类加载器五、双亲委派模型六、自定义类加载器 一、类的生命周期 当编写完一个 java 类之后&#xff0c;经过编译就能够得到一个 .class&#xff08;字节码&#xff09;文件&#xff0c;这种字节码文件需要在 JVM 中…

递归思路讲解

最近刷到了树这一模块的算法题&#xff0c;树相关的算法题几乎都是用递归来实现的&#xff0c;但递归的思路却有点抽象&#xff0c;每次遇到递归&#xff0c;都是通过递归来深度或广度地遍历树&#xff0c;但对于递归遍历树的遍历路线&#xff0c;却有点抽象难懂&#xff0c;不…

基于simulink使用射频模块集天线块对天线阵列的射频系统进行建模

一、前言 本 例 说明 如何 对 包括 天线 阵列 的 MIMO 接收 和 发射 RF 系统 进行 建模。该设计从单个RF链的预算分析开始&#xff0c;然后扩展到多个天线。RF Blockset 天线模块对天线阵列进行全波分析&#xff0c;支持对效应和缺陷进行高保真建模&#xff0c;并结合射频系统的…

2023年的深度学习入门指南(3) - 前端同学如何进行chatgpt开发

2023年的深度学习入门指南(3) - 前端同学如何进行chatgpt开发 在第二篇&#xff0c;我们使用openai的python库封装&#xff0c;搞得它有点像之前学习的PyTorch一样的库。这一节我们专门给它正下名&#xff0c;前端就是字面意义上的前端。 给gpt4写前端 下面我们写一个最土的…

【Web】前端框架对微软老旧浏览器的支持

零、原因 最近要做一个项目&#xff0c;要能在学校机房运行的&#xff0c;也要在手机上运行。电脑和手机&#xff0c;一次性开发&#xff0c;那最好的就是响应式前端框架了。手机和正常的电脑兼容性问题应该都不大&#xff0c;但是学校机房都是Win7的系统&#xff0c;自带的都…

【Linux内核解析-linux-5.14.10-内核源码注释】MM内存管理内核启动初始化源码解析

源码 这是Linux内核中的mm_init函数的代码&#xff0c;其作用是初始化内存管理相关的组件和数据结构。 static: 这是一个函数声明修饰符&#xff0c;表示该函数只在当前文件中可见。 void __init: 这是函数的返回类型和修饰符&#xff0c;表示该函数是内核初始化代码。 page…

SpringCloud详解

SpringCloud是一个基于SpringBoot的分布式系统开发框架&#xff0c;它能够帮助我们快速、稳定地构建分布式系统。本篇博客将对SpringCloud进行详细解析&#xff0c;介绍SpringCloud的主要组件和相关应用场景&#xff0c;同时提供代码示例以帮助读者更好地掌握SpringCloud的实际…

nodejs+vue学生考勤请假管理系统java python php

用户登录模块&#xff1a;用来区分二种用户&#xff0c;学生、管理员。 个人信息管理&#xff1a;用户登录后可以修改用户表中的个人信息。 主页模块&#xff1a;在信息表中读取信息并按照一定模板显示在首页。 信息搜索模块&#xff1a;将信息表中所有信息的标题或内容关键字与…

析构函数/拷贝构造/赋值重载

析构函数&#xff1a; // 析构函数~Stack(){_top 0;_capacity 0;free(_a);_a nullptr;} 1 、2两点与构造函数类似。 3、当我们未显示定义时&#xff0c;编译器会自动生成默认的析构函数。C中&#xff0c;对于内置类型不进行任何处理&#xff0c;对于自定义类型&#xff0…

【SAS应用统计分析】方差分析

声明&#xff1a;本文知识参考内容来自网络&#xff0c;如有侵权请联系删除。 目录 【anova过程】 1.anova过程的语句格式 2.语句说明 【glm过程】 1.glm过程的语句格式 2.语句说明 【实例分析】 【实验步骤】 总结 【anova过程】 SAS系统的START软件提供了anova过程…

TensorRT:自定义插件学习与实践 001

文章简述 本文简单列出了编写Tensorrt插件所需要的关键方法,分为两个部分&#xff0c;一是插件类的具体实现方法&#xff0c;另外是插件工厂的调用方法,插件类最终将编译为.so文件,使用时在c或python中调用,所以插件类的方法调用在其他部分&#xff0c;在本文中难以直观的体现调…

PyQt5

最近在学习pyqt5&#xff0c; 使用pyqt5的时候出现了一些莫名奇妙的问题&#xff0c;解决之后决定把它记录下来&#xff0c;方面pyqt5的初学者使用。 每个问题会按照如下方式进行描述 1、问题描述&#xff1a; 2、解决方法&#xff1a; 问题1&#xff1a; 使用pyinstaller打…

计算机网络笔记:TCP三次握手和四次挥手过程

TCP是面向连接的协议&#xff0c;连接的建立和释放是每一次面向连接的通信中必不可少的过程。TCP连接的管理就是使连接的建立和释放都能正常地进行。 三次握手 TCP连接的建立—三次握手建立TCP连接 ① 若主机A中运行了一个客户进程&#xff0c;当它需要主机B的服务时&#xff0…

迁移学习

迁移学习 什么是迁移学习 迁移学习【斯坦福21秋季&#xff1a;实用机器学习中文版】 迁移学习&#xff08;Transfer Learning&#xff09;是一种机器学习方法&#xff0c;它通过将一个领域中的知识和经验迁移到另一个相关领域中&#xff0c;来加速和改进新领域的学习和解决问…

OS开源项目周报0105

由OpenDigg 出品的iOS开源项目周报第四期来啦。iOS开源周报集合了OpenDigg一周来新收录的优质的iOS开发方面的开源项目&#xff0c;方便iOS开发人员便捷的找到自己需要的项目工具等。 Hero 酷炫的iOS动画引擎 Traits 实时修改原生iOS 应用属性 JSDBanTangHomeDemo 仿半糖首页…

【Git】‘git‘ 不是内部或外部命令,也不是可运行的程序

一、问题 我想利用git clone命令从github上下载项目源代码&#xff0c;发现报错&#xff1a; git 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。我用cmd跑一下git命令&#xff0c;发现报错&#xff1a; 二、问题分析 这个错误提示表明您的系统中没有安装…

Illustrator如何使用基础功能?

文章目录 0.引言1.菜单栏2.工具箱 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对Illustrator进行了学习&#xff0c;本文通过《Illustrator CC2018基础与实战》及其配套素材结合网上相关资料进行学习笔记总结&#xff0c;本文对软件界面基本功能进行阐述。    1…

第四章 数据关联分析方法

基本概念和方法 关联规则和算法应用 基本概念和术语 关联规则算法应用&#xff1a; 一个关联规则分析的例子—————超市购物篮分析 不要看 后面数字看不懂 项集&#xff1a;是指项的集合。包含k个项的项集称为k-项集 支持度&#xff1a;若A是一个项集&#xff0c;则A的…

Vue3 +TypeScript 引入 BabylonJs(Vue3实现3D)【一篇文章精通系列】

本文主要介绍如何使用Vue3和TypeScript引入BabylonJs技术实现3D效果。结合实际案例&#xff0c;详细讲解了如何在Vue3项目中引入BabylonJs&#xff0c;并了解其相关知识。通过本文的学习&#xff0c;相信读者可以轻松掌握Vue3实现3D效果以及BabylonJs的相关知识。 Vue3 TypeS…