第十二周项目4 判断顶点u到v是否有简单路径

本文介绍如何通过深度优先遍历算法判断图中的顶点u到顶点v是否存在不包含重复顶点的简单路径。在程序中,通过添加额外的v和has参数跟踪路径情况,当找到从u到v的路径时,has设为true,否则设为false。

       附:测试图结构及存储


这里写图片描述

 

 

/*
* Copyright (c)2015,烟台大学计算机与控制工程学院
* All rights reserved.
* 文件名称:项目3.cbp
* 作    者:朱希康
* 完成日期:2015年11月30日
* 版 本 号:v1.0


* 问题描述:判断顶点u到v是否有简单路径
* 输入描述:无
* 程序输出:测试数据
*/


 

#ifndef GRAPH_H_INCLUDED
#define GRAPH_H_INCLUDED

#define MAXV 100                //最大顶点个数
#define INF 32767       //INF表示∞
typedef int InfoType;

//以下定义邻接矩阵类型
typedef struct
{
    int no;                     //顶点编号
    InfoType info;              //顶点其他信息,在此存放带权图权值
} VertexType;                   //顶点类型

typedef struct                  //图的定义
{
    int edges[MAXV][MAXV];      //邻接矩阵
    int n,e;                    //顶点数,弧数
    VertexType vexs[MAXV];      //存放顶点信息
} MGraph;                       //图的邻接矩阵类型

//以下定义邻接表类型
typedef struct ANode            //弧的结点结构类型
{
    int adjvex;                 //该弧的终点位置
    struct ANode *nextarc;      //指向下一条弧的指针
    InfoType info;              //该弧的相关信息,这里用于存放权值
} ArcNode;

typedef int Vertex;

typedef struct Vnode            //邻接表头结点的类型
{
    Vertex data;                //顶点信息
    int count;                  //存放顶点入度,只在拓扑排序中用
    ArcN
<think>题目要求:求解带重边的有向图中从节点1出发到各点的最短路径,边权代表氧气消耗(非负?题目没有明确,但根据问题描述,道路消耗氧气,泊船回复氧气,所以边权可能为负?但用户后续要求无法到达输出1000000000,且没有提到负环,所以需要确认) 但用户问题描述中明确提到:“走每条路都会消耗小码君太空服中的氧气,幸运的是,太空基地之间有免费的泊船,不仅不消耗氧气,还可以回复氧气。” 这意味着边权可能为负数(回复氧气相当于负的消耗)。然而,在用户最后的查询中,要求求解“带重边的有向图”,并且没有提到负权边,但也没有说非负。所以我们需要考虑负权边的情况。 但是,用户要求输出从节点1到其他各点的最短距离,无法到达则输出1000000000(即10^9)。如果存在负权环,那么最短路径可能为负无穷,但题目没有说明这种情况如何处理。根据一般情况,如果存在负环,则某些点可能无法求出最短路径(因为可以无限减少)。 然而,用户提供的两个参考代码都是处理非负权重的(一个是数字三角形,一个是多重背包),所以可能用户实际需要的是非负权重?但题目描述明确有负权(回复氧气)。 因此,我们需要按照题目描述来处理:边权可能为负,并且图中可能有重边(即两个节点之间有多条有向边,我们需要保留最短的那条?因为重边中我们只关心消耗最小的那条,所以可以预处理,但注意:重边中可能有正有负,我们取最小值?) 但是,如果存在负权环,那么最短路径可能不存在(可以无限绕环来回复氧气,使得总消耗趋近于负无穷)。题目没有说明这种情况,所以我们需要判断负环,如果存在负环,则受负环影响的节点应该输出一个特殊值?但用户要求无法到达输出10^9,并没有说负环的情况。 重新阅读用户问题:“问从 s 基地移动到 t 基地最少消耗多少氧气。” 这里用户要求的是从1到其他各点,所以如果存在负环,那么从1到负环上的点以及负环能到达的点,最短路径都不存在(为负无穷)。但题目没有给出这种情况的处理方式。 然而,在用户最后的要求中,只提到“无法到达则输出1000000000”,并没有提到负环的情况。所以我们需要考虑: 1. 如果图中存在负权环,且从1号节点出发可以到达一个负权环,并且从该负权环可以到达目标节点,那么该目标节点的最短路径为负无穷(即不存在最小值)。 2. 但用户要求无法到达输出1000000000,而负环存在时,节点是可达的,只是没有最短路径(无限小)。所以我们需要区分两种情况: a. 节点不可达:输出1000000000。 b. 节点可达但存在负环:按照题目没有明确要求,但通常应该输出一个特殊值(如负无穷)?但用户没有给出,所以我们可以输出一个特殊值,或者题目保证没有负环? 由于题目描述中提到了“回复氧气”,但并没有说可以无限回复,所以可能图中没有负环?或者泊船只能坐一次?但题目没有说明。因此,为了安全,我们按照一般情况处理: 使用Bellman-Ford算法(或SPFA)来求解单源最短路径,并检测负环。 步骤: 1. 初始化:距离数组dist[1]=0,其他为无穷大(10^9数量级,但我们可以初始化为一个很大的数,比如0x3f3f3f3f,但注意这个数约等于10^9,所以符合要求)。 2. 进行n-1轮松弛操作(n为节点数)。 3. 再进行一轮松弛,如果还能更新,则说明存在负环。 4. 对于存在负环的情况,我们标记所有通过负环可达的节点(使用DFS/BFS遍历负环可达的节点),这些节点的最短路径不存在(负无穷)。但用户要求输出1000000000表示不可达,而这里节点是可达的,但没有最小值。所以我们需要输出什么? 题目要求:无法到达则输出1000000000。注意:不可达和存在负环是两种不同情况。 因此,我们需要输出三种情况: 1. 节点可达且存在最短路径:输出最短路径值(可能为负数,因为可以回复氧气)。 2. 节点不可达:输出1000000000。 3. 节点可达但存在负环(即没有最短路径):题目没有说明,但按照一般理解,这种情况应该输出一个特殊标记,比如负无穷?但题目没有要求。所以我们需要和用户确认,但这里用户没有给出。 由于用户最后要求“无法到达则输出1000000000”,我们可以这样处理: - 如果节点不可达,输出1000000000。 - 如果节点可达,但存在负环,我们也输出1000000000?或者输出一个更小的数?但题目没有说明。 但注意:负环存在时,节点是可达的,而且我们无法给出一个固定的最小值。所以,按照题目的字面要求,我们只处理不可达的情况(即没有路径)输出1000000000。而负环存在时,我们实际上可以到达,所以不应该输出1000000000。但是,由于负环存在,最短路径不存在,我们无法给出一个确定值。因此,我们需要特别处理:如果存在负环,则标记这些节点为负无穷,但输出上,由于用户没有要求,我们可以输出一个特殊值,比如-1000000000?但题目没有说。 考虑到用户提供的参考代码中,没有负权的情况,且题目背景是太空航行,可能不会出现无限回复的情况(即没有负环),所以我们假设图中没有负环。这样我们就可以使用Bellman-Ford或SPFA求出最短路径。 但是,题目要求处理重边,我们在读入时,对于同一条有向边(从u到v),保留权值最小的那条(因为多条边,我们当然选择消耗氧气最小的,即权值最小的边)。 算法选择: - 由于可能存在负权边,所以不能使用Dijkstra。 - 使用Bellman-Ford或SPFA。 由于节点数没有给出,但一般图论题目节点数在10^4以内,边数在10^5以内,所以SPFA效率较高。 步骤: 1. 读入节点数n,边数m。 2. 初始化邻接表(或使用邻接矩阵?但重边需要处理,所以用邻接表,但重边我们可以在输入时处理:对于同一条边(u,v)保留最小权值?或者不保留,在松弛时自然会选择最小的?但是重边在输入时直接处理可以节省空间和时间:用邻接矩阵g[u][v]保存最小权值?但节点数n未知,所以用邻接表,但邻接表存储每条边,重边不合并也可以,因为松弛的时候,一条边会被多次处理,但不会影响结果(因为如果有多条边,我们会在松弛时通过不同的边来更新,自然会更新到最小值)。所以我们可以不合并重边,直接存所有边。 但是,题目要求“带重边”,所以输入中可能有重边。我们有两种处理方式: a. 在输入时,对于相同的u,v,只保留最小权值的边(这样边的数量不会重复,节省时间)。 b. 保留所有边,然后在算法中,每次松弛遍历所有边,重边也会被遍历,这样会多花时间,但也可以。 由于边数m可能很大,保留所有重边可能导致边数过大,所以建议在输入时合并重边:对于每条边(u,v,w),我们只保留最小的w。 3. 使用SPFA算法: - 队列q,将起点1入队。 - dist数组,初始化为一个大数(比如0x3f3f3f3f,即十进制的1061109567,大于10^9,但用户要求输出10^9表示不可达,所以我们可以初始化为0x3f3f3f3f,然后最后如果dist[i]>=0x3f3f3f3f/2(因为存在负权,可能把无穷大更新小一点,所以用>=0x3f3f3f3f/2判断为不可达?但这样不严谨,因为负权可能把无穷大更新得更小?所以我们需要在SPFA中,对于不可达的节点,其dist不会更新,所以最后判断dist[i]是否为初始值(0x3f3f3f3f)?但注意,负权边可能更新其他节点,但不会更新到不可达节点(因为不可达节点没有被访问过)。所以最后如果dist[i] == 0x3f3f3f3f,则输出1000000000,否则输出dist[i]。 - 但是,如果存在负环,我们需要检测负环,并标记负环可达的节点,然后这些节点输出什么?题目没有说明,但用户要求不可达输出1000000000,而负环可达的节点是可达的,所以不能输出1000000000。因此,我们只能输出一个特殊值,但题目没有要求,所以我们暂时不处理负环?或者题目保证没有负环? 再次阅读题目:题目没有明确说没有负环,但泊船(负权边)可能不会形成环?或者形成环但总权值非负?所以为了安全,我们检测负环,如果存在负环,则输出一个特殊值(比如-1000000000)?但用户没有要求。 由于用户要求无法到达输出1000000000,所以我们可以这样处理: - 如果节点不可达(即没有路径),输出1000000000。 - 如果节点可达,且不存在负环,则输出最短路径值(可能为负)。 - 如果节点可达,但存在负环(即从1到该节点路径上存在负环),则输出一个特殊值(比如-1000000000)?但用户没有要求,所以我们可以输出一个比1000000000更小的负数,但题目没有说明。 然而,题目背景中,氧气消耗最小可以是负数(即回复的氧气大于消耗),所以负数输出是合理的。但负环会导致路径权值无限小,所以我们需要标记负环,并输出一个特殊标记。但用户没有给出,所以这里我们假设图中不存在负环(题目保证没有负环)?因为如果有负环,题目没有说明输出什么。 考虑到用户要求,我们只处理不可达的情况(输出1000000000),对于可达的节点,即使有负环,我们也输出最短路径?但这样不对,因为负环存在时,我们无法求出最短路径(可以无限小)。 因此,我们需要检测负环,并将负环可达的节点标记为负无穷,然后输出一个特殊值。但用户要求中没有,所以我们可以输出一个比1000000000小的负数(比如-10^9)?但题目没有说。 由于题目没有给出负环的处理,我们只能按照没有负环来假设。所以,我们使用SPFA算法,但不进行负环检测(因为题目保证没有负环),或者进行负环检测,如果检测到负环,则输出错误(但题目没有说)。 权衡之后,我们按照以下步骤: 1. 输入n, m。 2. 建立邻接表:对于每条边(u, v, w),我们直接加入邻接表(注意重边不需要合并,因为SPFA会处理,但合并重边可以提升效率,所以我们在输入时合并重边:对于相同的(u,v)取最小w)。 3. 使用SPFA求最短路,不进行负环检测(假设没有负环)。 4. 最后,对于每个节点i,如果dist[i] == INF(初始值),则输出1000000000,否则输出dist[i]。 但是,如果存在负环,SPFA在有负环的情况下会陷入死循环(或者我们需要限制迭代次数)。所以,为了安全,我们进行负环检测,如果检测到负环,则输出错误信息?但题目要求输出最短路径值。 由于题目背景中,泊船(负权边)是有限的,而且不会形成环?或者形成的环总权值非负?所以我们可以假设没有负环。 因此,我们使用SPFA算法(不检测负环),但设置一个最大迭代次数(比如n-1轮)?实际上,SPFA不需要显式的轮数,我们通过队列来实现,但为了检测负环,我们可以记录每个节点入队的次数,如果某个节点入队次数超过n次,说明存在负环。 所以,我们改进: - 增加一个cnt数组,记录节点入队次数。 - 在更新dist时,如果节点v入队次数超过n,则说明存在负环,我们标记负环,然后退出?但这样只能检测到一个负环,负环可达的节点可能没有全部标记。 由于用户要求输出从1到其他各点的最短路径,如果存在负环,那么负环可达的节点都没有最短路径(即我们无法给出一个确定值),所以我们需要标记所有负环可达的节点。 步骤: - 当发现一个节点入队次数>=n时,说明存在负环,我们记录该节点(比如标记为在负环中)。 - 然后,从该节点开始BFS,标记所有可达的节点(这些节点都是受负环影响的节点,即没有最短路径)。 - 最后,输出时,对于不可达的节点(dist[i]==INF),输出1000000000。 对于受负环影响的节点,输出一个特殊值(比如-1000000000)。 否则,输出dist[i]。 但用户没有要求输出特殊值,所以我们可以这样:受负环影响的节点也输出1000000000?但这样不合理,因为它们是可达的。或者输出一个更小的数?但题目没有说明。 经过权衡,题目背景中,负环的情况可能不会出现,所以我们先不处理负环,只处理不可达。如果用户数据有负环,那么程序会出错(死循环或错误结果)。所以,我们加入负环检测,如果检测到负环,则对于受负环影响的节点,我们输出一个特殊值(比如-1000000000),并在程序注释中说明。 总结步骤: 1. 输入:n, m。 2. 初始化邻接表:使用vector<Edge> g[N]。注意重边在输入时合并:用map<pair<int,int>, int> 记录(u,v)的最小权值,或者用邻接矩阵(但n未知,可能很大,所以不用)。我们可以在输入时,对于每条边,我们比较当前输入的w和已有的g[u][v]的边权,取最小值。但邻接表存储,我们可以用map<pair<int,int>, int> 来记录最小权重,然后构建邻接表时只存储最小权重的边(这样避免重边)。 另一种方法:用vector< pair<int, int> > g[N],然后输入时,对于每条边(u, v, w),我们直接加入g[u]中。然后,在SPFA中,对于同一个u,我们遍历所有v,这样重边也会被遍历,但SPFA会自然选择最短的路径(因为松弛操作会取最小值),所以重边不需要预处理,SPFA会处理。 3. 初始化: - dist[1]=0,其他为INF(0x3f3f3f3f)。 - 队列q,将1入队,标记入队状态inqueue[1]=true。 - cnt[1]=1(入队次数)。 4. 循环:队列非空: 取队首u,弹出,设置inqueue[u]=false。 遍历u的所有出边(u, v, w): 如果dist[u]+w < dist[v]: dist[v] = dist[u]+w 如果v不在队列中: 将v入队,inqueue[v]=true,cnt[v]++。 如果cnt[v] > n:说明存在负环,标记v,然后从v开始BFS标记所有可达节点(这些节点在负环中或受负环影响)。 5. 输出:对于每个节点i(从1到n): 如果i受负环影响:输出-1000000000(或其他特殊值) 否则如果dist[i] == INF:输出1000000000 否则:输出dist[i] 但是,负环检测在SPFA中,当某个节点入队次数超过n时,我们标记负环。但一个图中可能有多个负环,所以我们需要标记所有负环。我们可以这样:当发现一个节点v的入队次数>=n时,我们标记v,然后从v开始DFS/BFS,标记所有v可达的节点(这些节点都是受负环影响的节点,因为负环存在,所以v可达的节点都可以通过负环无限减少距离)。注意,负环可能不包含v,但v被负环影响(即负环在v的路径上),所以v的入队次数会超过n。因此,我们标记v可达的节点都是受负环影响的节点。 但是,我们只需要标记一次,因为一旦发现一个负环,我们就标记所有受负环影响的节点,然后退出?不行,因为可能还有另一个负环。所以,我们需要继续运行直到所有节点都标记?或者继续运行直到队列为空?这样会超时。所以,我们可以在发现负环时,标记受负环影响的节点,然后继续运行(但不再更新负环节点?)?这样比较复杂。 另一种做法:当发现一个节点v的入队次数>=n时,我们并不立即退出,而是记录这个负环,然后继续运行,直到队列为空,然后最后再统一处理负环可达的节点?但这样队列中还会继续更新负环节点,导致死循环。 因此,我们使用一个bool数组cycle,标记受负环影响的节点。当发现某个节点v的入队次数>=n时,我们设置一个标记,然后从这个节点v开始进行BFS,标记所有可达的节点为cycle[i]=true。然后,在后续的松弛中,如果遇到cycle[i]为true的节点,我们跳过更新(因为已经知道没有最短路径了)。但这样需要修改算法。 具体步骤: - 初始化cycle[i]=false。 - 在SPFA中,当更新dist[u]+w < dist[v]时,如果v已经被标记为cycle,则跳过更新。 - 当发现一个节点v的入队次数>=n时,我们创建一个队列q2,将v入队,然后BFS所有v可达的节点,标记cycle[i]=true(并且设置dist[i]为一个特殊值,比如-INF?但不需要,因为输出时我们根据cycle标记输出)。 - 然后,将v标记为cycle[v]=true,并且不将v的后继节点加入SPFA的队列(因为已经标记cycle,所以跳过)。 但是,这样修改后,算法继续运行,直到队列为空。 然而,这样实现比较复杂,而且题目可能没有负环,所以我们先不实现负环检测,如果遇到负环,我们只标记检测到的节点(但可能漏掉其他节点)。 由于时间限制,我们按照以下简单处理: - 如果检测到某个节点入队次数>=n,我们标记这个节点(cycle[node]=true),然后继续运行(不进行BFS扩展),这样可能漏掉其他节点,但我们可以通过多次检测:每次检测到入队次数>=n,就标记该节点,最后输出时,对于标记了cycle的节点,输出-1000000000。 但这样,可能只标记了负环上的部分节点,而负环可达的节点没有被标记。所以不完整。 因此,我们采用标准做法:在SPFA结束后,再进行一次BFS来标记所有负环可达的节点。具体: 1. 进行SPFA,记录每个节点入队次数,当发现某个节点v入队次数>=n时,我们将其加入一个集合S(负环节点候选)。 2. 然后,我们创建一个队列,将集合S中所有节点加入,进行BFS,标记所有可达的节点为cycle(受负环影响)。 注意:在SPFA过程中,我们可能没有收集到所有负环节点,但一旦有一个节点被检测到入队次数>=n,那么负环一定存在,并且负环上的节点不一定全部入队次数>=n,但负环上的节点至少有一个会被检测到(因为负环会导致节点被反复更新)。所以,我们只需要从一个被检测到的节点开始BFS,标记所有它可达的节点(包括负环上的节点和负环可达的节点)。 具体步骤: 在SPFA过程中: 当更新dist[v]时,如果v不在cycle中,则更新。 如果发现某个节点v的入队次数>=n: 我们创建一个队列q2,将v加入,标记cycle[v]=true,然后BFS:从v出发,遍历所有v的后继节点u,如果u还没有被标记cycle,则标记cycle[u]=true,并将u加入q2。 注意:这里BFS遍历的是原图(有向图)中v可达的所有节点。 然后,在SPFA中,对于已经被标记cycle的节点,我们跳过更新(因为已经知道没有最短路径了)。 但是,在BFS过程中,我们可能会标记一些节点,而这些节点的前驱节点可能还没有被标记,所以我们需要继续BFS直到所有可达节点都被标记。 因此,我们在SPFA中,一旦发现一个节点v的入队次数>=n,我们就进行BFS标记所有从v可达的节点(包括负环上的节点和负环可达的节点),然后继续SPFA(但跳过cycle节点)。 这样,我们可以在SPFA过程中实时标记负环影响的节点。 代码步骤: 初始化: dist[1]=0 inqueue[1]=true, queue.push(1), cnt[1]=1 cycle[1..n]=false while (!q.empty()): u = q.front(); q.pop(); inqueue[u]=false if (cycle[u]) continue; // 跳过负环节点 for each edge (u, v, w) in g[u]: if (cycle[v]) continue; // 跳过负环节点 if (dist[u] + w < dist[v]): dist[v] = dist[u] + w if (!inqueue[v]): inqueue[v]=true q.push(v) cnt[v]++ if (cnt[v] >= n): // 发现负环,从v开始BFS标记所有可达节点 queue<int> q2; q2.push(v); cycle[v] = true; while (!q2.empty()): int x = q2.front(); q2.pop(); for each edge (x, y, w2) in g[x]: if (!cycle[y]): cycle[y] = true; q2.push(y); 输出: for i from 1 to n: if (cycle[i]): cout << -1000000000 << " "; else if (dist[i] == INF): cout << 1000000000 << " "; else: cout << dist[i] << " "; 注意:在BFS标记负环可达节点时,我们使用新的队列q2,并且遍历所有边(包括重边),所以不会漏掉节点。 但是,在SPFA过程中,我们可能会多次发现负环节点(比如多个节点入队次数>=n),所以我们需要确保每个节点只被标记一次(cycle数组)。 最后,输出时,对于cycle[i]为true的节点,我们输出-1000000000(一个特殊值),表示没有最短路径(负环)。 这个方案可以处理负环,并且输出三种情况。 但是,题目要求从节点1到其他各点的最短距离,如果节点1在负环上,那么从1出发的所有节点都是负环可达的?不一定,因为负环可能只包含部分节点。所以,我们标记负环可达节点时,是从发现负环的节点v开始BFS,标记所有v可达的节点。注意:节点1可能不在负环上,但负环在路径上(比如1->...->v->负环->目标),所以v可达的节点包括目标节点。 因此,这个标记是合理的。 最后,考虑节点数n和边数m,SPFA的时间复杂度最坏O(n*m),而负环标记的BFS总复杂度O(n+m)(每次发现负环只做一次BFS,但可能多次发现负环?实际上,一旦一个节点被标记为cycle,它就不会再被访问(因为跳过更新),所以每个节点最多被标记一次,因此BFS的总复杂度是O(n+m))。 所以,整个算法复杂度O(n*m)(最坏),但实际中SPFA很快。 代码实现: 使用邻接表:vector<vector<pair<int, int>>> g(n+1); // g[u] = vector of (v, w) 注意:重边不需要预处理,因为SPFA会处理(重边存在,我们遍历u的出边时,会遍历到多条边到同一个v,但松弛操作会取最小的路径,所以重边不影响)。 但是,重边在输入时,我们直接加入g[u]中,这样g[u]中可能有多个(v,w)相同的边?但不会,因为我们是直接加入,所以会有重复。但这样会增加不必要的遍历,所以我们在输入时,对于同一条边(u,v),我们只保留最小权值?这样更高效。 因此,输入时,我们可以用map<pair<int,int>, int> 来记录(u,v)的最小权值,然后构建邻接表时只加入一次。 输入时处理重边: map<pair<int,int>, int> mp; for i in range(m): int u, v, w; cin >> u >> v >> w; if (mp.count({u,v})) { if (w < mp[{u,v}]) mp[{u,v}] = w; } else { mp[{u,v}] = w; } // 然后,将mp中的边加入邻接表 for (auto it : mp) { int u = it.first.first, v = it.first.second, w = it.second; g[u].push_back({v, w}); } 或者,我们不用map,而用二维数组(但n可能很大,不行),所以用map。 但注意:可能有自环(u==v),但题目没有说,所以按一般情况处理。 然后,SPFA实现。 注意:节点编号从1开始。 初始化: const long long INF = 0x3f3f3f3f3f3f3f3f; // 使用long long,因为边权可能很大?但题目没有说,但用户要求输出1000000000,所以边权绝对值在10^9以内,但总路径可能很大,所以用long long。 vector<long long> dist(n+1, INF); vector<int> cnt(n+1, 0); vector<bool> inqueue(n+1, false); vector<bool> cycle(n+1, false); queue<int> q; dist[1]=0; q.push(1); inqueue[1]=true; cnt[1]=1; 然后进行SPFA。 注意:负权边可能导致dist变为负数,所以INF要足够大(0x3f3f3f3f3f3f3f3f,约等于4.6e18)。 最后输出时,如果cycle[i]为true,则输出-1000000000;否则如果dist[i]==INF,则输出1000000000;否则输出dist[i]。注意:dist[i]可能为负数,所以直接输出。 但是,题目要求输出整数,所以用%lld或者cout。 另外,如果节点1在负环上,那么我们在SPFA开始后就会检测到负环(因为节点1的入队次数会超过n),然后标记所有1可达的节点(包括1),所以输出时,除了1以外的节点,如果1可达,则都被标记为cycle,输出-1000000000。 但是,节点1本身,我们标记为cycle[1]=true,然后输出-1000000000。但节点1的最短路径应该是0,但负环存在,所以节点1的最短路径不存在(因为可以经过负环无限减少),所以输出-1000000000是合理的。 因此,代码实现如下: 注意:SPFA中,当从队列中取出u时,如果u已经被标记为cycle,则跳过(不进行松弛)。 代码: #include <iostream> #include <vector> #include <queue> #include <map> #include <cstring> #include <climits> #include <algorithm> using namespace std; typedef long long LL; const LL INF = 0x3f3f3f3f3f3f3f3f; int main() { int n, m; cin >> n >> m; // 用map处理重边 map<pair<int, int>, LL> edgeMap; for (int i = 0; i < m; i++) { int u, v; LL w; cin >> u >> v >> w; // 注意:题目是有向图 if (edgeMap.count({u, v})) { if (w < edgeMap[{u, v}]) // 保留最小权值的边 edgeMap[{u, v}] = w; } else { edgeMap[{u, v}] = w; } } // 构建邻接表 vector<vector<pair<int, LL>>> g(n + 1); // (v, w) for (auto &e : edgeMap) { int u = e.first.first; int v = e.first.second; LL w = e.second; g[u].push_back({v, w}); } // 初始化 vector<LL> dist(n + 1, INF); vector<int> cnt(n + 1, 0); vector<bool> inqueue(n + 1, false); vector<bool> cycle(n + 1, false); // 标记节点是否在负环中(或受负环影响) queue<int> q; dist[1] = 0; q.push(1); inqueue[1] = true; cnt[1] = 1; while (!q.empty()) { int u = q.front(); q.pop(); inqueue[u] = false; // 如果u已经在负环中,跳过 if (cycle[u]) continue; for (auto &edge : g[u]) { int v = edge.first; LL w = edge.second; // 如果v已经在负环中,跳过 if (cycle[v]) continue; // 松弛操作 if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; if (!inqueue[v]) { q.push(v); inqueue[v] = true; cnt[v]++; // 检测负环:入队次数超过n if (cnt[v] > n) { // 标记负环:从v开始BFS标记所有可达节点 queue<int> q2; q2.push(v); cycle[v] = true; while (!q2.empty()) { int x = q2.front(); q2.pop(); for (auto &e2 : g[x]) { int y = e2.first; if (!cycle[y]) { cycle[y] = true; q2.push(y); } } } } } } } } // 输出 for (int i = 1; i <= n; i++) { if (cycle[i]) { cout << -1000000000; } else if (dist[i] == INF) { cout << 1000000000; } else { cout << dist[i]; } if (i < n) cout << " "; else cout << endl; } return 0; } 注意:输出格式,每个节点输出一个整数,空格分隔,最后换行。 但是,题目要求无法到达则输出1000000000,而负环节点输出-1000000000(我们自定义)。如果题目没有负环,那么负环输出不会出现。 测试:无负环情况,有重边情况,有负权边情况,有负环情况。 注意:在负环标记的BFS中,我们只标记节点,不改变dist值,所以dist值可能是一个很大的负数(但实际没有意义),我们输出时忽略dist,直接输出-1000000000。 这个程序可以满足要求。 但是,如果图中没有负环,那么负环检测部分不会执行,所以SPFA正常结束。 如果存在负环,那么一旦检测到某个节点入队次数超过n,我们就标记所有从该节点可达的节点(包括负环上的节点和负环可达的节点),然后这些节点在后续SPFA中会被跳过。 最后,输出时,负环节点输出-1000000000,不可达节点输出1000000000,其他输出dist[i]。 注意:INF的值(0x3f3f3f3f3f3f3f3f)足够大,可以表示不可达。 但是,题目中氧气消耗可能很大,但总路径和可能超过10^18?题目没有说,但用户要求输出1000000000表示不可达,所以如果总路径和超过10^18,但题目没有要求,我们使用long long。 因此,我们使用long long存储距离。 代码中,我们使用LL=long long。 最后,输出时,我们直接输出整数,不需要格式化。 注意:输入可能有重边,我们已经在输入时处理。 如果题目没有重边,map处理也不影响。 现在,我们按照用户要求,输出从节点1到其他各点的最短距离(节点1到节点1为0)。 如果节点1不可达节点i,则输出1000000000。 如果节点i在负环中,则输出-1000000000。 否则,输出距离(可能为负)。 这个程序可以满足要求。 注意:在负环检测的BFS中,我们使用了一个新的队列q2,并且只遍历一次(每个节点最多被标记一次),所以不会增加太多复杂度。 但是,在负环标记后,SPFA仍然会继续运行,直到队列为空(但负环节点被跳过,所以不会死循环)。 因此,我们提交这个代码。 由于题目背景中,泊船可能形成负权边,但负环可能不存在,所以这个程序可以处理。 如果用户数据没有负环,那么负环检测部分不会执行,程序效率与SPFA相同。 如果用户数据有负环,那么负环检测会标记所有受影响的节点,然后跳过,所以不会死循环。 所以,最终代码如上。</think>为了解决带重边的有向图中单源最短路径问题(允许负权边),我们使用SPFA(Shortest Path Faster Algorithm)算法,并处理负环检测。以下是完整的C++实现: ```cpp #include <iostream> #include <vector> #include <queue> #include <map> #include <climits> using namespace std; typedef long long LL; const LL INF = 0x3f3f3f3f3f3f3f3f; // 表示无穷大 const LL UNREACHABLE = 1000000000; // 不可达时的输出值 const LL NEG_CYCLE = -1000000000; // 负环影响的节点输出值 int main() { int n, m; cin >> n >> m; // 用map处理重边:保留(u,v)的最小权值 map<pair<int, int>, LL> edgeMap; for (int i = 0; i < m; i++) { int u, v; LL w; cin >> u >> v >> w; auto edge = make_pair(u, v); if (edgeMap.count(edge)) { if (w < edgeMap[edge]) edgeMap[edge] = w; } else { edgeMap[edge] = w; } } // 构建邻接表 vector<vector<pair<int, LL>>> graph(n + 1); for (auto &e : edgeMap) { int u = e.first.first; int v = e.first.second; LL w = e.second; graph[u].push_back({v, w}); } // 初始化数据结构 vector<LL> dist(n + 1, INF); // 最短距离 vector<int> count(n + 1, 0); // 节点入队次数 vector<bool> inQueue(n + 1, false); // 节点是否在队列中 vector<bool> inNegCycle(n + 1, false); // 节点是否受负环影响 queue<int> q; dist[1] = 0; q.push(1); inQueue[1] = true; count[1] = 1; // SPFA主循环 while (!q.empty()) { int u = q.front(); q.pop(); inQueue[u] = false; if (inNegCycle[u]) continue; // 跳过负环节点 for (auto &edge : graph[u]) { int v = edge.first; LL w = edge.second; if (inNegCycle[v]) continue; // 跳过负环节点 // 松弛操作 if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; if (!inQueue[v]) { q.push(v); inQueue[v] = true; count[v]++; // 检测负环:入队次数超过节点数 if (count[v] > n) { // 标记所有负环可达的节点 queue<int> q2; q2.push(v); inNegCycle[v] = true; while (!q2.empty()) { int x = q2.front(); q2.pop(); for (auto &e2 : graph[x]) { int y = e2.first; if (!inNegCycle[y]) { inNegCycle[y] = true; q2.push(y); } } } } } } } } // 输出结果 for (int i = 1; i <= n; i++) { if (inNegCycle[i]) { cout << NEG_CYCLE; } else if (dist[i] >= INF / 2) { // 考虑负权边的更新 cout << UNREACHABLE; } else { cout << dist[i]; } if (i < n) cout << " "; } cout << endl; return 0; } ``` ### 算法说明: 1. **重边处理**:使用`map<pair<int,int>, LL>`存储每条边的最小权值,确保同一条有向边只保留最优值[^1]。 2. **SPFA算法**: - 初始化距离数组`dist[]`,起点1的距离为0 - 使用队列存储待松弛节点 - 对每个出边进行松弛操作:若`dist[u] + w < dist[v]`则更新距离 3. **负环检测**: - 记录节点入队次数`count[]` - 当`count[v] > n`时,从`v`开始BFS标记所有可达节点为负环影响节点 4. **输出处理**: - 受负环影响节点:输出`-1000000000` - 不可达节点:输出`1000000000` - 正常情况:输出最短距离 ### 复杂度分析: - **时间复杂度**:平均$O(m)$,最坏$O(nm)$($n$为节点数,$m$为边数) - **空间复杂度**:$O(n + m)$ ### 测试建议: 1. 正常路径(无负权边) 2. 包含负权边但无环 3. 包含负权环 4. 存在重边 5. 不可达节点 ### 注意事项: - 使用`long long`防止溢出 - 负环检测后立即标记所有可达节点 - 输出时区分不可达和负环状态
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值