AcWing 904 虫洞

本文介绍了一种利用SPFA算法解决农夫约翰通过虫洞回到过去的问题,详细解析了如何判断图中是否存在负权回路,从而实现从任意起点出发并在出发前返回的挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述:

农夫约翰在巡视他的众多农场时,发现了很多令人惊叹的虫洞。

虫洞非常奇特,它可以看作是一条 单向 路径,通过它可以使你回到过去的某个时刻(相对于你进入虫洞之前)。

农夫约翰的每个农场中包含N片田地,M条路径(双向)以及W个虫洞。

现在农夫约翰希望能够从农场中的某片田地出发,经过一些路径和虫洞回到过去,并在他的出发时刻之前赶到他的出发地。

他希望能够看到出发之前的自己。

请你判断一下约翰能否做到这一点。

下面我们将给你提供约翰拥有的农场数量F,以及每个农场的完整信息。

已知走过任何一条路径所花费的时间都不超过10000秒,任何虫洞将他带回的时间都不会超过10000秒。

输入格式

第一行包含整数F,表示约翰共有F个农场。

对于每个农场,第一行包含三个整数N,M,W。

接下来M行,每行包含三个整数S,E,T,表示田地S和E之间存在一条路径,经过这条路径所花的时间为T。

再接下来W行,每行包含三个整数S,E,T,表示存在一条从田地S走到田地E的虫洞,走过这条虫洞,可以回到T秒之间。

输出格式

输出共F行,每行输出一个结果。

如果约翰能够在出发时刻之前回到出发地,则输出“YES”,否则输出“NO”。

数据范围

1≤F≤5
1≤N≤500,
1≤M≤2500,
1≤W≤200,
1≤T≤10000,
1≤S,E≤N

输入样例:

2
3 3 1
1 2 2
1 3 4
2 3 1
3 1 3
3 2 1
1 2 3
2 3 4
3 1 8

输出样例:

NO
YES

分析:

本题属于spfa求负环的裸题,题目意思大概是农村中有若干个田地,通过田地间的某些边需要消耗一定的时间,而一些边上存在虫洞,走过去便可以回到过去,也就相当于负权边。问从某片田地出发能不能在出发时刻前回到起点?注意这里的问法意味着我们可以任选起点,只要从这点出发最后在出发之前回到该点即可,也就是说只要图中存在负权回路就可以了。之前在AcWing 852 spfa判断负环里已经介绍了spfa算法求负环的解法,这里再简单的复习下。

负权回路是指图中的某个回路中所有边权之和是负数,意味着只要沿着这个回路一直走,所走的总路程就会越来越小。我们知道,BellmanFord算法是遍历所有的边,一轮轮去松弛相邻的点,最多n-1轮后,起点就可以松弛到终点了。而spfa算法,只将这轮松弛过的点加入到队列,因为只有上一轮被松弛的点才可能继续去松弛周围的点。没有负权回路的话,从起点到终点的最短路径至多只会经过n - 1条边,如果从起点经过了不少于n条边还在松弛周围的点,则说明肯定存在负权回路。可以用cnt[i]表示松弛到i点已经经过的边数,一旦出现cnt的值达到n的点,就说明存在负权回路了。

之前实现spfa算法使用的是stl里现成的queue,这里采用数组模拟队列,队列里至多有n个点同时在队列里,而每个点可能不止入队一次,所以可以使用循环队列,当hh和tt到达队列上限N的时候就归零即可。循环队列的终止条件就是hh == tt时,因为队列空间循环使用的缘故,就不能使用hh < tt作为循环条件了。

还有个值得注意的点是,spfa算法求最短路时需要将距离数组初始化为无穷大,只给起点的距离赋初值0,那是因为我们需要知道准确的路径长度,而判断负环是否存在的问题我们不关心具体回路的长度,只关心它是不是负数,所以初值可以一律设置为0,当遇见负权边的时候,即使d是0也会被松弛的。另外需要注意的是多组测试用例时,不要忘了给链表头数组h赋初值-1,以及给边的序号idx赋初值0,以防止数组越界。具体实现见代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 505,M = 5205;
int idx,h[N],e[M],w[M],ne[M];
int n,m1,m2,d[N],cnt[N],q[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++;
}
bool spfa(){
    memset(st,false,sizeof st);
    memset(d,0,sizeof d);
    memset(cnt,0,sizeof cnt);
    int hh = 0,tt = 0;
    for(int i = 1;i <= n;i++){
        q[tt++] = i;
        st[i] = true;
    }
    while(hh != tt){
        int u = q[hh++];
        if(hh == N) hh = 0;
        st[u] = false;
        for(int i = h[u];~i;i = ne[i]){
            int j = e[i];
            if(d[u] + w[i] < d[j]){
                d[j] = d[u] + w[i];
                cnt[j] = cnt[u] + 1;
                if(cnt[j] >= n) return true;
                if(!st[j]){
                    q[tt++] = j;
                    if(tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d%d",&n,&m1,&m2);
        int a,b,c;
        memset(h,-1,sizeof h);
        idx = 0;
        for(int i = 0;i < m1;i++){
            scanf("%d%d%d",&a,&b,&c);
            add(a,b,c),add(b,a,c);
        }
        for(int i = 0;i < m2;i++){
            scanf("%d%d%d",&a,&b,&c);
            add(a,b,-c);
        }
        if(spfa())  puts("YES");
        else    puts("NO");
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值