问题描述
农夫约翰正在针对一个新区域的牛奶配送合同进行研究。他打算分发牛奶到T个城镇(标号为1..T),这些城镇通过R条标号为(1..R)的道路和P条标号为(1..P)的航路相连。
每一条公路i或者航路i表示成连接城镇Ai(1<=A_i<=T)和Bi(1<=Bi<=T)代价为Ci。每一条公路,Ci的范围为0<=Ci<=10,000;由于奇怪的运营策略,每一条航路的Ci可能为负的,也就是-10,000<=Ci<=10,000。
每一条公路都是双向的,正向和反向的花费是一样的,都是非负的。
每一条航路都根据输入的Ai和Bi进行从Ai->Bi的单向通行。实际上,如果现在有一条航路是从Ai到Bi的话,那么意味着肯定没有通行方案从Bi回到Ai。
农夫约翰想把他那优良的牛奶从配送中心送到各个城镇,当然希望代价越小越好,你可以帮助他嘛?配送中心位于城镇S中(1<=S<=T)。
输入格式
输入的第一行包含四个用空格隔开的整数T,R,P,S。
接下来R行,描述公路信息,每行包含三个整数,分别表示Ai,Bi和Ci。
接下来P行,描述航路信息,每行包含三个整数,分别表示Ai,Bi和Ci。
输出格式
输出T行,分别表示从城镇S到每个城市的最小花费,如果到不了的话输出NO PATH。
样例输入
6 3 3 4
1 2 5
3 4 5
5 6 10
3 5 -100
4 6 -100
1 3 -10
样例输出
NO PATH
NO PATH
5
0
-95
-100
数据规模与约定
对于20%的数据,T<=100,R<=500,P<=500;
对于30%的数据,R<=1000,R<=10000,P<=3000;
对于100%的数据,1<=T<=25000,1<=R<=50000,1<=P<=50000。
这题乍看之下是一个有向图和无向图的混合型,但是,我们可以知道在最短路,对于一条正权边,能经过的正权边当然是越少越好,可是题中有负权,但是题目说了有负权的边是有向边,也就是说只会经过一次(排除负环情况),这个就只能用Bellman-Ford算法,这个算法之前有讲过,还有一个优化,称为SPFA算法,但是没有详细讲这个优化,这次就算用这个算法解决这道题
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
public class Main
{
static int INF=100000*100000+1;
public static void main(String[] args)
{
Scanner sc=new Scanner(System.in);
int T=sc.nextInt();
int R=sc.nextInt();
int P=sc.nextInt();
int S=sc.nextInt();//四个题设要求
ArrayList<Edge> list[]=new ArrayList[T+1];//邻接表
int x,y,w;
for(int i=0;i<R;i++)
{
x=sc.nextInt();
y=sc.nextInt();
w=sc.nextInt();
if(list[x]==null)list[x]=new ArrayList<>();
if(list[y]==null)list[y]=new ArrayList<>();
list[x].add(new Edge(y,w));
list[y].add(new Edge(x,w));//对于无向边
}
for(int i=0;i<P;i++)
{
x=sc.nextInt();
y=sc.nextInt();
w=sc.nextInt();
if(list[x]==null)list[x]=new ArrayList<>();
list[x].add(new Edge(y,w));//对于有向边,观察有向边和无向边的建立由什么不同之处
}
int d[]=new int[T+1];//所有S到其他的可达的点的最短路都将存储在这里
boolean inQ[]=new boolean[T+1];//如果一个点入队,则置为true
for(int i=1;i<=T;i++)d[i]=INF;//初始化均为无穷大
d[S]=0;//自己到自己的距离为0
inQ[S]=true;//将起点入队的标志
Queue<Integer> queue=new LinkedList<>();
queue.add(S);
while(!queue.isEmpty())
{
int s=queue.poll();//出队
inQ[s]=false;//标志
if(list[s]==null)continue;//假设一个点只有入度没有出度,那么在上面的建图过程中是没有赋予ArrayList这个对象的,只有一个引用而已,防止出现运行错误
for(int i=0;i<list[s].size();i++)
{
Edge e=list[s].get(i);//顺次获得与s邻接的一条边的信息
if(d[s]<INF&&d[e.to]>d[s]+e.w)//松弛造作
{
d[e.to]=d[s]+e.w;
if(!inQ[e.to])//如果一个点的最短路被修改了信息,但没有入队,,那么就将其入队
{
queue.add(e.to);
inQ[e.to]=true;
}
}
}
}
for(int i=1;i<=T;i++)
{
if(d[i]==INF)//说明这点一次都没有经过松弛操作,那么说明这个点不可达
System.out.println("NO PATH");
else
System.out.println(d[i]);
}
}
}
class Edge//边的定义
{
int to,w;
Edge(int to,int w)
{
this.to=to;
this.w=w;
}
}
对于这个算法,我们可以这样理解,之所以要把一个经过松弛操作的点入队(假设之前没有在队列)是因为这个点的更新会对后续的点的最短路的更新产生影响,假设每个点都存在最短路或者不可达,那么经过若干轮循环,松弛操作将不会再发生,也因而,不会再有入队的情况,队列最终为空,因为题目要求了不会有负环。假如有负环,那么某些点的更新操作会一直进行会趋向于无穷小,会产生死循环,因为队列会一直不为空,对于这种情况的侦测,我们可以在设置一个数组update[T+1]来记录一个点的更新次数,如果一个点的更新次数超过了T-1次, 说明这个图存在负环,以此来终止程序。