wormholes(判断负环)

Time Limit: 2000ms

Memory Limit: 65536KB

64-bit integer IO format: %lld      Java class name: Main


While exploring his many farms, Farmer John has discovered a number of amazing wormholes. A wormhole is very peculiar because it is a one-way path that delivers you to its destination at a time that is BEFORE you entered the wormhole! Each of FJ's farms comprises N (1 ≤ N ≤ 500) fields conveniently numbered 1..N, M (1 ≤ M ≤ 2500) paths, and W (1 ≤ W ≤ 200) wormholes.

As FJ is an avid time-traveling fan, he wants to do the following: start at some field, travel through some paths and wormholes, and return to the starting field a time before his initial departure. Perhaps he will be able to meet himself :) .

To help FJ find out whether this is possible or not, he will supply you with complete maps to F (1 ≤ F ≤ 5) of his farms. No paths will take longer than 10,000 seconds to travel and no wormhole can bring FJ back in time by more than 10,000 seconds.


Input 

Line 1: A single integer, F. F farm descriptions follow.
Line 1 of each farm: Three space-separated integers respectively: N, M, and W
Lines 2..M+1 of each farm: Three space-separated numbers (S, E, T) that describe, respectively: a bidirectional path between S and E that requires T seconds to traverse. Two fields might be connected by more than one path.
Lines M+2..M+W+1 of each farm: Three space-separated numbers (S, E, T) that describe, respectively: A one way path from S to E that also moves the traveler back T seconds.


Output

Lines 1..F: For each farm, output "YES" if FJ can achieve his goal, otherwise output "NO" (do not include the quotes).


Sample Input

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

Sample Output

NO
YES

AC代码(485ms 744KB):

#include<iostream>
#include<cstring>
using namespace std;
int dis[1000];
int edge;
int n,m,w;
struct weight
{
		int s;
		int e;
		int t;
}wei[6000];
//上面也可以用class类,不过要加上public,因为默认是private。
bool bellman()
{
	bool flag;
	int j,k;
	for(k=0;k<n-1;k++)
	{
		flag=false;
		for(j=0;j<edge;j++)
		{//cout<<j<<"   "<<dis[wei[j].e]<<"   "<<dis[wei[j].s]<<"   "<<wei[j].t<<endl;
    		if(dis[wei[j].e]>dis[wei[j].s]+wei[j].t)
    		{
    			dis[wei[j].e]=dis[wei[j].s]+wei[j].t;
    			flag=true;
    		}
		}
		if(flag==false)
    	break;
	}
//迭代/松弛n-1次
	for(j=0;j<edge;j++){
            //cout<<j<<"  "<<dis[wei[j].e]<<"  "<<dis[wei[j].s]<<"  "<<wei[j].t<<endl;
		if(dis[wei[j].e]>dis[wei[j].s]+wei[j].t)
    		return true;}
	return false;
}
int main()
{
	int i,f;
	int x,y,time;
	cin>>f;
	while(f--)
	{
		memset(dis,0,sizeof(dis));
		cin>>n>>m>>w;
		edge=0;
		for(i=0;i<m;i++)
		{
			cin>>x>>y>>time;
			wei[edge].s=x;
			wei[edge].e=y;
			wei[edge++].t=time;
			wei[edge].s=y;
			wei[edge].e=x;
			wei[edge++].t=time;
		}
		for(i=0;i<w;i++)
		{
			cin>>x>>y>>time;
			wei[edge].s=x;
			wei[edge].e=y;
			wei[edge++].t=-time;
		}
		if(bellman())
    		cout<<"YES"<<endl;
		else
    		cout<<"NO"<<endl;
	}
	return 0;
}

以下部分摘自http://blog.sina.com.cn/s/blog_6803426101014l1v.html

(如有版权问题联系我删除)

Bellman-Ford算法

Bellman-ford算法是求含负权单源最短路径算法,效率很低,但代码很容易写。即进行不停地松弛(原文是这么写的,为什么要叫松弛,争议很大),每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个旗帜flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。Bellman-ford算法浪费了许多时间做无必要的松弛,所以SPFA算法用队列进行了优化,效果十分显著,高效难以想象。SPFA还有SLF,LLL,滚动数组等优化。

<Bellman-Ford算法>

  Dijkstra算法中不允许边的权是负权,如果遇到负权,则可以采用Bellman-Ford算法。

  Bellman-Ford算法能在更普遍的情况下(存在负权边)解决单源点最短路径问题。对于给定的带权(有向或无向)图 G=(V,E),其源点为s,加权函数 w是 边集 E 的映射。对图G运行Bellman-Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s到 图G的任意顶点v的最短路径d[v]。

  适用条件&范围

  1.单源最短路径(从源点s到其它所有顶点v);

  2.有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图);

  3.边权可正可负(如有负权回路输出错误提示);

  4.差分约束系统;

  Bellman-Ford算法描述:

  1,.初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0;

  2.迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)

  3.检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中

  描述性证明:

  首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。

  其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。

  在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1 条边,所以,只需要循环|v|-1 次。

  每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,怎么优化?单纯的优化是否可行?)

  如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。

  如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。

引申:SPFA算法

  算法简介

  SPFA(Shortest Path Faster Algorithm)是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。

算法流程

  算法大致流程是用一个队列来进行维护。 初始时将源加入队列。 每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束。

不过改的spfa算法,注意每个节点进入队列的次数至多为n-1次(一共n个节点),若进入大于等于n次了,则说明图中存在负权回路,此时正好满足题目中时光倒流的要求,另外注意,vector每次用的时候清空。
下面是spfa算法的简单说明:
我们用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是松弛:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
注意对刚出队列的节点的所有相邻节点都要做松弛操作,不管该节点是否在队列中,不在队列中的松弛点加入到队列即可。


自己的一些理解:

负环指的是一张图中存在权重只和为负数的环。

所以在松弛n-1次之后就可以得出除了回到起点的其他所有路径的最小值,这个值如果是负,并且加上回到起点的权重依然是负的话就说明是负环,其中可以有路径的权重是正值。

所以初始化memset最好用T的最大值或者更大,memset成0的话也可以,我代入第一组样例观察了一下,可以说很有代表性吧,由此也可以知道为什么要反复松弛,也可以自己再加一个点更明显。


解题中遇到的一些细节性的小问题:

memset的数字虽然不过大但是会溢出,原因是memset中间的数是四位二进制,并且有一个符号位。


让我纠结了很久的问题:为什么bellman判断时候松弛是要以n-1为循环次数的边界,明明根本不需要循环那么多次,最终的到解决有两点:

第一是虽然是以n-1为边界,但是一旦固定下来的话就break了; 

第二是与松弛的顺序有关,举了一个极端的例子(如图):

其实也就是在某一个松弛顺序下,一次只能更新一个点,从等于0开始,到小于n-1截至(也就是n-1次),如果不是负环的话:如果 memset时是0,那么最后一个是0不会变,所以在第n次就全部不会变了;如果 memset的是比较大的值,最后一个点的dis值也会保持不变,因为变了之后会一直变那就是负环了。

并且在此题中n-1这个条件用w+2来替换,得到的结果是对的,但是举个例子,如果n=5,n-1=4,w=1,w+2=3,如果卡这个条件的话可能会错,但是可能并没有刻意在这里设置障碍,因为基本都会直接用n-1(w+1是不可以的,因为只循环两次的话,在点数多的情况下,输入边顺序更多样,出错可能性更大)。


关于spfa的解法:

https://blog.csdn.net/Bepthslowly/article/details/53293219

0x7fffffff是long int的最大值。

关于原理,只要理解了bellman就基本理解了,其实就是看会不会有不变的情况,如果可以不变的话,在n时一定就不变了,也就是在n-1次是时最后一次改变(bfs)。

dfs的话,必须用dis[1]=0,其他点循环初始化为最大值,因为原理是,首先计算出由每一点出发的边数,检验边的末端的值是否大于初始点的值加权重,如果大的话就松弛更新dis值,然后标记末端点已经被更新过,接着把这个末端点当作起始点再次检验,直到出现被标记过的点重新满足被更新的条件并且被更新,如果不必更新的话,说明就算是末端被更新过也没什么关系,因为不表现出有负环。

为什么必须用inf初始化呢(bfs&dfs)?

首先解决一个问题:为什么必须先松弛才能继续对末端spfa?

因为这样才能标记该点被松弛过,如果直接把所有进行判断操作的末端都直接标记并且直接spfa(末端)(也就是还在循环体内但是与判断大小并列)会造成死循环,因为正常的路径也会两个端点不断spfa对方,flag=1;return;当然也无法解决,因为还有过程在进行。

所以为了spfa末端的点就必须保证第一次先松弛一遍,用0的话,很多点就不能得到更新,就不会spfa末端了,必须用inf。

其实也可以这样想,如果第一个点在不满足松弛条件下可以开始spfa第二个点那所有的点就互相之间胡乱spfa个没完了,就算可以输出个正确结果也会因为死循环出错。

出错可能是因为无限递归(加return也不行,可能同时运行的太多?),栈溢出。

错误代码:Process returned -1073741571 (0x00000FD)

bfs实质上也是递推,但是是用队列来递推。

关于bfs与dfs的对比探究主要参照:https://blog.csdn.net/hy1405430407/article/details/51089345

没有自己写程序,是在这位博主的程序上改动探究的。


弄懂这道题的过程也是写这篇文章的思路:bellman--spfa(bfs)--spfa(dfs)

首先是不懂bellman判断负环:

①为什么要比较大小?

②为什么松弛以n(农场数,也就是模型中定点数)-1为界?与w(虫洞数)有关系吗?

之后发现还有spfa解法:

bfs的方法很好理解,与bellman的想法差不多;

①dfs是什么原理?

②为什么不能在初始化时用0?在探究过程中顺便知道了递归的具体顺序。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值