题解:虫洞问题
一、题目背景
农夫约翰的每个农场包含 (N) 片田地、(M) 条双向路径以及 (W) 个单向虫洞,虫洞可以让人回到过去某个时刻。约翰希望从某片田地出发,经过一些路径和虫洞,能在出发时刻之前回到出发地,需要判断是否能实现。
二、解题思路
本题可将农场的田地看作图的节点,路径和虫洞看作边,通过判断图中是否存在负权回路来解决。由于负权回路意味着可以不断绕圈使总时间减少,从而有可能回到出发时刻之前,所以使用SPFA(Shortest Path Faster Algorithm)算法判断负权回路。
具体步骤如下:
1. 定义图的存储结构,包括邻接表相关数组、距离数组、队列数组、记录每个节点入队次数的数组以及节点是否在队列中的标记数组。
2. 编写添加边的函数,用于构建邻接表。
3. 实现SPFA算法,通过不断更新节点距离,判断是否存在某个节点入队次数达到节点总数(即存在负权回路)。
4. 在 main
函数中,读取农场数量 (T),对于每个农场:
- 读取田地数量 (n)、路径数量 (m1) 和虫洞数量 (m2)。
- 初始化邻接表,读取路径信息并添加双向边,读取虫洞信息并添加单向负权边。
- 调用SPFA算法判断是否存在负权回路,根据结果输出 YES
或 NO
。
三、代码逐段分析
(一)头文件和全局变量
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 5210;
int n, m1, m2;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int q[N], cnt[N];
bool st[N];
- 头文件:
#include <cstdio>
:用于标准输入输出函数,如scanf
和printf
。#include <cstring>
:用于字符串操作和内存初始化函数,如memset
。#include <iostream>
:提供C++风格的输入输出流,如cin
和cout
。#include <algorithm>
:包含一些常用的算法函数。using namespace std;
:使用标准命名空间,以便直接使用标准库中的函数和类型。
- 常量定义:
const int N = 510, M = 5210;
:定义田地数量的最大值和边数量的最大值。
- 变量定义:
int n, m1, m2;
:分别表示田地数量、路径数量和虫洞数量。int h[N], e[M], w[M], ne[M], idx;
:邻接表相关数组,h[N]
存储每个节点的头指针,e[M]
存储边的终点,w[M]
存储边权,ne[M]
存储下一条边的指针,idx
用于边的编号。int dist[N];
:记录从源点到每个节点的距离。int q[N], cnt[N];
:q[N]
是队列数组,用于SPFA算法;cnt[N]
记录每个节点入队的次数。bool st[N];
:标记每个节点是否在队列中。
(二)添加边函数
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
该函数用于向邻接表中添加一条边,参数 a
为起点,b
为终点,c
为边权。通过更新邻接表数组,将边的信息存储起来。
(三)SPFA算法函数
bool spfa()
{
memset(dist, 0, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
q[tt ++ ] = i;
st[i] = true;
}
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
- 初始化:
memset(dist, 0, sizeof dist);
:将距离数组初始化为0。memset(cnt, 0, sizeof cnt);
:将每个节点的入队次数初始化为0。memset(st, 0, sizeof st);
:将节点在队列中的标记初始化为false
。
- 入队操作:
- 将所有节点入队,标记为在队列中。
- 队列处理:
- 当队列不为空时,取出队头节点
t
,标记为不在队列中。 - 遍历节点
t
的所有邻接边,如果通过节点t
到邻接节点j
的距离更短,则更新距离dist[j]
,入队次数cnt[j]
加1。 - 如果某个节点的入队次数
cnt[j]
达到节点总数n
,说明存在负权回路,返回true
。 - 如果邻接节点
j
不在队列中,则将其入队并标记为在队列中。
- 当队列不为空时,取出队头节点
- 返回结果:
- 遍历完所有边后,若没有发现负权回路,返回
false
。
- 遍历完所有边后,若没有发现负权回路,返回
(四)main
函数
int main()
{
int T;
scanf("%d", &T);
while (T -- )
{
scanf("%d%d%d", &n, &m1, &m2);
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < m1; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < m2; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, -c);
}
if (spfa()) puts("YES");
else puts("NO");
}
return 0;
}
- 读取农场数量:
scanf("%d", &T);
:读取农场的数量T
。
- 处理每个农场:
- 对于每个农场,读取田地数量
n
、路径数量m1
和虫洞数量m2
。 - 初始化邻接表,将头指针数组
h
初始化为-1
,边编号idx
初始化为0。 - 读取路径信息,添加双向边。
- 读取虫洞信息,添加单向负权边。
- 对于每个农场,读取田地数量
- 判断并输出结果:
- 调用SPFA算法判断是否存在负权回路。
- 如果存在负权回路,输出
YES
;否则,输出NO
。
- 结束程序:
return 0;
:程序正常结束。
四、时间复杂度和空间复杂度分析
(一)时间复杂度
- 输入和建图:读取每个农场的信息并构建邻接表,时间复杂度为 (O(m1 + m2)),其中 (m1) 是路径数量,(m2) 是虫洞数量。
- SPFA算法:在最坏情况下,每个节点都可能入队 (n) 次,每次入队处理其邻接边,时间复杂度为 (O(nm)),这里 (m = m1 + m2)。但在实际情况中,SPFA算法通常比这个复杂度要快。
总体时间复杂度为 (O(T(nm + m1 + m2))),其中 (T) 是农场的数量。
(二)空间复杂度
- 邻接表:存储边的信息,空间复杂度为 (O(m)),其中 (m = m1 + m2)。
- 其他数组:距离数组
dist[N]
、队列数组q[N]
、入队次数数组cnt[N]
和标记数组st[N]
,空间复杂度均为 (O(n))。
总体空间复杂度为 (O(n + m))。
希望这份题解能帮助你理解这道题的解题思路和代码实现。如果有任何疑问,欢迎随时提问。