蓝桥杯最短路(java实现)

题目

资源限制

内存限制: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]);
        }
    }
}
  1. 可以看到,使用Dijkstra算法不能通过所有数据。本题也不推荐使用Dijkstra算法来解决。因为题目提到有向图的部分边可能为负权,而Dijkstra算法并不能很好的解决有负权的图。

  1. 注意到代码中有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, 因为这个数的每个字节都是0x3fmemset是按字节来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]);
        }
    }
}
  1. Dijkstra算法不能解决带有负权边的问题,而Bellman-ford算法可以解决带有负权边的问题,是求解带负权边的单源最短路问题的经典算法。但本题用Bellman-ford算法也无法通过是因为其通过遍历所有的边来找到最短路径,对于数据量大的测试而言容易造成内存超限和运行超时。

  1. 需要注意的是,在遍历所有的“点对/边”进行松弛操作前,需要先对 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;
        }
    }
}
  1. 在Bellman-ford算法不可行的情况下,我们考虑使用SPFA算法。其显著特点是可以求含负权图的单源最短路径,且效率较高。在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。

  1. SPFA算法思想:动态逼近法->设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

  1. 队列函数操作:

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;
        }
    }
}

可以清楚的看到两者在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();

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值