每日一题 —— 882. 细分图中的可到达节点

news2024/9/21 7:55:02

882. 细分图中的可到达节点

给你一个无向图(原始图),图中有 n 个节点,编号从 0n - 1 。你决定将图中的每条边 细分 为一条节点链,每条边之间的新节点数各不相同。

图用由边组成的二维数组 e d g e s edges edges 表示,其中 e d g e s [ i ] = [ u i , v i , c n t i ] edges[i] = [u_i, v_i, cnt_i] edges[i]=[ui,vi,cnti] 表示原始图中节点 $u_i $和 v i v_i vi 之间存在一条边, c n t i cnt_i cnti 是将边 细分 后的新节点总数。注意, c n t i = = 0 cnt_i == 0 cnti==0 表示边不可细分。

细分 [ u i , v i ] [u_i, v_i] [ui,vi] ,需要将其替换为 ( c n t i + 1 ) (cnt_i + 1) (cnti+1) 条新边,和 c n t i cnt_i cnti 个新节点。新节点为 x 1 , x 2 , . . . , x c n t i x_1, x_2, ..., x_{cnt_i} x1,x2,...,xcnti ,新边为 [ u i , x 1 ] , [ x 1 , x 2 ] , [ x 2 , x 3 ] , . . . , [ x c n t i − 1 , x c n t i ] , [ x c n t i , v i ] [u_i, x_1], [x_1, x_2], [x_2, x_3], ..., [x_{cnt_i-1}, x_{cnt_i}], [x_{cnti}, v_i] [ui,x1],[x1,x2],[x2,x3],...,[xcnti1,xcnti],[xcnti,vi]

现在得到一个 新的细分图 ,请你计算从节点 0 出发,可以到达多少个节点?如果节点间距离是 maxMoves 或更少,则视为 可以到达

给你原始图和 maxMoves ,返回 新的细分图 中从节点 0 出发 可到达的节点数

示例

image-20221205143618111

输入:edges = [[0,1,10],[0,2,1],[1,2,2]], maxMoves = 6, n = 3
输出:13
解释:边的细分情况如上图所示。
可以到达的节点已经用黄色标注出来。

提示

  • 0 <= edges.length <= min(n * (n - 1) / 2, 10^4)
  • edges[i].length == 3
  • 0 < = u i < v i < n 0 <= u_i < v_i < n 0<=ui<vi<n
  • 图中 不存在平行边
  • 0 < = c n t i < = 1 0 4 0 <= cnt_i <= 10^4 0<=cnti<=104
  • 0 < = m a x M o v e s < = 1 0 9 0 <= maxMoves <= 10^9 0<=maxMoves<=109
  • 1 <= n <= 3000

思路

在细分图上操作

首先,考虑能不能暴力做。暴力做,就是先对细分后的图,进行建图,建完图后,原始节点细分后产生的新节点便没有什么区别。求解从0经过最多maxMoves可到达的节点数,那么只需要用BFS,对每个节点,求一个最短距离(距节点0的距离),然后统计那些距离小于等于maxMoves的节点,进行累加计数即可。思路非常简单。但是看一眼数据范围,发现边最多有 1 0 4 10^4 104,每条边上的细分节点数,最多有 1 0 4 10^4 104,那么细分节点的总数最大有 1 0 8 10^8 108,加上原始节点的3000。细分后的图中的节点数量,会达到 1 0 8 10^8 108级别,此时用BFS进行遍历,会访问每个节点一次,则时间复杂度会达到 1 0 8 10^8 108 级别,是会超时的。所以暴力做法不行。

那么只能在原始图上进行操作。

在原始图上操作

我一开始的思路很naive,就是遍历。(其实从上面暴力思路中能够看出,关键在于求解出每个点到节点0最短距离,但我一开始的思路只是遍历,还是用DFS遍历的)

具体思路是:从0开始,对原始图进行遍历,对已经走过的点打上标记,以及在访问某个节点时,需要带上剩余的可移动步数。对于边上(假设某条边的2个点为ab,那么用[a, b]来表示这条边)的细分点,用一个二维数组表示一下,cnt[a][b]表示从ab走,最多能经过多少个细分点,cnt[b][a]类似。由于从0到某个节点x可能有多条路径,我们当然要选最短的一个,这样的话,当走到x时,剩余的可移动步数才会最大。所以其实,就算是通过遍历,也应当使用BFS,而不是DFS,因为需要求解最短距离。而BFS才具有最短路的特性。

下面是我用BFS提交通过的代码

// C++  540ms
const int N = 3010, M = 2e4 + 10;
typedef pair<int, int> PII;
class Solution {
public:

    int h[N], e[M], ne[M], idx; // 图的邻接表表示

    int cnt[N][N]; // 某个边上的节点数

    int hasMoved[N][N]; // hasMoved[a][b] 表示边[a, b]上, 从a到b方向, 已经访问过的节点数量

    int maxRemain[N]; // maxRemain[a] 表示访问到节点a时,最大的剩余移动步数

    bool st[N]; // 是否已计数过该节点

    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx++;
    }


    int reachableNodes(vector<vector<int>>& edges, int maxMoves, int n) {
        memset(h, -1, sizeof h);
        memset(maxRemain, -1, sizeof maxRemain);
        // 建图
        for (auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b);
            add(b, a);
            cnt[a][b] = cnt[b][a] = c;
        }

        int ans = 0;
        queue<PII> q;
        q.push({0, maxMoves});
        maxRemain[0] = maxMoves;

        while (q.size()) {
            PII p = q.front();
            q.pop();
            int x = p.first, remainMoves = p.second;
            if (maxRemain[x] > remainMoves) continue; // 该节点在队列中有更大的remain, 则跳过该次的计算
            if (!st[x]) {
                ans++;
                st[x] = true;
                //还未计数过该原始节点时, 进行计数 
            }
            // 遍历邻接点
            for (int i = h[x]; i != -1; i = ne[i]) {
                int u = e[i];
                if (remainMoves >= cnt[x][u] + 1) {
                    // 能走过去, 则需要加上整条边上的细分节点数
                    // 但是由于这条边可能之前已经被走过了, 所以需要减掉一些已经走过的节点
                    // 这条线上, 已经走过的节点数, 从左从右都要计算
                    int hasMoveCnt = min(cnt[x][u], hasMoved[x][u] + hasMoved[u][x]);
                    ans += cnt[x][u] - hasMoveCnt; // 答案需要加上那些没走过的节点
                    hasMoved[x][u] = hasMoved[u][x] = cnt[x][u]; // 整条线都被走过, 更新走过的节点数
                    int newRemain = remainMoves - cnt[x][u] - 1; // 走过去后, 剩余的步数
                    if (newRemain > maxRemain[u]) {
                        // 如果当前节点的剩余步数比较大, 则入队列
                        // 当第一次遇到newRemain = 0时, 也需要将u入队, 所以需要初始化maxRemain为-1
                        maxRemain[u] = newRemain;
                        q.push({u, newRemain});
                    }
                } else {
                    // 无法走过去, 则看一下最多能走多少
                    // 因为是双向的, 要减掉重叠部分
                    // 首先看一下对侧走过来的, 走过了多少个节点
                    // 剩余能走的节点数, 要么是remainMoves要么是cnt[x][u] - hasMoved[u][x]
                    int canMaxMoves = min(remainMoves, cnt[x][u] - hasMoved[u][x]);
                    ans += max(0, canMaxMoves - hasMoved[x][u]); // 本侧能走的节点减去本侧已走的节点, 就是新走过的节点
                    hasMoved[x][u] = max(hasMoved[x][u], remainMoves); // 更新本侧走过的节点
                }
            }
        }

        return ans;
    }
};

其实从上面的代码,就能看出,此题其实就是需要求解一个最短路,我们其实可以将每条边上的细分点的个数,看成是这条边的权重。上述BFS在求最短距离时(上面求的是最大剩余步数,其实就是最短距离),对于某一个节点,可能会被多次访问,多次入队。所以效率其实比较低。

只要看出了这道题需要求最短路,并且有意识的把边上的细分点的个数,看成是边的权重,那么很容易就应该想到最短路的经典算法 —— Dijkstra。

因为此题边权都是正数,所以理应用Dijkstra。先用Dijkstra求出0点到每个点的最短距离,然后进行统计

  • 先统计原始图中的点,遍历0n - 1所有点,当点的距离小于等于maxMoves时进行计数
  • 再统计细分产生的新的点,遍历所有边,查看左右两个的点的最短距离,并计算这条边上的点最多有多少个能被访问到
typedef pair<int, int> PII;
class Solution {
public:

    int reachableNodes(vector<vector<int>>& edges, int maxMoves, int n) {
        int INF = 1e9;
        vector<int> d(n, INF); // 0号点到所有点的最短距离
        vector<bool> st(n);
        vector<vector<PII>> g(n); // 图的邻接表表示, 存邻接点以及边权
        for (auto& e : edges) {
            int a = e[0], b = e[1], w = e[2];
            g[a].push_back({b, w + 1}); // 边权为节点数加1
            g[b].push_back({a, w + 1});
        }
        // dijkstra
        // 小根堆优化
        priority_queue<PII, vector<PII>, greater<PII>> heap;
        d[0] = 0;
        heap.push({0, 0});
        while (heap.size()) {
            PII p = heap.top();
            heap.pop();
            int node = p.second;
            if (st[node]) continue; // 这个点的最短距离已经被确定了, 跳过
            st[node] = true; // 该点的最短距离已经确定
            // 更新出边
            for(auto& t : g[node]) {
                int u = t.first, w = t.second;
                if (!st[u] && d[u] > d[node] + w) {
                    d[u] = d[node] + w;
                    heap.push({d[u], u});
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (d[i] <= maxMoves) ans++;
        }
        for (auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            int left = max(maxMoves - d[a], 0); // a往b走最多能走的节点数
            int right = max(maxMoves - d[b], 0); // b往a走最多能走的节点数
            ans += min(left + right, c);
        }
        return ans;
    }
};

另外注意:朴素版的Dijkstra的时间复杂度为 O ( n 2 ) O(n^2) O(n2),堆优化版的Dijkstra的复杂度为 O ( m l o g n ) O(mlogn) O(mlogn)

(其中 n n n是节点数, m m m是边数)

当图是稀疏图时(边数较少),适合用堆优化版的Dijkstra

当图是稠密图时(边数非常多, m m m 基本达到了 n 2 n^2 n2 级别),适合用朴素版的Dijkstra

朴素版Dijkstra,建图采用邻接矩阵。一共需要 n n n次循环,每次确定一个点的最短距离。在每轮循环内,首先需要遍历 n n n 个点,从未确定最短距离的点中,找到距离最短的点,随后再更新这个点的所有出边的距离。

  • 外层循环为 n n n
  • 内层循环每次
    • 选出当前的最短距离的点(需要遍历所有点,计算量为 n n n
    • 更新这个点的全部出边(由于是邻接矩阵表示,实际计算量也是 n n n

所以朴素版Dijkstra复杂度为 O ( n 2 ) O(n^2) O(n2)

堆优化版Dijkstra,建图采用邻接表。一共需要 n n n 次循环,每次确定一个点的最短距离。在每轮循环内,可以用 O ( 1 ) O(1) O(1) 的开销获取当前距离最短的点,但是需要更新这个点的所有出边的点的距离。

  • 外层循环为 n n n
  • 内层循环
    • 每次用 O ( 1 ) O(1) O(1) 选出最短距离的点,但是pop完堆顶后还要调整堆,时间复杂度为 O ( l o g n ) O(logn) O(logn)
    • 需要更新这个点的所有出边的点的距离(需要插入到堆中,堆会进行调整)

总的来说,需要从堆中取 n n n 次堆顶。而每走一条边,就可能会更新一个点的距离,整个Dijkstra会访问每条边一次。

我们可以想象为,先把所有点插入到堆中,然后随着算法的进行,每走过一条边,就会更新一个点的距离,这个点在堆中的更新,消耗的时间为 O ( l o g n ) O(logn) O(logn),而一共有 m m m 条边,则需要更新 m m m次,则更新的时间复杂度为 O ( m l o g n ) O(mlogn) O(mlogn),而每次pop出堆顶后,调整堆需要 O ( l o g n ) O(logn) O(logn),而一共要pop n n n次,所以这部分的复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),那么总的复杂度是 O ( ( n + m ) l o g n ) O((n+m)logn) O((n+m)logn)

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

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

相关文章

182:vue+openlayers 使用d3实现地图区块呈现不同颜色的效果

第182个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+openlayers中加载解析geojson文件,同时利用d3的颜色功能,使得美国每个州呈现出不同的颜色区块,方便识别。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果; 注意如果OpenStreetMap无法加载,…

UNIX环境高级编程_文件IO_文件描述符

这篇文章记录文件描述符&#xff0c;下一篇文章记录文件描述表。 1 文件描述符 先说说什么是文件IO。文件的IO就是文件的输入输出&#xff0c;也就是文件的读写。读和写是以CPU为参考的&#xff0c;从CPU向文件中写入数据&#xff0c;就是写操作&#xff1b;从文件中读取数据…

Aviation turbofan starting model

Aviation turbofan starting model 涡扇发动机(Turbofan)即涡轮风扇发动机,来源于涡轮喷气发动机,主要是为了解决涡轮喷气发动机耗油率过高的问题。其结构特点是流过风扇的空气一部分进入压气机(内涵道),一部分进入由压气机外部通道(外涵道)流过,这部分气流不经过燃烧…

01 - Linux系统概要(再论计算机系统)

---- 整理自狄泰软件唐佐林老师课程 1. 再论计算机系统 计算机系统由躯体和灵魂两部分组成 – 躯体&#xff1a;构成计算机系统的电子设备&#xff08;硬件&#xff09; – 灵魂&#xff1a;指挥躯体完成动作的指令序列&#xff08;软件&#xff09; 躯体核心&#xff1a;中央…

2022-12-05 优化el-tree懒加载选人树

今后就都拼抵抗力了嗷 需求描述 此处有一棵懒加载树&#xff08;可选人&#xff09;&#xff0c;右侧展示已选中的人。且父子关联&#xff0c;可以通过选中一个部门勾选所有子节点。问题是&#xff0c;选中父节点&#xff0c;当子节点未加载时&#xff0c;是获取不到勾选的子…

Redis配置、持久化以及相命令

Redis 什么是Redis Redis&#xff08;远程字典服务器&#xff09;是一个开源的、使用C语言编写的NoSQL数据库 Redis 基于内存运行并支持持久化&#xff0c;采用key-value&#xff08;键值对&#xff09;的存储形式&#xff0c;是目前分布式架构中不可或缺的一环。 Redis服务…

新来的性能测试工程师工资25K,看了他做的性能测试,那才真叫牛

一直深耕于互联网行业的测试工作&#xff0c;前期测试主要以项目为主&#xff0c;也就是 一个人负责2-3个项目 的测试工作&#xff0c;当然包括项目上功能、自动化和性能等一切测试工作。 我有几个朋友也在互联网大厂工作&#xff0c;从他们当中了解到其实真正的互联网大厂&…

【前端CSS】网站都变成灰色了,它是怎么实现的?(含源代码解析)

目录&#xff1a;网站都变成灰色了&#xff0c;它是怎么实现的&#xff1f;一、前言二、如何实现的三、代码的理解3.1 CSS3 filter(滤镜) 属性3.2 定义和使用3.2.1 CSS动画演示3.2.2 JS语法演示3.3 浏览器支持3.4 CSS 语法3.5 Filter 函数四、实例展示4.1 模糊实例4.2 Brightne…

3、JSP——Servlet、IDEA创建Web项目、IDEA创建JSP页面

目录 一、Servlet的概念 二、Servlet的作用 三、IDEA中创建Web项目 四、手动部署 五、自动部署 1、IDEA部署Tomcat服务器 2、IDEA部署JavaWeb项目 3、JSP页面 一、Servlet的概念 &#xff08;1&#xff09;Servlet&#xff1a;Server Applet的简称&#xff0c;是运…

云服务器配置Code-Server环境并运行Python和C++

目录1、前言2、部署流程2.1 前置准备2.2 运行docker安装Code-Server3、运行Code-Server3.1 配置运行环境3.2 运行Python3.3 运行C1、前言 云服务器需要配置C开发环境&#xff0c;了解到有code-server这个VsCode提供的云端服务&#xff0c;因此选择在云服务器上部署。 2、部署流…

D3--FPGA IIC接口通信2022-12-05

1.IIC简介 1.1 IIC概述 IIC即 Inter-Integrated Circuit(集成电路总线&#xff09;&#xff0c;是由 Philips 半导体公司在八十年代初设计出来的一种简单、双向、二线制总线标准。多用于主机和从机在数据量不大且传输距离短的场合下的主从通信。主机启动总线&#xff0c;并产…

[附源码]计算机毕业设计少儿节目智能推荐系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Android 基础知识4-2.2常用控件提示(Toast)

效果图&#xff1a; 介绍&#xff1a; Toast是Android提供的“快显讯息”类&#xff0c;Toast类的使用非常简单&#xff0c;而且用途很多。比如&#xff0c;当退出应用程序时&#xff0c;可以用它来提示用户“需要更新”&#xff0c;或者当在输入框中输入文本时&#xff0c;可以…

含有双硫键的交联剂NHS-PEG1-SS-PEG1-NHS,NHS-SS-NHS,活性酯-双硫键-活性酯

基础产品数据&#xff08;Basic Product Data&#xff09;&#xff1a; 中文名&#xff1a;活性酯-双硫键-活性酯 英文名&#xff1a;NHS-SS-NHS&#xff0c;NHS-PEG1-SS-PEG1-NHS 结构式&#xff08;Structural&#xff09;&#xff1a; 详细产品数据&#xff08;Detailed Pro…

批量查询谷歌PR权重的方法有哪些?是什么影响着谷歌PR值?

批量查询谷歌PR权重的方法有哪些&#xff1f; 查询谷歌PR权重最简单最最直接的方法就是使用站长工具查询&#xff0c;具体操作如下&#xff1a; 首先打开站长工具&#xff0c;在域名输入框输入网站的域名&#xff08;一行一个&#xff09;&#xff1b; 然后勾选需要查询的功能&…

NumPy模块使用介绍

NumPy使用介绍1.NumPy科学计算库介绍和环境准备 ​ NumPy&#xff08;Numerical Python&#xff09;是Python的⼀种开源的数值计算扩展。提供多维数组对象&#xff0c;各种派⽣对象&#xff08;如掩码数组和矩阵&#xff09;&#xff0c;这种⼯具可⽤来存储和处理⼤型矩阵&…

Qt下多线程的四种使用方法总结及代码示例

文章目录前言一、继承QThread&#xff0c;重写run()函数二、继承QObject&#xff0c;使用moveToThread()函数三、继承QRunnable&#xff0c;重写run()函数&#xff0c;使用QThreadPool线程池四、使用QtConcurrent的run()函数五、示例代码六、下载链接总结前言 在之前的Qt开发工…

时间序列分析的基本流程(R语言版——实验篇)

数据处理 1.导入数据&#xff08;.csv&#xff09; 能导入绝大所数形式的格式文件 ex52<-read.table("C:\\Users\\33035\\Desktop\\习题5.2数据.txt",headerT,fileEncoding GBK) #header &#xff1a;T:表示留第一行 #fileEncoding:有中文时最好改为GBK 2.对数…

MySQL---DDL

MySQL简介DDL操作 文章目录MySQL简介DDL操作数据库分类关系型数据库&#xff08;SQL&#xff09;非关系型数据库&#xff08;NOSQL&#xff09;区别DBMSMySQL简介概念特点MySQL运行机制SQL通用语法结构化查询语言分类DDL操作数据库操作表查询创建数据类型数值类型字符串类型日期…

入行测试已经4年了 ,进阿里后迷茫了3个月,做完这个项目我决定离职....

转行测试 我是大专非计科&#xff0c;我转行之前从事的工作是商场管理&#xff0c;努力了4年左右的时间才做到楼层经理&#xff0c;但是工资太低并且事情太多&#xff0c;薪资才6K。 更多的是坚定了自己的想法&#xff0c;我要改变自己 恰好有几个大学同学在互联网公司工作&a…