<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`防止溢出
- 负环检测后立即标记所有可达节点
- 输出时区分不可达和负环状态