题目
资源限制
内存限制:256.0MB Java时间限制:3.0s
问题描述
给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。
输入格式
第一行两个整数n, m。接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。
输出格式
共n-1行,第i行表示1号点到i+1号点的最短路。
样例输入
3 3
1 2 -1
2 3 -1
3 1 2
样例输出
-1
-2
数据规模与约定
对于10%的数据,n = 2,m = 2。
对于30%的数据,n <= 5,m <= 10。
对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。
代码
Ⅰ.Dijkstra算法
import java.util.Scanner;
public class Main {
//不能设置为Integer.MAX_VALUE,否则Integer.MAX_VALUE相加会溢出导致出现负权
public static int INF = 0x3f3f3f3f;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(); //顶点数
int m = in.nextInt(); //边数
int[][] len = new int[n+1][n+1];
//初始化邻接矩阵
for (int i = 1; i < n+1; i++) {
for (int j = 1; j < n+1; j++) {
len[i][j] = INF;
}
}
for (int i = 0; i < m; i++) {
int u = in.nextInt();
int v = in.nextInt();
int l = in.nextInt();
len[u][v] = l;
}
int source = 1; //源点为1
dijstra(len, 1);//调用dijstra算法计算最短路径
}
public static void dijstra(int[][] len, int source) {
//最短路径长度
int[] shortest = new int[len.length];
//判断该点的最短路径是否求出
int[] visited = new int[len.length];
//存储输出路径
String[] path = new String[len.length];
//初始化出发节点
shortest[source] = 0; //设置出发结点的访问距离为0
visited[source] = 1; //设置出发节点被访问过
for (int i = 2; i < len.length; i++) {
int min = INF;
int index = -1;
for (int j = 2; j < len.length; j++) {
//已经求出最短路径的节点visit[j]==1排除在外无需判断
if (visited[j] == 0 && len[source][j] < min) {
min = len[source][j];
index = j;
}
}
//更新最短路径
shortest[index] = min;
visited[index] = 1;
//更新从index跳到其它节点的较短路径
for (int m = 1; m < len.length; m++) {
if (visited[m] == 0 && len[source][index] + len[index][m] < len[source][m]) {
len[source][m] = len[source][index] + len[index][m];
}
}
}
//打印最短路径
for (int i = 2; i < len.length; i++) {
System.out.println(shortest[i]);
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/cd4f7781c5ba0c3b7ce9a303c94c65f5.png)
可以看到,使用Dijkstra算法不能通过所有数据。本题也不推荐使用Dijkstra算法来解决。因为题目提到有向图的部分边可能为负权,而Dijkstra算法并不能很好的解决有负权的图。
注意到代码中有public static int INF = 0x3f3f3f3f; 我们经常需要设置一个常量用来代表"无穷大"。比如对于int类型的数,有的人会采用INT_MAX,即0x7fffffff作为无穷大。但是以INT_MAX为无穷大常常面临一个问题,即加一个其他的数会溢出。所以我们常采用0x3f3f3f3f来作为无穷大。
使用INF=0x3f3f3f3f的好处:
1. 0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即10^9数量级,而一般场合下的数据都是小于10^9的。
2. 0x3f3f3f3f * 2 = 2122219134, 无穷大相加依然不会溢出。
3. 可以使用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f, 因为这个数的每个字节都是0x3f。 memset是按字节来memset的,int有四个字节,就会把每个字节都变成0x3f。 一般要么memset成0,要么memset成-1,因为int 型 0每个字节都是0,-1每个字节都是-1.
Ⅱ. Bellman-ford算法
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int INF = 0x3f3f3f3f;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(); //n个顶点
int m = in.nextInt(); //m条边
//使用邻接矩阵来存储数据
int[][] len = new int[n+1][n+1];
//初始化邻接矩阵
for (int i = 1; i < n+1; i++) {
for (int j = 1; j < n+1; j++) {
len[i][j] = i == j ? 0 : INF;
}
}
for (int i = 0; i < m; i++) {
int u = in.nextInt();
int v = in.nextInt();
int l = in.nextInt();
len[u][v] = l;
}
int source = 1; //源点为1
getShortPaths(len,source,n); //调用函数求出最短距离
}
//返回第1个顶点到其它所有顶点之间的最短距离
public static void getShortPaths(int[][] len, int source, int n) {
int[] dist = new int[n+1]; //数组dist,存储最短距离
Arrays.fill(dist, INF); //初始化数组dist[]
dist[source] = 0; //令出发的dist[]为0,即dist[1]为0
for (int limit = 0; limit < n-1; limit++) {
int[] clone = dist.clone(); //对dist[]进行备份
for (int i = 1; i < n+1; i++) {
for (int j = 1; j < n+1; j++) {
//松弛操作使用了从 i 到 j 的当前最短距离来更新 dist[j]
//注意在迭代时使用备份clone[i]来进行更新操作
dist[j] = Math.min(dist[j], clone[i] + len[i][j]);
}
}
}
//打印最短路径
for (int i = 2; i < n+1; i++) {
System.out.println(dist[i]);
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/79f9bccf9a398a62643e1d9cf0a52af5.png)
Dijkstra算法不能解决带有负权边的问题,而Bellman-ford算法可以解决带有负权边的问题,是求解带负权边的单源最短路问题的经典算法。但本题用Bellman-ford算法也无法通过是因为其通过遍历所有的边来找到最短路径,对于数据量大的测试而言容易造成内存超限和运行超时。
需要注意的是,在遍历所有的“点对/边”进行松弛操作前,需要先对 dist 进行备份,否则会出现「本次松弛操作所使用到的边,也是在同一次迭代所更新的」,从而不满足边数限制的要求。举个 🌰,例如本次松弛操作使用了从 a 到 b 的当前最短距离来更新 dist[b],直接使用 dist[a] 的话,不能确保 dist[a]不是在同一次迭代中所更新,如果 dist[a] 是同一次迭代所更新的话,那么使用的边数将会大于 n 条。
Ⅲ. SPFA算法
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.Scanner;
public class Main {
static int leng[];
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(); //n个顶点
int m = in.nextInt(); //m条边
List<node>list[]=new ArrayList[n];//存储路径
for(int i=0;i<n;i++)//声明
{
list[i]=new ArrayList<>();
}
leng=new int[n];
boolean jud[]=new boolean[n];//判断是否在队列内
for(int i=1;i<n;i++) {leng[i]=Integer.MAX_VALUE;}//初始最长均为max
for(int i=0;i<m;i++)
{
int u = in.nextInt();
int v = in.nextInt();
int l = in.nextInt();
//此处给定顶点序号为1开始,所以u-1,v-1
list[u-1].add(new node(v-1, l));
}
Queue<Integer>q1=new ArrayDeque<Integer>();
q1.add(0);//第一个
while(!q1.isEmpty())
{
int x=q1.poll(); //获取队列头元素并删除该元素
jud[x]=false; //出队列的元素状态置为false
//list[x].size()为x的邻居个数
for(int i=0;i<list[x].size();i++)//遍历
{
int index=list[x].get(i).x;//x邻居该节点的编号
int length=list[x].get(i).leng;//x到这个邻居的距离
if(leng[index]>leng[x]+length)
{
leng[index]=leng[x]+length;
if(!jud[index])//队列中没有该点
{
q1.add(index); //将该元素加入队列尾部
jud[index]=true; //入队列,设置元素状态为true
}
}
}
}
for(int i=1;i<n;i++)
{
System.out.println(leng[i]);
}
}
static class node
{
int x;
int leng;
public node(int x,int leng)
{
this.x=x;
this.leng=leng;
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/2dbdf336490f417d173ed7a2fc33e4e2.png)
在Bellman-ford算法不可行的情况下,我们考虑使用SPFA算法。其显著特点是可以求含负权图的单源最短路径,且效率较高。在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。
SPFA算法思想:动态逼近法->设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
队列函数操作:
void add(Object e):将指定元素加入此队列的尾部
Object element():获取列队头部的元素,但是不删除该元素
boolean offer(Object e):将指定元素加入该队列的尾部,当使用有容量限制的队列时,
此方法通常比void add(Object e)方法更好,使用此方法时,如果发现队列已满无法添加时,会直接返回false
Object peek():获取队列头部的元素,但是不删除该元素,如果此队列为空,则返回null
Object poll():获取队列头部的元素,并删除该元素,如果此队列为空,则返回null
Object remove():获取队列头部的元素,并删除该元素,队列为空时抛出异常NoSuchElementException
Ⅳ. SPFA算法上加强输入输出
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
public class Main {
static int leng[];
public static void main(String[] args) throws IOException {
StreamTokenizer in=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();int n=(int)in.nval;
in.nextToken();int m=(int)in.nval;
List<node>list[]=new ArrayList[n];//存储路径
for(int i=0;i<n;i++)//声明
{
list[i]=new ArrayList<>();
}
leng=new int[n];
boolean jud[]=new boolean[n];//判断是否在队列内
for(int i=1;i<n;i++) {leng[i]=Integer.MAX_VALUE;}//初始最长均为max
for(int i=0;i<m;i++)
{
in.nextToken();int u=(int)in.nval;
in.nextToken();int v=(int)in.nval;
in.nextToken();int l=(int)in.nval;
//此处给定顶点序号为1开始,所以u-1,v-1
list[u-1].add(new node(v-1, l));
}
Queue<Integer>q1=new ArrayDeque<Integer>();
q1.add(0);//第一个
while(!q1.isEmpty())
{
int x=q1.poll(); //获取队列头元素并删除该元素
jud[x]=false; //出队列的元素状态置为false
//list[x].size()为x的邻居个数
for(int i=0;i<list[x].size();i++)//遍历
{
int index=list[x].get(i).x;//x邻居该节点的编号
int length=list[x].get(i).leng;//x到这个邻居的距离
if(leng[index]>leng[x]+length)
{
leng[index]=leng[x]+length;
if(!jud[index])//队列中没有该点
{
q1.add(index); //将该元素加入队列尾部
jud[index]=true; //入队列,设置元素状态为true
}
}
}
}
for(int i=1;i<n;i++)
{
out.println(leng[i]);
}
out.flush();
}
static class node
{
int x;
int leng;
public node(int x,int leng)
{
this.x=x;
this.leng=leng;
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/e2fb8cdffe7ec8c2bfa382c06dabd1ca.png)
![](https://i-blog.csdnimg.cn/blog_migrate/3a83105366434d6d2b818a5791330120.png)
可以清楚的看到两者在CPU与内存使用上的对比。
StreamTokenized类
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
public class Tian {
public static void main(String[]args) throws IOException {
StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
st.nextToken();//每次输入前都要加
int n = (int) st.nval;//st.nval默认解析出的类型是double,记得强转
st.nextToken();
String c = st.sval;//等同next
// while (st.nextToken() != StreamTokenizer.TT_EOF)//EOF终止
// while (st.nextToken() != StreamTokenizer.TT_EOL)//\n终止
}
}
PrintWriter类
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
····
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
out.println(x);
out.flush();