最短路徑算法

單源最短路徑問題

問題描述:給你一個頂點作源點,你想要知道,如何從源點到達其餘全部點的最短路徑。 php

OK,這個問題看起來沒什麼用。咱們通常想知道的是A點到B點的最短路徑,這個單源最短路徑問題告訴咱們A點到全部點的最短路徑,會不會計算過多了呢? html

有趣的是,解決A點到B點的最短路徑算法不會比單源最短路徑問題簡單,咱們所知的求A點到B點的最短路徑算法就是求A點到任何點的最短路徑。咱們除了這樣作,好像也沒什麼好辦法了。 java

Dijkstra算法

基本原理: 算法

每次新擴展一個距離最短的點,更新與其相鄰的點的距離。當全部邊權都爲正時,因爲不會存在一個距離更短的沒擴展過的點,因此這個點的距離永遠不會再被改變,於是保證了算法的正確性。不過根據這個原理,用Dijkstra求最短路的圖不能有負權邊,由於擴展到負權邊的時候會產生更短的距離,有可能就破壞了已經更新的點距離不會改變的性質。 數組

適用條件與限制: ide

  • 有向圖無向圖均可以使用本算法,無向圖中的每條邊能夠當作相反的兩條邊。
  • 用來求最短路的圖中不能存在負權邊。(能夠利用拓撲排序檢測)

算法流程: 優化

在如下說明中,s爲源,w[u,v]爲點u和v之間的邊的長度,結果保存在dist[] 動畫

  1. 初始化:源的距離dist[s]設爲0,其餘的點距離設爲正無窮大,同時把全部的點的狀態設爲沒有擴展過。
  2. 循環n-1次:
    1. 在沒有擴展過的點中取距離最小的點u,並將其狀態設爲已擴展。
    2. 對於每一個與u相鄰(有向圖只kao慮出度)的點v,執行Relax(u,v),也就是說,若是dist[u]+w[u,v]<dist[v],那麼把dist[v]更新成更短的距離dist[u]+w[u,v]。此時到點v的最短路徑上,前一個節點即爲u。
  3. 結束。此時對於任意的u,dist[u]就是s到u的距離。

執行動畫過程以下圖: this

實例: spa

用Dijkstra算法找出以A爲起點的單源最短路徑步驟以下

代碼:

咱們在OJ上完成這個算法:http://acm.hdu.edu.cn/showproblem.php?pid=1874

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;

public class Main
{
	static int[][] v = new int[201][201];
	static int[] dist = new int[201];
	static boolean[] visit = new boolean[201];

	public static void main(String[] args) throws IOException
	{
		StreamTokenizer in = new StreamTokenizer(new BufferedReader(
				new InputStreamReader(System.in)));
		int n, m, begin, end;
		while (in.nextToken() != StreamTokenizer.TT_EOF)
		{
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			init(n);
			for (int i = 0; i < m; i++)
			{
				in.nextToken();
				int a = (int) in.nval;
				in.nextToken();
				int b = (int) in.nval;
				in.nextToken();
				int d = (int) in.nval;
				if (d < v[a][b])
				{
					v[a][b] = d;
					v[b][a] = d;
				}
			}
			in.nextToken();
			begin = (int) in.nval;
			in.nextToken();
			end = (int) in.nval;
			if (begin == end)
			{
				System.out.println("0");
				continue;
			}
			dist[begin] = 0;
			visit[begin] = true;
			dijkstra(begin, n);
			if (!visit[end])
			{
				System.out.println("-1");
			}
			else
			{
				System.out.println(dist[end]);
			}
		}
	}

	public static void init(int n)
	{
		for (int i = 0; i < n; i++)
		{
			visit[i] = false;
			for (int j = 0; j < n; j++)
			{
				v[i][j] = 10000;
			}
		}
	}

	public static void dijkstra(int s, int n)
	{
		for (int i = 0; i < n; i++)
		{
			dist[i] = v[s][i];
		}
		for (int i = 0; i < n - 1; i++)
		{
			int min = 10000;
			int index = 0;
			for (int j = 0; j < n; j++)
			{
				if (!visit[j] && dist[j] < min)
				{
					min = dist[j];
					index = j;
				}
			}
			visit[index] = true;
			for (int j = 0; j < n; j++)
			{
				if (!visit[j] && dist[index] + v[index][j] < dist[j])
				{
					dist[j] = dist[index] + v[index][j];
				}
			}
		}
	}
}
能夠看出時間複雜度爲 O(v^2)。有沒有更快的方法呢?

因爲每次都要找到未訪問的點中距離最小的點,咱們使用優先隊列來解決這個問題,關於優先隊列請查看這篇Blog。如下是利用優先隊列實現的算法

public static void dijkstrapq(int s, int n)
	{
		class Item implements Comparable<Item>
		{
			public int idx;
			public int weight;

			public Item(int idx, int weight)
			{
				this.idx = idx;
				this.weight = weight;
			}

			@Override
			public int compareTo(Item item)
			{
				if (this.weight == item.weight)
				{
					return 0;
				}
				else if (this.weight < item.weight)
				{
					return -1;
				}
				else
				{
					return 1;
				}
			}
		}
		PriorityQueue<Item> pq = new PriorityQueue<Item>();
		for (int i = 0; i < n; i++)
		{
			dist[i] = v[s][i];
			if (i != s)
			{
				pq.offer(new Item(i, dist[i]));
			}
		}
		Item itm = null;
		while (!pq.isEmpty())
		{
			itm = pq.poll();
			int index = itm.idx;
			int weight = itm.weight;
			if (weight == 10000)
			{
				break;
			}
			visit[index] = true;
			for (int j = 0; j < n; j++)
			{
				if (!visit[j] && dist[index] + v[index][j] < dist[j])
				{
					dist[j] = dist[index] + v[index][j];
					pq.offer(new Item(j, dist[j]));
				}
			}
		}
	}
若是是稠密圖(邊比點多),則直接掃描全部未收錄頂點比較好,即第一種方法,每次O(V),整體算法複雜度T=O(V^2+E)

若是是稀疏圖(邊比點少),則使用優先隊列(最小堆)比較好,即第二種方法,每次O(logV),插入更新後的dist,O(logV)。整體算法複雜度T=O(VlogV+ElogE)

固然還有更加優秀的斐波那契堆,時間複雜度爲 O(e+vlogv)

無權值(或者權值相等)的單源點最短路徑問題,Dijkstra算法退化成BFS廣度優先搜索。

那麼,爲何BFS會比Dijkstra在這類問題上表現得更加好呢?

1. BFS使用FIFO的隊列來代替Dijkstra中的優先隊列(或者heap之類的)。

2. BFS不須要在每次選取最小結點時判斷其餘結點是否有更優的路徑。

BFS的時間複雜度爲O(v+e)

Bellman-Ford算法

Dijkstra很優秀,可是使用Dijkstra有一個最大的限制,就是不能有負權邊。而Bellman-Ford適用於權值能夠爲負、無權值爲負的迴路的圖。這比Dijkstra算法的使用範圍要廣。其基本思想爲:首先假設源點到全部點的距離爲無窮大,而後從任一頂點u出發,遍歷其它全部頂點vi,計算從源點到其它頂點vi的距離與從vi到u的距離的和,若是比原來距離小,則更新,遍歷完全部的頂點爲止,便可求得源點到全部頂點的最短距離。

Bellman-Ford算法能夠大體分爲三個部分

  • 第一,初始化全部點。每個點保存一個值,表示從原點到達這個點的距離,將原點的值設爲0,其它的點的值設爲無窮大(表示不可達)。
  • 第二,進行循環,循環下標爲從1到n-1(n等於圖中點的個數)。在循環內部,遍歷全部的邊,進行鬆弛計算。
  • 第三,遍歷途中全部的邊(edge(u,v)),判斷是否存在這樣狀況:d(v) > d (u) + w(u,v)。則返回false,表示途中存在從源點可達的權爲負的迴路。

對有向帶權圖G = (V, E),從頂點s起始,利用Bellman-Ford算法求解各頂點最短距離,算法描述以下:

for(int k=1;k<=n-1;k++)//遍歷點的次數  
   {  
       for(int i=1;i<=m;i++)//遍歷邊的次數  
       {  
           if(dis[v[i]]>dis[u[i]]+w[i])//若是從u到v的距離可以經過w這條邊壓縮路徑 就要進行鬆弛操做  
           {  
               dis[v[i]]=dis[u[i]]+w[i];  
           }  
       }  
   }

很明顯Bellman-Ford算法複雜度爲O(VE),比Dijkstra要慢,可是解決了負權值問題。

圖解:

固然不用老是鬆弛E次,可能遠小於E次時,全部邊都不能鬆弛了。因此加個check來優化,若是每一個邊都沒鬆弛,就break。

for(int k=1;k<=n-1;k++)  
{  
    check=0;//用check檢查是否進行下一輪次的操做  
    for(int i=1;i<=m;i++)  
    {  
        if(dis[v[i]]>dis[u[i]]+w[i])  
        {  
            dis[v[i]]=dis[u[i]]+w[i];  
            check=1;  
        }  
    }  
    if(check==0)break;  
}

 咱們一直說的是有向圖的鬆弛,若是是無向圖則要鬆弛兩次(由於A到B有邊,那麼B到A也有邊)

for(int k=1;k<=n-1;k++)  
{  
    check=0;  
    for(int i=1;i<=m;i++)  
    {  
        if(dis[v[i]]>dis[u[i]]+w[i])  
        {  
            dis[v[i]]=dis[u[i]]+w[i];  
            check=1;  
        }  
        if(dis[u[i]]>dis[v[i]]+w[i])  
        {  
            dis[u[i]]=dis[v[i]]+w[i];  
            check=1;  
        }  
    }  
    if(check==0)break;  
}

代碼:

咱們在OJ上驗證這個算法:http://acm.hdu.edu.cn/showproblem.php?pid=2544

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;

public class Main
{
	static int[] begin = new int[121212];
	static int[] end = new int[121212];
	static int[] wight = new int[121212];
	static int[] dist = new int[121212];

	public static void main(String[] args) throws IOException
	{
		StreamTokenizer in = new StreamTokenizer(new BufferedReader(
				new InputStreamReader(System.in)));
		int n, m;
		while (in.nextToken() != StreamTokenizer.TT_EOF)
		{
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			init(n);
			if (n == 0 || m == 0)
			{
				break;
			}
			for (int i = 1; i <= m; i++)
			{
				in.nextToken();
				int a = (int) in.nval;
				in.nextToken();
				int b = (int) in.nval;
				in.nextToken();
				int d = (int) in.nval;
				begin[i] = a;
				end[i] = b;
				wight[i] = d;
				if (a == 1)
				{
					dist[b] = d;
				}
				if (b == 1)
				{
					dist[a] = d;
				}
			}
			bellmanFord(n, m);
			System.out.println(dist[n]);
		}
	}

	private static boolean bellmanFord(int n, int m)
	{
		dist[1] = 0;
		int check;
		for (int i = 1; i <= n - 1; i++)
		{
			check = 0;
			for (int j = 1; j <= m; j++)
			{
				int b = begin[j];
				int e = end[j];
				if (dist[b] + wight[j] < dist[e])
				{
					check = 1;
					dist[e] = wight[j] + dist[b];
				}
				if (dist[e] + wight[j] < dist[b])
				{
					check = 1;
					dist[b] = wight[j] + dist[e];
				}
			}
			if (check == 0)
			{
				break;
			}
		}
		return true;
	}

	public static void init(int n)
	{
		for (int i = 1; i <= n; i++)
		{
			dist[i] = 9999999;
		}
	}

}
OJ上的這個題目沒有負值環,Bellman-Ford是能夠檢查負值環的,就如上面所說,最後再遍歷一遍邊,若是還能鬆弛,說明有負值環。
for (int j = 1; j <= m; j++)
{
       int b = begin[j];
	int e = end[j];
	if (dist[b] + wight[j] < dist[e])
	{
		return false;
	}
	if (dist[e] + wight[j] < dist[b])
	{
		return false;
	}
}

SPFA算法

SPFA(Shortest Path Faster Algorithm)(隊列優化)算法是求單源最短路徑的一種算法,它還有一個重要的功能是判負環(在差分約束系統中會得以體現),在Bellman-ford算法的基礎上加上一個隊列優化,減小了冗餘的鬆弛操做,是一種高效的最短路算法。

SPFA算法維護一個隊列,裏面存放全部須要進行迭代的點。初始時隊列中只有一個點S。用一個布爾數組記錄每一個點是否處在隊列中。

SPFA算法能夠分爲大體3步

  • 初始化階段除了和Bellman-ford算法相同的地方外,還要加上將源點S入隊,而且在判斷點是否在隊列中的數組上作標記(inside[1] = 1)
  • 進行迭代,每次迭代,取出隊頭的點v,遍歷全部與v相連的邊,進行鬆弛操做,若是可以鬆弛而且該點不在隊列中,就將它放入隊尾。這樣一直迭代下去直到隊列變空。
  • 若一個點入隊次數超過n,則有負權環。

SPFA 在形式上和寬度優先搜索很是相似,不一樣的是寬度優先搜索中一個點出了隊列就不可能從新進入隊列,可是SPFA中一個點可能在出隊列以後再次被放入隊列,也就是一個點改進過其它的點以後,過了一段時間可能自己被改進,因而再次用來改進其它的點,這樣反覆迭代下去。

代碼:

咱們在OJ上驗證這個算法:http://acm.hdu.edu.cn/showproblem.php?pid=2544

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.LinkedList;
import java.util.Queue;

public class Main
{
	static int[][] v = new int[101][101];
	static int[] dist = new int[101];
	static int[] inside = new int[101];
	static Queue<Integer> queue;

	public static void main(String[] args) throws IOException
	{
		StreamTokenizer in = new StreamTokenizer(new BufferedReader(
				new InputStreamReader(System.in)));
		int n, m;
		while (in.nextToken() != StreamTokenizer.TT_EOF)
		{
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			init(n);
			if (n == 0 || m == 0)
			{
				break;
			}
			for (int i = 1; i <= m; i++)
			{
				in.nextToken();
				int a = (int) in.nval;
				in.nextToken();
				int b = (int) in.nval;
				in.nextToken();
				int d = (int) in.nval;
				v[a][b] = d;
				v[b][a] = d;
			}
			SPFA(n);
			System.out.println(dist[n]);
		}
	}

	private static void SPFA(int n)
	{
		queue.add(1);
		inside[1] = 1;
		dist[1] = 0;
		while (!queue.isEmpty())
		{
			int top = queue.poll();
			inside[top] = 0;
			for (int i = 1; i <= n; i++)
			{
				if (v[top][i] < 9999999)
				{
					if (dist[top] + v[top][i] < dist[i])
					{
						dist[i] = dist[top] + v[top][i];
						if (inside[i] == 0)
						{
							queue.add(i);
							inside[i] = 1;
						}
					}
				}
			}
		}
	}

	public static void init(int n)
	{
		for (int i = 1; i <= n; i++)
		{
			dist[i] = 9999999;
			inside[i] = 0;
			for (int j = 1; j <= n; j++)
			{
				v[i][j] = 9999999;
			}
		}
		queue = new LinkedList<Integer>();
	}

}
因爲上述代碼使用鄰接矩陣來存儲,因此在遍歷與某點相連的邊時,複雜度較高。若是將其改爲鄰接表來實現會更加明顯。

在平均狀況下,SPFA算法的指望時間複雜度爲O(E)。可是這一說法有爭議,在這裏就不討論了,總之SPFA是一種Bellman-Ford算法的優化。

多源最短路徑問題

咱們已經介紹了3種解決單源最短路徑問題的算法,

那麼多源最短路徑問題該怎麼解決呢?

很明顯有一種方法就是,將單源最短路徑問題使用N次,那麼使用普通的Dijkstra算法的時間複雜度爲T=O(V^3+V*E),對於稀疏圖的效果比較好。

而第二種方法則是要介紹的Floyd算法,它的時間複雜度爲T=O(V^3),對於稠密圖來講效果更好。

Floyd算法

對於最短路徑算法來講,其重點都是鬆弛。因爲如今是多源最短路徑問題,之前單源把dist[i]做爲源點S到i的最短路徑,如今源點不單一了,因此直接表示成e(i,j)表示i到j的最短路徑。

咱們已經知道鬆弛的緣由是,有了第三個點爲過渡點,使得距離變小了。

Floyd算法運用動態規劃的思想經過考慮最佳子路徑來獲得最佳路徑

Floyd算法的步驟就分爲如下兩步

  • 初始化:從任意一條單邊路徑開始。全部兩點之間的距離是邊的權,或者無窮大,若是兩點之間沒有邊相連。
  • 鬆弛:對於每一對頂點 u 和 v,看看是否存在一個頂點 w 使得從 u 到 w 再到 v 比己知的路徑更短。若是是更新它。

思想很是簡單,簡單來講就是遍歷全部的頂點,看看這個頂點是否能讓任意兩個頂點鬆弛。

核心代碼:

for (int k = 1; k <= n; k++)
		{
			for (int i = 1; i <= n; i++)
			{
				for (int j = 1; j <= n; j++)
				{

					if (v[i][k] + v[k][j] < v[i][j])
					{
						v[i][j] = v[i][k] + v[k][j];
					}

				}
			}
		}

代碼:

咱們在OJ上驗證這個算法:http://acm.hdu.edu.cn/showproblem.php?pid=2544

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;

public class Main
{
	static int[][] v = new int[101][101];

	public static void main(String[] args) throws IOException
	{
		StreamTokenizer in = new StreamTokenizer(new BufferedReader(
				new InputStreamReader(System.in)));
		int n, m;
		while (in.nextToken() != StreamTokenizer.TT_EOF)
		{
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			init(n);
			if (n == 0 || m == 0)
			{
				break;
			}
			for (int i = 1; i <= m; i++)
			{
				in.nextToken();
				int a = (int) in.nval;
				in.nextToken();
				int b = (int) in.nval;
				in.nextToken();
				int d = (int) in.nval;
				v[a][b] = d;
				v[b][a] = d;
			}
			floyd(n);
			System.out.println(v[1][n]);
		}
	}

	private static void floyd(int n)
	{
		for (int k = 1; k <= n; k++)
		{
			for (int i = 1; i <= n; i++)
			{
				for (int j = 1; j <= n; j++)
				{

					if (v[i][k] + v[k][j] < v[i][j])
					{
						v[i][j] = v[i][k] + v[k][j];
					}

				}
			}
		}
	}

	public static void init(int n)
	{
		for (int i = 1; i <= n; i++)
		{
			for (int j = 1; j <= n; j++)
			{
				v[i][j] = 9999999;
			}
		}
	}

}
算法時間複雜度很直觀O(V^3),由於要用鄰接矩陣來存儲圖,空間複雜度O(V^2)。

另外須要注意的是:Floyd算法也不能解決帶有「負權迴路」

總結:

經過以上4種最短路徑算法,咱們發現,最短路徑算法大概分爲3步

  1. 初始化
  2. 鬆弛
  3. 判斷是否有負值環

其中Bellman-Ford(鬆弛之後若是還能鬆弛則有負值環)與SPFA(每一個元素的入隊次數不能超過n)能檢測負值環。

咱們簡單的說一下4種算法的鬆弛過程:

  1. Dijkstra:由源點出發,鬆弛每條邊。而後選出其中最小的邊,將其做爲中間點,鬆弛其餘未訪問的邊,如此循環n-1次。
  2. Bellman-Ford:遍歷全部邊,查看兩個端點可否經過這條邊進行鬆弛。
  3. SPFA:用隊列來優化Bellman-Ford,從隊列中取出某個點,查看通過這個點可否使邊鬆弛,若是可以鬆弛而且沒有在隊列中,將另外一個點加入隊列中。
  4. Floyd:遍歷全部的頂點,看看這個頂點是否能讓任意兩個頂點鬆弛。

時間複雜度:

Dijkstra:普通:O(V^2+E),最小堆優化:O(VlogV+ElogE),斐波那契堆優化:O(E+VlogV)

Bellman-Ford:O(VE)

SPFA:O(kE),有爭論,總之比Bellman-Ford更加快

Floyd:O(V^3)

Reference:

1. http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html

2. http://www.nocow.cn/index.php/Dijkstra%E7%AE%97%E6%B3%95

3. http://blog.csdn.net/collonn/article/details/18155655

4. http://blog.csdn.net/mengxiang000000/article/details/50266373

相關文章
相關標籤/搜索