最短路径问题

12 篇文章 0 订阅
5 篇文章 4 订阅

Dijkstra单源最短路径算法

  • 前提:图中不能有负权边
  • 复杂度:O(ElogV)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码


/*
 * Created by wxn
 * 2018/12/17 23:12
 */

import java.util.Stack;
import java.util.Vector;

/**
 * Dijkstra单源最短路径算法
 */
public class Dijkstra<Weight extends Number & Comparable<Weight>> {

    private WeightGraph<Weight> graph;		//图的引用
    private int s;				//起始点
    private Number[] distTo;			//distTo[i]记录从起始点s到i的最短路径长度
    private boolean[] marked;			//标记数组,在算法运行过程中标记节点i是否被访问
    private Edge[] from;			//from[i]记录最短路径中,到达i节点的是哪一条边
    						//用来回复整个最短路径


    public Dijkstra(WeightGraph<Weight> graph, int s) {
	this.graph = graph;
	this.s = s;
	distTo = new Number[graph.V()];
	marked = new boolean[graph.V()];
	from = new Edge[graph.V()];
	for (int i = 0; i < graph.V(); i++) {
	    distTo[i] = 0.0;
	    marked[i] = false;
	    from[i] = null;
	}

	// 使用索引堆记录当前找到的到达每个顶点的最短距离
	IndexMinHeap<Weight> indexMinHeap = new IndexMinHeap<>(graph.V());

	//对起始点s初始化
	distTo[s] = 0.0;
	from[s] = new Edge<>(s, s, (Weight) (Number) 0.0);
	indexMinHeap.insert(s, (Weight) distTo[s]);
	marked[s] = true;
	while (!indexMinHeap.isEmpty()){
	    int v = indexMinHeap.extractMinIndex();
	    // distTo[v]就是s到v的最短距离
	    marked[v] = true;

	    //对v的所有相邻节点进行更新
	    for (Edge<Weight> edge : graph.adj(v)) {
		int w = edge.other(v);
		//如果从s到w点的最短路径还没有找到
		if (!marked[w]){
		    //如果w点以前没有访问过
		    //或者访问过,但是通过当前的v点到w距离更短 则更新
		    if (from[w]==null || distTo[v].doubleValue()+ edge.wt().doubleValue()<distTo[w].doubleValue()){
		        distTo[w] = distTo[v].doubleValue()+ edge.wt().doubleValue();
		        from[w] = edge;
		        if (indexMinHeap.contain(w)){
		            indexMinHeap.change(w, (Weight) distTo[w]);
			}else {
		            indexMinHeap.insert(w, (Weight) distTo[w]);
			}
		    }
		}
	    }
	}
    }

    // 返回从s点到w点的最短路径长度
    Number shortestPathTo( int w ){
	assert w >= 0 && w < graph.V();
	assert hasPathTo(w);
	return distTo[w];
    }

    // 判断从s点到w点是否联通
    boolean hasPathTo( int w ){
	assert w >= 0 && w < graph.V() ;
	return marked[w];
    }

    // 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
    Vector<Edge<Weight>> shortestPath(int w){

	assert w >= 0 && w < graph.V();
	assert hasPathTo(w);

	// 通过from数组逆向查找到从s到w的路径, 存放到栈中
	Stack<Edge<Weight>> s = new Stack<>();
	Edge<Weight> e = from[w];
	while( e.v() != this.s ){
	    s.push(e);
	    e = from[e.v()];
	}
	s.push(e);

	// 从栈中依次取出元素, 获得顺序的从s到w的路径
	Vector<Edge<Weight>> res = new Vector<>();
	while( !s.empty() ){
	    e = s.pop();
	    res.add( e );
	}

	return res;
    }
    // 打印出从s点到w点的路径
    void showPath(int w){

	assert w >= 0 && w < graph.V();
	assert hasPathTo(w);

	Vector<Edge<Weight>> path =  shortestPath(w);
	for( int i = 0 ; i < path.size() ; i ++ ){
	    System.out.print( path.elementAt(i).v() + " -> ");
	    if( i == path.size()-1 )
		System.out.println(path.elementAt(i).w());
	}
    }

    public static void main(String args[]) {
	String filename = "testG1.txt";
	int V = 5;

	SparseWeightedGraph<Double> g = new SparseWeightedGraph<>(V, false);
	// Dijkstra最短路径算法同样适用于有向图
	//SparseGraph<int> g = SparseGraph<int>(V, false);
	ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);

	System.out.println("Test Dijkstra:\n");
	Dijkstra<Double> dij = new Dijkstra<>(g,0);
	for( int i = 1 ; i < V ; i ++ ){
	    if(dij.hasPathTo(i)) {
		System.out.println("Shortest Path to " + i + " : " + dij.shortestPathTo(i));
		dij.showPath(i);
	    }
	    else
		System.out.println("No Path to " + i );

	    System.out.println("----------");
	}
    }


}

运行结果

在这里插入图片描述
在这里插入图片描述

Bellman-Ford单源最短路径算法

  • 前提:图中不能有负权环
  • 如果一个图没有负权环,从一点到另外一点的最短路径,最多经过所有的v个顶点,有v-1条边
  • 否则,存在顶点经过了两次,即存在负权环
  • 对所有的点进行v-1次松弛操作,理论上就找到了从原点到其他所有点的最短路径
  • 如果还可以继续松弛,说明原图中有负权环

代码


/*
 * Created by wxn
 * 2018/12/18 22:31
 */


import java.util.Stack;
import java.util.Vector;

/**
 * BellmanFord单源最短路径算法
 */
public class BellmanFord<Weight extends Number & Comparable<Weight>>{

    private WeightGraph<Weight> G;    // 图的引用
    private int s;              // 起始点
    private Number[] distTo;    // distTo[i]存储从起始点s到i的最短路径长度
    Edge<Weight>[] from;        // from[i]记录最短路径中, 到达i点的边是哪一条
    // 可以用来恢复整个最短路径
    boolean hasNegativeCycle;   // 标记图中是否有负权环

    // 构造函数, 使用BellmanFord算法求最短路径
    public BellmanFord(WeightGraph graph, int s){

	G = graph;
	this.s = s;
	distTo = new Number[G.V()];
	from = new Edge[G.V()];
	// 初始化所有的节点s都不可达, 由from数组来表示
	for( int i = 0 ; i < G.V() ; i ++ )
	    from[i] = null;

	// 设置distTo[s] = 0, 并且让from[s]不为NULL, 表示初始s节点可达且距离为0
	distTo[s] = 0.0;
	from[s] = new Edge<>(s, s, (Weight)(Number)(0.0));

	// Bellman-Ford的过程
	// 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离
	for( int pass = 1 ; pass < G.V() ; pass ++ ){

	    // 每次循环中对所有的边进行一遍松弛操作
	    // 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边
	    for( int i = 0 ; i < G.V() ; i ++ ){
		// 使用我们实现的邻边迭代器遍历和所有顶点相邻的所有边
		for( Object item : G.adj(i) ){
		    Edge<Weight> e = (Edge<Weight>)item;
		    // 对于每一个边首先判断e->v()可达
		    // 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()]
		    // 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离, 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()]
		    if( from[e.v()] != null && (from[e.w()] == null || distTo[e.v()].doubleValue() + e.wt().doubleValue() < distTo[e.w()].doubleValue()) ){
			distTo[e.w()] = distTo[e.v()].doubleValue() + e.wt().doubleValue();
			from[e.w()] = e;
		    }
		}
	    }
	}

	hasNegativeCycle = detectNegativeCycle();
    }

    // 判断图中是否有负权环
    boolean detectNegativeCycle(){

	for( int i = 0 ; i < G.V() ; i ++ ){
	    for( Object item : G.adj(i) ){
		Edge<Weight> e = (Edge<Weight>)item;
		if( from[e.v()] != null && distTo[e.v()].doubleValue() + e.wt().doubleValue() < distTo[e.w()].doubleValue() )
		    return true;
	    }
	}

	return false;
    }

    // 返回图中是否有负权环
    boolean negativeCycle(){
	return hasNegativeCycle;
    }

    // 返回从s点到w点的最短路径长度
    Number shortestPathTo( int w ){
	assert w >= 0 && w < G.V();
	assert !hasNegativeCycle;
	assert hasPathTo(w);
	return distTo[w];
    }

    // 判断从s点到w点是否联通
    boolean hasPathTo( int w ){
	assert( w >= 0 && w < G.V() );
	return from[w] != null;
    }

    // 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
    Vector<Edge<Weight>> shortestPath(int w){

	assert w >= 0 && w < G.V() ;
	assert !hasNegativeCycle ;
	assert hasPathTo(w) ;

	// 通过from数组逆向查找到从s到w的路径, 存放到栈中
	Stack<Edge<Weight>> s = new Stack<>();
	Edge<Weight> e = from[w];
	while( e.v() != this.s ){
	    s.push(e);
	    e = from[e.v()];
	}
	s.push(e);

	// 从栈中依次取出元素, 获得顺序的从s到w的路径
	Vector<Edge<Weight>> res = new Vector<>();
	while( !s.empty() ){
	    e = s.pop();
	    res.add(e);
	}

	return res;
    }

    // 打印出从s点到w点的路径
    void showPath(int w){

	assert( w >= 0 && w < G.V() );
	assert( !hasNegativeCycle );
	assert( hasPathTo(w) );

	Vector<Edge<Weight>> res = shortestPath(w);
	for( int i = 0 ; i < res.size() ; i ++ ){
	    System.out.print(res.elementAt(i).v() + " -> ");
	    if( i == res.size()-1 )
		System.out.println(res.elementAt(i).w());
	}
    }

    // 测试我们的Bellman-Ford最短路径算法
    public static void main(String[] args) {

	String filename = "testG2.txt";
	//String filename = "testG_negative_circle.txt";
	int V = 5;

	SparseWeightedGraph<Double> g = new SparseWeightedGraph<>(V, true);
	ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);

	System.out.println("Test Bellman-Ford:\n");

	int s = 0;
	BellmanFord<Integer> bellmanFord = new BellmanFord<Integer>(g, s);
	if( bellmanFord.negativeCycle() )
	    System.out.println("The graph contain negative cycle!");
	else
	    for( int i = 0 ; i < V ; i ++ ){
		if(i == s)
		    continue;

		if(bellmanFord.hasPathTo(i)) {
		    System.out.println("Shortest Path to " + i + " : " + bellmanFord.shortestPathTo(i));
		    bellmanFord.showPath(i);
		}
		else
		    System.out.println("No Path to " + i );

		System.out.println("----------");
	    }
    }
}

运行结果

在这里插入图片描述

时间复杂度

  • O(VE)

我的github仓库:
https://github.com/649733108/Mook-Play-with-Algorithms/tree/master/ShortestPath/src
欢迎骚扰!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值