1. 程式人生 > >貪心演算法之最短路徑問題(Dijkstra演算法)

貪心演算法之最短路徑問題(Dijkstra演算法)

1、問題

一個求單源最短路徑的問題。給定有向帶權圖 G =(V, E ),
其中每條邊的權是非負實數。此外,給定 V 中的一個頂點,
稱為源點。現在要計算從源到所有其他各頂點的最短路徑長
度,這裡路徑長度指路上各邊的權之和。


2、分析



3、程式碼實現

1、普通C++實現
#include <iostream>
#include <cstdio>
#include <stack>
#include <string>

using namespace std;

/*
一定要記得如果初始化矩陣的話,肯定需要一個變數儲存長和寬的最大值,
如果看到權重的話,肯定是需要有個變數儲存最大值的權重
*/

//城市的節點數目的最大值
const int MAX_CITY_NUM = 100;
//節點權值的最大值
const int MAX_POLICY = 1e7;
//初始化權重矩陣
int map[MAX_CITY_NUM][MAX_CITY_NUM];
//源點到各個頂點的最短具體陣列
int dist[MAX_CITY_NUM];
//下標表示當前節點值,然後值儲存為上個節點值
int p[MAX_CITY_NUM];
//城市的節點數目和線段的個數
int n, m;
//是否加入集合S,如果在集合S裡面的話,值為true,否則在集合S-V裡面,值為false;
bool flag[MAX_CITY_NUM];


//Dijkstra演算法
void  dijkstra(int start)
{
	//初始化和源點相連的頂點進行初始化
	for (int i = 1; i <= n; ++i) 
	{
		//先把前節點都設定成false
		flag[i] = false;
		//先把和源點關聯在一起的進行初始化,如果沒有源點關聯在一起的話
		//就設定為權重的最大值,如果我在下面的條件裡面判斷,部分節點的權重
		//可能為0,後面可能有問題
		dist[i] = map[start][i];
		if (dist[i] != MAX_POLICY) 
		{
		  	//dist[i] = map[start][i];
			p[i] = start;
		}
		else
		{
			p[i] = -1;	
		}
	}
	flag[start] = true;
	dist[start] = 0;
	//一開始,我忘記了dist[i]裡面沒有和源點關聯的值應該是MAX_POLICY
	//然後我也忘記了p[i]裡面如果沒有放值的話應該初始化為-1
	//然後我也忘記了dist[start]=0,源點到源點的權重是0

	//上面s集合裡面只有源點,我們接下來需要在s集合裡面新增其它的頂點,
	for (int i = 1; i <= n; ++i)
	{
		//t儲存我們每次找到的最小節點權重,然後min_dist用來每次儲存最小的節點權重,然後每次更新這個資料
		int min_dist = MAX_POLICY, t = start;
		//先在V-S集合裡面找到dist[i]裡面權重最小的資料,然後把頂點加入s集合	
		for (int j = 1; j <= n; j++) 
		{
			//這裡需要得到最小的dist[j],所以我們這裡不能用!=,必須用<
			if (dist[j] < min_dist && !flag[j]) 
			{
				min_dist = dist[j];
				t = j;
			}	
		}
		//我們發現程式退出的時候,p[j]裡面只有源點的值是-1,其它的值都不是-1
		//所以我們需要在迴圈裡面打個標記,如果進去了,說明不能退出,如果沒有進去
		//這個臨時變數t和之前的臨時變數的值是一樣,我們就跳出迴圈
		if (t == start) return;
		//找到之後我們需要先設定flag[j]為true
		flag[t] = true;
		//加入到s集合之後,如果發現新權重比在dist[i]裡面要小,就需要更新dist[i]
    	for (int j = 1; j <= n; j++)
		{   //C++裡面false的值是0,不是-1,true的值是1,以後一定不能忘記
			if (map[t][j] < MAX_POLICY && !flag[j])
			{
				if (dist[j] > (dist[t] + map[t][j]))
				{
					//更新新的定點權重
					dist[j] = dist[t] + map[t][j];
					//找到之後要記得設定之前的頂點
					p[j] = t;
				}
			}
		}
	}
}

//打印出每個頂點的路徑,這裡值儲存了前一個節點的key
//所以我們需要用到棧的特點,先進後出
void showProcess(int start)
{
	int value;
	stack<int> stack;
	for (int i = 1; i <= n; ++i)
	{
		value = p[i];
		std::cout << "源點"<< start << "到"<< i << "的路徑是"; 
		while (value != -1) 
		{
			stack.push(value);
			value = p[value];
		}
		while (!stack.empty())
		{
			//pop函式是出來棧,沒有返回值,先取出棧頂值,然後出棧
			int node = stack.top();
			stack.pop();
			std::cout << node << "-";
		}
		std::cout  << i << "最短距離為" << dist[i] << std::endl;
	}
}

int main()
{
	//定點u到定點v的權重是w, 然後輸入的起始地點是start;
	int u, v, w, start;
	std::cout << "請輸入城市的節點個數" << std::endl;
	std::cin >> n;
	if (n <= 0)
	{
		std::cout << "輸入的城市節點個數因該大於0" << std::endl;
		return -1;
	}
	std::cout << "請輸入城市之間線路的個數" << std::endl;
	std::cin >> m;
	if (m <= 0) 
	{
		std::cout << "輸入的城市之前的線路個數不能小於0" << std::endl;
		return -1;
	}
	//鄰接舉證的初始化,預設都為最大值,注意這裡下標都是從1開始
	for (int i = 1; i <= n; ++i) 
	{
		for (int j = 1; j <= n; ++j)
		{
			map[i][j] = MAX_POLICY;	
		}
	}
    std::cout << "請輸入城市頂點到城市頂點之前的權重" << std::endl;
    //這裡也可以使用while(--m),因為不涉及到用i
	for (int i = 0; i < m; ++i) 
	{
		std::cin >> u >> v >> w;
		if (u > n || v > n) 
			std::cout << "您輸入的定點有誤" << std::endl;
		//如果2次輸入一樣頂點,那麼取最小的
		map[u][v] = min(map[u][v], w);
	}
	std::cout << "請輸入小明的位置" << std::endl;
	//請輸入起始的頂點
	std::cin >> start;
	if (start < 0 || start > n)
	{
		std::cout << "輸入的起始城市定點有誤" << std::endl;
		return 0;
	}
	dijkstra(start);
	std::cout << "小明所在的位置 " << start << std::endl;
	for (int i = 1; i <= n; ++i)
	{
		std::cout << "小明(" << start << ")要去的位置是" << i;
		if (dist[i] == MAX_POLICY)
			std::cout << "無路可到" << std::endl;
		else
			std::cout << "最短距離為" << dist[i] << std::endl;
	}
	showProcess(start);
	return 0;	
}


2、類C++實現
#include <iostream>
#include <cstdio>
#include <stack>
#include <string>

using namespace std;

//城市的節點數目的最大值
const int MAX_CITY_NUM = 100;
//節點權值的最大值
const int MAX_POLICY = 1e7;

/*
一定要記得如果初始化矩陣的話,肯定需要一個變數儲存長和寬的最大值,
如果看到權重的話,肯定是需要有個變數儲存最大值的權重
*/

class Dijkstra 
{
public:
    //初始化工作
	void init();
	//dijkstra演算法
	void dijkstra();
	//顯示源點到其它頂點的經過的頂點
	void showProcess();
	//顯示源點到各個頂點的最小權重
	void showMinPolicy();
private:
    //城市的節點數目和線段的個數和起始位置
	int n, m, start;
	//初始化權重矩陣
	int map[MAX_CITY_NUM][MAX_CITY_NUM];
	//源點到各個頂點的最短具體陣列
	int dist[MAX_CITY_NUM];
	//下標表示當前節點值,然後值儲存為上個節點值
	int p[MAX_CITY_NUM];
	//是否加入集合S,如果在集合S裡面的話,值為true,否則在集合S-V裡面,值為false;
	bool flag[MAX_CITY_NUM];
};

//Dijkstra演算法
void Dijkstra::dijkstra()
{
	//初始化和源點相連的頂點進行初始化
	for (int i = 1; i <= n; ++i) 
	{
		//先把前節點都設定成false
		flag[i] = false;
		//先把和源點關聯在一起的進行初始化,如果沒有源點關聯在一起的話
		//就設定為權重的最大值,如果我在下面的條件裡面判斷,部分節點的權重
		//可能為0,後面可能有問題
		dist[i] = map[start][i];
		if (dist[i] != MAX_POLICY) 
		{
		  	//dist[i] = map[start][i];
			p[i] = start;
		}
		else
		{
			p[i] = -1;	
		}
	}
	flag[start] = true;
	dist[start] = 0;
	//一開始,我忘記了dist[i]裡面沒有和源點關聯的值應該是MAX_POLICY
	//然後我也忘記了p[i]裡面如果沒有放值的話應該初始化為-1
	//然後我也忘記了dist[start]=0,源點到源點的權重是0

	//上面s集合裡面只有源點,我們接下來需要在s集合裡面新增其它的頂點,
	for (int i = 1; i <= n; ++i)
	{
		//t儲存我們每次找到的最小節點權重,然後min_dist用來每次儲存最小的節點權重,然後每次更新這個資料
		int min_dist = MAX_POLICY, t = start;
		//先在V-S集合裡面找到dist[i]裡面權重最小的資料,然後把頂點加入s集合	
		for (int j = 1; j <= n; j++) 
		{
			//這裡需要得到最小的dist[j],所以我們這裡不能用!=,必須用<
			if (dist[j] < min_dist && !flag[j]) 
			{
				min_dist = dist[j];
				t = j;
			}	
		}
		//我們發現程式退出的時候,p[j]裡面只有源點的值是-1,其它的值都不是-1
		//所以我們需要在迴圈裡面打個標記,如果進去了,說明不能退出,如果沒有進去
		//這個臨時變數t和之前的臨時變數的值是一樣,我們就跳出迴圈
		if (t == start) return;
		//找到之後我們需要先設定flag[j]為true
		flag[t] = true;
		//加入到s集合之後,如果發現新權重比在dist[i]裡面要小,就需要更新dist[i]
    	for (int j = 1; j <= n; j++)
		{   //C++裡面false的值是0,不是-1,true的值是1,以後一定不能忘記
			if (map[t][j] < MAX_POLICY && !flag[j])
			{
				if (dist[j] > (dist[t] + map[t][j]))
				{
					//更新新的定點權重
					dist[j] = dist[t] + map[t][j];
					//找到之後要記得設定之前的頂點
					p[j] = t;
				}
			}
		}
	}
}

//打印出每個頂點的路徑,這裡值儲存了前一個節點的key
//所以我們需要用到棧的特點,先進後出
void Dijkstra::showProcess()
{
	int value;
	stack<int> stack;
	for (int i = 1; i <= n; ++i)
	{
		value = p[i];
		std::cout << "源點"<< start << "到"<< i << "的路徑是"; 
		while (value != -1) 
		{
			stack.push(value);
			value = p[value];
		}
		while (!stack.empty())
		{
			//pop函式是出來棧,沒有返回值,先取出棧頂值,然後出棧
			int node = stack.top();
			stack.pop();
			std::cout << node << "-";
		}
		std::cout  << i << "最短距離為" << dist[i] << std::endl;
	}
}

void Dijkstra::init()
{
	//定點u到定點v的權重是w, 然後輸入的起始地點是start;
	int u, v, w;
	std::cout << "請輸入城市的節點個數" << std::endl;
	std::cin >> n;
	if (n <= 0)
	{
		std::cout << "輸入的城市節點個數因該大於0" << std::endl;
		return;
	}
	std::cout << "請輸入城市之間線路的個數" << std::endl;
	std::cin >> m;
	if (m <= 0) 
	{
		std::cout << "輸入的城市之前的線路個數不能小於0" << std::endl;
		return;
	}
	//鄰接舉證的初始化,預設都為最大值,注意這裡下標都是從1開始
	for (int i = 1; i <= n; ++i) 
	{
		for (int j = 1; j <= n; ++j)
		{
			map[i][j] = MAX_POLICY;	
		}
	}
    std::cout << "請輸入城市頂點到城市頂點之前的權重" << std::endl;
	//這裡也可以使用while(--m),因為不涉及到用i
	for (int i = 0; i < m; ++i) 
	{
		std::cin >> u >> v >> w;
		if (u > n || v > n) 
			std::cout << "您輸入的定點有誤" << std::endl;
		//如果2次輸入一樣頂點,那麼取最小的
		map[u][v] = min(map[u][v], w);
	}
	std::cout << "請輸入小明的位置" << std::endl;
	//請輸入起始的頂點
	std::cin >> start;
	if (start < 0 || start > n)
	{
		std::cout << "輸入的起始城市定點有誤" << std::endl;
		return;
	}
}

void Dijkstra::showMinPolicy()
{
	std::cout << "小明所在的位置 " << start << std::endl;
    for (int i = 1; i <= n; ++i)
	{
		std::cout << "小明(" << start << ")要去的位置是" << i;
		if (dist[i] == MAX_POLICY)
		    std::cout << "無路可到" << std::endl;
	    else
	        std::cout << "最短距離為" << dist[i] << std::endl;
	}
}

int main()
{
	Dijkstra dij;
	dij.init();
	dij.dijkstra();
	dij.showMinPolicy();
	dij.showProcess();
	return 0;	
}


4、執行結果和時間複雜度和空間複雜度

請輸入城市的節點個數
5
請輸入城市之間線路的個數
11
請輸入城市頂點到城市頂點之前的權重
1 5 12
5 1 8
1 2 16
2 1 29
5 2 32
2 4 13
4 2 27
1 3 15
3 1 21
3 4 7
4 3 19
請輸入小明的位置
5
小明所在的位置 5
小明(5)要去的位置是1最短距離為8
小明(5)要去的位置是2最短距離為24
小明(5)要去的位置是3最短距離為23
小明(5)要去的位置是4最短距離為30
小明(5)要去的位置是5最短距離為0
源點5到1的路徑是5-1最短距離為8
源點5到2的路徑是5-1-2最短距離為24
點5到3的路徑是5-1-3最短距離為23
源點5到4的路徑是5-1-3-4最短距離為30
源點5到5的路徑是5最短距離為0

時間複雜度O(n2),空間複雜的O(n);


5、總結

  1、 最短路徑我們採用貪心演算法,每次找S-V集合裡面最小權重得放到S集合,然後再找S集合裡面零邊的權重,是否更新陣列的權重   2、我們用 for迴圈的時候,如果不涉及使用i,比如while(--m){}和for(int i = 0; i < m; ++i)等效   3、初始化的時候,要記得先把所有的dist[i]設定位最大值,然後還有所有得flag[i]為false,還有就是p[i]為-1   4、我們下次遇到問題,比如把集合資料拉到另外一個集合得時候,我們要記得構築flag[i]來標識,比如true在一個集合,false代表另外一個集合   5、當一個數組的value儲存得值是,另外一個數組的key,也就是題目中的p[i],保持著前驅節點,這個時候我們需要用棧(stack),先push,然後pop處理,以後一定要有這個思想   6、在C++裡面,我們把常量放在類的外面,然後全域性變數,全域性可以使用,如果在都在類方法裡面使用的話,我們也可以把全域性變數作為類的私有變數   7、C++裡面儘量用標頭檔案#include <cstdio>,不要用#include <stdio.h>   8、獲取集合裡面最小權重,我們可以先定義一個變數,預設最大值,然後通過for迴圈遍歷獲取權重,判斷每次是否小於這個變數,遇到小的就跟新這個變數,就是這個比較小的權值。   9、這個問題跳出迴圈,我們分析,最後S-V集合裡面沒有資料,也就是說flag裡面變成了true,所以我們先定義一個變數節點為源點,然後通過for迴圈裡面找,如果發現都不匹配條件,(只有匹配條件才更新變數節點),然後最後發現變數節點依然是源點,我們就return.