算法筆記16:單源最短路徑(Dijkstra算法)

數學建模大賽中很多問題都能歸約到下面的問題:從一副航線圖中找到從一個城市到另外一個城市中代價最少的路線,這個代價可能是距離最近、花費最少或者時間最短,表示這些問題的數據結構是加權有向圖。如下圖所示,藍色是從0到6的最短路徑,是加權圖和有向圖的結合。
在這裏插入圖片描述

加權有向圖的實現

和加權無向圖的實現類似,程序見下面。

/**
 * 加權有向邊
 * @author XY
 *
 */
public class WeightedDiEdage {
	private int v;
	private int w;
	private double we;
	public WeightedDiEdage(int v,int w,double we){
		this.v=v;
		this.w=w;
		this.we=we;
	}
	public int from(){
		return v;
	}
	public int to(){
		return w;
	}
	public double weight(){
		return we;
	}
	@Override
	public String toString() {
		return "from:"+v+" to:"+w+" weight:"+we;
	}
	public static void main(String[] args) {
		WeightedDiEdage dEdage=new WeightedDiEdage(0, 1, 0.2);
		System.out.println(dEdage);
	}

}

使用加權有向邊實現加權有向圖。

import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;
/**
 * 加權有向圖的實現
 * @author XY
 *
 */
public class EdageWeightedDigraph {
	private int V=0;
	private int E=0;
	private ArrayList<WeightedDiEdage>[] adj;
	public EdageWeightedDigraph(int v){
		this.V=v;
		adj=(ArrayList<WeightedDiEdage>[])new ArrayList[v];
		for (int i = 0; i < adj.length; i++) {
			adj[i]=new ArrayList<WeightedDiEdage>();
		}
	}
	public EdageWeightedDigraph(Scanner scanner){
		this(scanner.nextInt());
		int e=scanner.nextInt();
		for (int i = 0; i <e; i++) {
			int v=scanner.nextInt();
			int w=scanner.nextInt();
			double we=scanner.nextDouble();
			WeightedDiEdage edage=new WeightedDiEdage(v, w, we);
			addEdage(edage);
		}
	}
	public void addEdage(WeightedDiEdage e){
		int v=e.from();
		adj[v].add(e);
		E++;
	}
	public int V(){
		return V;
	}
	public int E(){
		return E;
	}
	public Iterable<WeightedDiEdage> adj(int v){
		return adj[v];
	}
	
	@Override
	public String toString() {
		StringBuffer sb=new StringBuffer();
		sb.append("V: "+V+" E: "+E+"\n");
		for (int i = 0; i < V; i++) {
			for(WeightedDiEdage e:adj[i])
				sb.append(e+"\n");
		}
		return sb.toString();
	}
	public static void main(String[] args) throws Exception {
		EdageWeightedDigraph dgraph=new EdageWeightedDigraph(new Scanner
				(new File("E:"+File.separator+"wedigraph.txt")));
		System.out.println(dgraph);
	}

}

最短路徑的普適算法

在這裏插入圖片描述
用兩個數組表示最短路徑,edageTo[]表示從起點到該頂點的最短路徑中指向該頂點的有向邊,distTo[]表示起點到該頂點的最短路徑的距離。
最短路徑:對任何一點最短路徑的定義就是對任何路徑e:v→w,都有distTo[w]<distTo[v]+e.weight。
頂點的放鬆:對於頂點v,將所有從v指出的邊e:v→w,執行下面的程序,例如如果原來edageTo[5]=D,那麼在放鬆頂點3的時候就會變爲edageTo[5]=B,distTo[5]也會變小,在這個過程中捨棄了比較長的路徑而爲頂點5選擇了較短的路徑,因爲我們在尋找最短路徑。

private void relax(EdageWeightedDigraph g,int v){
		for(WeightedDiEdage e:g.adj(v)){
			int w=e.to();
			if(disto[w]>disto[v]+e.weight()){
				disto[w]=disto[v]+e.weight();
				edageto[w]=e;				
			} 			
		}
	}

假設在放鬆頂點2的時候,distTo[5]=0.5,edageTo[5]=B,那麼distTo[w]<distTo[v]+e.weight(),也就意味着邊D是無效邊,它不會影響到頂點5的最短路徑。
普適最短路徑算法:將起點s的distTo[s]初始化爲0,其他元素的distTo[]初始化爲無窮大,放鬆圖中的任意邊,直到不存在有效邊爲止。當不存在有效邊的時候就意味着每個頂點的路徑都是最短路徑。
在講頂點的放鬆的時候先放鬆頂點2還是先放鬆頂點3的情況是不一樣的,所以在放鬆頂點的時候的順序很重要,如果選擇了好的順序,執行if(disto[w]>disto[v]+e.weight())的時候dist[v]已經是頂點v的最短路徑,那麼頂點w不需要重新放鬆,如果順序選擇的不夠好,disto[v]不是最小值,那麼在放鬆完頂點v後放鬆其他頂點直到disto[v]是最小值後需要再次放鬆頂點v,才能得到頂點v的最短路徑。
下面要講到的Bellman-ford算法就是普適算法,它可以解決所有的最短路徑問題,Dijkstra算法和無環加權有向圖的最短路徑算法都是在特定情況下選擇好的順序來使最短路徑算法簡單的算法。

Dijkstra算法

Dijkstra算法適合於所有權重都爲正的加權有向圖。
Dijksrtra算法和Prim算法相似,從起點開始,放鬆起點,將起點指向的其他頂點加入優先序列,選擇距離最小的點進行放鬆。Prim算法和索引優先序列的實現見最小生成樹

import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;
import linmin.Indexpriority;
/**
 * Dijkstra算法,和Prim的即時算法相似,同樣的可以模仿延時Prim算法形成延時版的Dijkstra算法
 * @author XY
 *
 */
public class Dijkstra {
	private WeightedDiEdage[] edageto;
	private double[] disto;
	private Indexpriority<Double> pq;
	private int source;
	public Dijkstra(EdageWeightedDigraph graph,int s){
		edageto=new WeightedDiEdage[graph.V()];
		disto=new double[graph.V()];		
		for (int i = 0; i < disto.length; i++) {
			disto[i]=Double.POSITIVE_INFINITY;
		}
		disto[s]=0;
		pq=new Indexpriority<Double>(graph.V());
		pq.insert(s, 0.0);
		while(!pq.isEmpty()){
			relax(graph, pq.delMin());
		}
		
	}
	private void relax(EdageWeightedDigraph g,int v){//頂點放鬆
		for(WeightedDiEdage e:g.adj(v)){
			int w=e.to();
			if(disto[w]>disto[v]+e.weight()){
				disto[w]=disto[v]+e.weight();
				edageto[w]=e;
				pq.insert(w, disto[w]);
				//disto[w]變小,改變優先序列裏面w對應的值。因爲索引優先序列裏面處理了插入和改變,所以這裏
				//沒有執行“存在w則改變,不存在w則插入”的邏輯
			} 			
		}
	}
	public boolean hasPathTo(int v){
		return disto[v]<Double.POSITIVE_INFINITY;
	}
	public double distTo(int v){
		return disto[v];
	}
	public Iterable<WeightedDiEdage> pathTo(int v){
		ArrayList<WeightedDiEdage> list=new ArrayList<WeightedDiEdage>();
		for(int i=v;i!=source;i=edageto[i].from())
			list.add(edageto[i]);
		return list;
	}
	public static void main(String[] args) throws Exception {//測試用例
		EdageWeightedDigraph dgraph=new EdageWeightedDigraph(new Scanner
				(new File("E:"+File.separator+"wedigraph.txt")));
		Dijkstra sp=new Dijkstra(dgraph, 0);
		if(sp.hasPathTo(1))
			for(WeightedDiEdage e:sp.pathTo(1))
				System.out.println(e);
	}

}

可以使用下面的加權有向圖test.txt進行測試。

8
15
4 5 0.35
5 4 0.35
4 7 0.37
5 7 0.28
7 5 0.28
5 1 0.32
0 4 0.38
0 2 0.26
7 3 0.39
1 3 0.29
2 7 0.34
6 2 0.40
3 6 0.52
6 0 0.58
6 4 0.93

無環加權有向圖的最短路徑算法

針對無環加權圖我們選擇它的拓撲排序的順序放鬆頂點,在拓撲順序中,有向邊只存在於位於前面的頂點到後面的頂點,也就是說位於前面的頂點v放鬆時的distTo[v]就是最小值,因爲拓撲順序中在v的後面再也不存在指向v的頂點,也就是distTo[v]不會被改變。位於拓撲排序前面的頂點的distTo[]不會改變意味着所有的頂點不需要重新放鬆。而如果存在環,就意味着存在後面的頂點到前面的頂點的有向邊,那也就是前面的頂點的distTo[]會改變,那麼所有的頂點都必須重新放鬆。
爲了得到拓撲排序必須針對加權有向圖改變有向圖的拓撲排序的程序。有向圖的拓撲排序見有向圖

mport java.io.File;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
import java.util.Stack;
/**
 * 加權有向圖的深度優選次序,逆後序就是拓撲排序
 * @author XY
 *
 */
public class WeightDFOrder {
	private boolean[] marked;
	private Queue<Integer> pre;//根據訪問的順序
	private Queue<Integer> post;//深度優先搜索結束的順序,先結束進
	private Stack<Integer> reverse;//深度優先搜索結束的順序的逆序,先結束進棧
	public  WeightDFOrder(EdageWeightedDigraph dgraph) {
		marked = new boolean[dgraph.V()];
		pre = new LinkedList<Integer>();
		post = new LinkedList<Integer>();
		reverse = new Stack<Integer>();
		for (int i = 0; i < dgraph.V(); i++) {
			if (!marked[i])
				dfs(dgraph, i);
		}

	}

	private void dfs(EdageWeightedDigraph dgraph, int s) {
		marked[s] = true;
		pre.add(s);
		for (WeightedDiEdage x : dgraph.adj(s)) {
			int w=x.to();
			if (!marked[w])
				dfs(dgraph, w);
		}
		post.add(s);
		reverse.push(s);
	}

	public Iterable<Integer> pre() {
		return pre;
	}

	public Iterable<Integer> post() {
		
		return post;
	}

	public Iterable<Integer> reverse() {
		Stack<Integer> stack=new Stack<Integer>();
		//因爲JAVA中Stack是Vector的子類,其迭代順序是Vector的順序,不是棧頂到棧底的順序所以反轉
		while(!reverse.isEmpty()){			
			stack.push(reverse.pop());
			}
		return stack;
	}
	public static void main(String[] args) throws Exception {
		EdageWeightedDigraph dgraph=new EdageWeightedDigraph(new Scanner
				(new File("E:"+File.separator+"wedigraph.txt")));
		WeightDFOrder order=new WeightDFOrder(dgraph);
		for(int s:order.reverse())
			System.out.println(s);
	}

}

下面是無環加權有向圖的最短路徑算法。

import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;
/**
 * 無環加權有向圖的最短路徑算法
 * @author XY
 *
 */
public class NoCycleSP {
	private double[] disto;
	private WeightedDiEdage[] edageto;
	private int source;
	public NoCycleSP(EdageWeightedDigraph dgraph,int s){
		disto=new double[dgraph.V()];
		edageto=new WeightedDiEdage[dgraph.V()];
		for (int i = 0; i < disto.length; i++) {
			disto[i]=Double.POSITIVE_INFINITY;
		}
		disto[s]=0.0;
		this.source=s;
		Iterable<Integer> order=new WeightDFOrder(dgraph).reverse();//拓撲排序
		for(int x:order){
			relax(dgraph, x);
		}
	}
	private void relax(EdageWeightedDigraph dgraph,int v){
		for(WeightedDiEdage e:dgraph.adj(v)){
			int w=e.to();
			if(disto[w]>disto[v]+e.weight()){
				disto[w]=disto[v]+e.weight();
				edageto[w]=e;
			}
		}			
	}
	public boolean hasPathTo(int v){
		return disto[v]<Double.POSITIVE_INFINITY;
	}
	public double distTo(int v){
		return disto[v];
	}
	public Iterable<WeightedDiEdage> pathTo(int v){
		ArrayList<WeightedDiEdage> list=new ArrayList<WeightedDiEdage>();
		for(int i=v;i!=source;i=edageto[i].from())
			list.add(edageto[i]);
		return list;
	}
	public static void main(String[] args) throws Exception {
		EdageWeightedDigraph dgraph=new EdageWeightedDigraph(new Scanner
				(new File("E:"+File.separator+"wedigraph.txt")));
		NoCycleSP sp=new NoCycleSP(dgraph, 0);
		if(sp.hasPathTo(6))
			for(WeightedDiEdage e:sp.pathTo(6))
				System.out.println(e);
	}

}

Bellman-ford算法的實現

Bellman-ford算法是普適算法,可以解決所有的圖的最短路徑問題,包括環和負權重邊。
Bellman-ford算法:將起點s的distTo[s]初始化爲0,其他元素的distTo[]初始化爲無窮大,以任意順序放鬆有向圖的所有邊,重複V輪。此時不存在有效邊。
Bellman-ford算法時間與VE成正比, 算法筆記17會介紹改進的Bellman-ford算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章