1. 程式人生 > >圖論演算法:最短路徑——無權最短路徑演算法和Dijkstra演算法C++實現

圖論演算法:最短路徑——無權最短路徑演算法和Dijkstra演算法C++實現

前言

        今天將給大家介紹的是圖論演算法中的另外一個基礎部分——最短路徑演算法;其中又分為無權最短路徑,單源最短路徑,具有負邊的最短路徑以及無圈圖等;而這次將介紹常見的兩個——無權最短路徑以及單源最短路徑。接下來就開始我們的講解吧~~

原理

        最短路徑演算法,顧名思義是一種用來找出從某地到另外某個地方所經過的路徑長度最短的演算法,如圖一所示。

上圖中所展示的正是Dijkstra演算法查詢後的結果,我們可以清楚的瞭解從v1點開始,到其他點的最短路徑該如何選擇,以及其所經過的沒條邊的權重;接下來我將具體介紹其實現方式:無權最短路徑:        在無權圖中因為邊上不存在權重,因此我們可以把每條邊的權重都當做1。當我們從某個頂點開始檢索的時候,我們將該節點壓入一個佇列中或者是棧中(當然你也可以通過for迴圈來檢索,不過時間複雜度會上升到O(n * n) )
,然後將該節點標位已知節點,接下來檢索所有與該節點相連線的節點,更新他們的路徑與距離,並標位已知,接下來重複剛才的步驟,只不過我們只對未更新過的節點(即距離無窮的節點)進行操作,直到所有節點都已知。        以下是虛擬碼:
int Unweighted(vertex start) {
    queue Q;
    vertex v, w;

    enqueue(start);
    // 遍歷所有的節點
    while(all vertex is retrieved) {
        v = dequeue;
        v is known;

        // 遍歷相鄰節點
        for each w adjacent to v
            // 更新路徑
            if Dist to w is infinity {
                Dist to w = Dist to v + 1;
                Path to w is v;
                enqueue(w);
            }
    }
}
虛擬碼中有幾點需要說明:1.遍歷所有節點我們只需要保證當佇列是空時即可,這和拓撲排序很相似,大家可以想一想;2.更新路徑時我們只需要保證路徑未被更新過即可進行更新,因為只要我們更新過得路徑即為最短路徑,關於這一點大家也可以想一想;2.Dijkstra演算法:        在賦權圖中情況看似變得很複雜了,但其實我們用和無權圖中相似的思想,也可以解決它,這就是我接下來介紹的Dijkstra演算法;同上面類似,我們也從某個頂點開始檢索將其標位已知,並更新其連結的頂點的路徑資訊,不同之處是,我們選擇未知節點中路徑距離最短的作為下一次檢索的起始頂點,然後一直進行檢索直到所有頂點都已知;        以下是虛擬碼:
int Dijkstra(vertex start) {
    vertex v, w;

    // 遍歷所有的節點
    while(true) {
        v = smallest unknown distance vertex;
        if v = novertex
            break;

        v is known;
        // 遍歷相鄰節點
        for each w adjacent to v
            // 更新路徑
            if(w is unknown && Dist to v + v->w < Dist to w) {
                Dist to w = Dist ti v + v->w;
                Path to w is v;
            }
    }
}
當然上訴程式碼中也有幾點需要注意的地方:1.查詢最短距離未知節點的方法會直接影響到我們的時間複雜度,最好的方法是使用優先佇列來完成;當然使用不同優先佇列也會有差距,其中我們按照時間複雜度來排序的話:斐波拉契堆(O(E + V * logV)) < 配對堆(O(E * logV)) < 二叉堆(O(E * logV + V * logV));
2.查詢最短距離未知節,並以此節點開始檢索也是演算法中最重要的東西,他保證了我們每次更新的起始節點v的距離一定是最短的,從而保證了演算法的正確性;

C++實現:

        最後給出的是整個實現的程式碼,按照慣例先是.h檔案:
#ifndef ALGRAPH_H
#define ALGRAPH_H

#include <iostream>
#include <iomanip>
#include <queue>
using namespace std;

// 重定義邊節點,便於操作
typedef struct ArcNode *Position;

/* 邊節點
 * 儲存元素:
 * adjvex:該有向邊連向的節點
 * Weight:該有向邊的權重
 * Next:該有向邊頭節點的其他邊節點
 */
struct ArcNode {
	int adjvex;
	int Weight;
	Position Next;
};

/* 頂點節點
 * 儲存元素:
 * Name:該節點的姓名;
 * firstArc:該頂點連結的第一個有向邊;
 */
struct VexNode {
	int Name;
	Position firstArc;
};

/* 表節點
 * 儲存元素:
 * Known:該節點檢測狀態;
 * Dist:該節點到目標節點的最小距離;
 * Path:最小距離時其連結的上一個節點;
 */
struct TNode {
	bool Known;
	int Dist;
	int Path;
};

/* ALGraph類
 * 介面:
 * Creat:建立功能,在選定節點之間建立有向邊;
 * MakeEmpty:置空功能,將所有有向邊刪除,初始化各個頂點;
 * Unweighted:排序功能,找出選定頂點對於其他頂點的最短無權路徑;
 * Display:展示功能,展示該圖的路徑資訊;
 * Dijkstra:Dijkstra演算法,用於計算賦權圖最短路徑
 * WeightNegative:排序功能,用於計算具有負邊值的圖的最短路徑
 */
class ALGraph
{
public:
	// 建構函式
	ALGraph(int = 10);
	// 解構函式
	~ALGraph();

	// 介面函式
	// 基礎函式
	void Creat();
	void MakeEmpty();
	void Display();

	// 最短路徑函式
	void Unweighted(int);
	void Dijkstra(int);
	void WeightNegative(int);

private:
	// 輔助函式
	void InitTable();

	// 資料成員
	int VexNum; // 儲存頂點數
	int ArcNum; // 儲存邊數
	VexNode *AdjList; // 儲存鄰接表
	TNode *Table; // 儲存距離表
};


#endif // !ALGRAPH_H 
然後是.cpp檔案,不過在這個檔案中還有計算帶負邊的有向圖的最短路徑演算法,有興趣的朋友可以自己看下:
#include "stdafx.h"
#include "ALGraph.h"


/* 建構函式:初始化物件
 * 返回值:無
 * 引數:vnum:圖中需要的頂點數
 */
ALGraph::ALGraph(int vnum)
: VexNum(vnum), ArcNum(0){
	// 申請鄰接表
	AdjList = new VexNode[VexNum + 1];
	// 申請距離表
	Table = new TNode[VexNum + 1];

	// 判斷是否申請成功
	if (AdjList == NULL || Table == NULL)
		cout << "鄰接表申請失敗!" << endl;

	else {
		for (int i = 0; i < VexNum + 1; i++) {
			// 初始化鄰接表
			AdjList[i].Name = i;
			AdjList[i].firstArc = NULL;

			// 初始化距離表
			Table[i].Dist = INT_MAX;
			Table[i].Known = false;
			Table[i].Path = 0;
		}
	}
	
}

/* 解構函式:物件消亡時回收儲存空間
 * 返回值:無
 * 引數:無
 */
ALGraph::~ALGraph()
{
	// 置空所有邊
	MakeEmpty();

	// 刪除鄰接表
	delete AdjList;
	AdjList = NULL;

	// 刪除距離表
	delete Table;
	Table = NULL;
}

/* 建立函式:在指定的頂點之間建立有向邊
 * 返回值:無
 * 引數:無
 */
void ALGraph::Creat() {
	// 儲存此次建立的邊數
	// 並更新到總邊數中
	int tmp;
	cout << "請輸入要建立的邊數:";
	cin >> tmp;
	ArcNum += tmp;

	// 建立所有新的有向邊
	for (int i = 0; i < tmp; i++) {
		// v:有向邊的頭結點
		// w:有向邊的尾節點
		// weight:有向邊的權值
		int v, w, weight;
		cout << "請輸入要建立有向邊的兩個頂點(v,w): ";
		cin >> v >> w;
		cout << "請輸入其權值:";
		cin >> weight;

		// 建立新的階段
		Position P = new ArcNode();
		if (P == NULL) {
			cout << "有向邊建立失敗!" << endl;
			return;
		}

		// 更新節點資訊
		P->adjvex = w;
		P->Weight = weight;
		P->Next = AdjList[v].firstArc;

		// 連結到鄰接表上
		AdjList[v].firstArc = P;
	}

}

/* 初始化函式:初始化距離表
 * 返回值:無
 * 引數:無
 */
void ALGraph::InitTable() {
	// 遍歷距離表
	for (int i = 0; i < VexNum + 1; i++) {
		// 初始化引數
		Table[i].Dist = INT_MAX;
		Table[i].Known = false;
		Table[i].Path = 0;
	}
}

/* 置空函式:將所有的有向邊置空
 * 返回值:無
 * 引數:無
 */
void ALGraph::MakeEmpty() {
	// 暫時儲存中間節點
	Position P;

	// 遍歷鄰接表
	for (int i = 1; i < VexNum + 1; i++) {
		P = AdjList[i].firstArc;

		// 遍歷所有連結的邊
		while (P != NULL) {
			AdjList[i].firstArc = P->Next;
			delete P;
			P = AdjList[i].firstArc;
		}
	}
}

/* 排序函式:找出指定節點對於其他節點的最短無權路徑
 * 返回值:無
 * 引數:Start:想要進行查詢的節點
 */
void ALGraph::Unweighted(int Start) {
	// Q:儲存佇列,用於儲存UnKnown節點
	// v:有向邊的頭結點
	// w:有向邊的尾節點
	queue <int> Q;
	int v, w;
	
	// 初始化距離表
	InitTable();

	// 起始節點距離為0,並壓入佇列
	Table[Start].Dist = 0;
	Q.push(Start);

	while (!Q.empty()) {
		// 獲取佇列元素,並刪除
		v = Q.front();
		Q.pop();

		// 該節點已知,不再需要使用
		Table[v].Known = true;

		// 遍歷所以以該節點為頭結點的有向邊
		Position P = AdjList[v].firstArc;
		while (P != NULL) {
			// 獲取尾節點
			w = P->adjvex;
			// 判斷尾節點是否需要更新
			if (Table[w].Dist == INT_MAX) {
				// 更新資訊
				Table[w].Dist = Table[v].Dist + 1;
				Table[w].Path = v;
				// 重新壓入佇列
				Q.push(w);
			}

			// 更新有向邊位置
			P = P->Next;
		}
	}
}

/* 展示函式:展示該圖的路徑資訊
 * 返回值:無
 * 引數:無
 */
void ALGraph::Display() {
	for (int i = 1; i < VexNum + 1; i++)
		cout << "Vex: " << i << "  ;Dist: " << Table[i].Dist << "  ; Path: " << Table[i].Path << endl;
}

/* Dijkstra演算法:對賦權圖進行單源最短路徑排序
 * 返回值:無
 * 引數:Start:進行演算法起始頂點
 */
void ALGraph::Dijkstra(int Start) {
	// v:單次排序的起始節點
	// w:單次排序的中值節點
	int v, w;

	// 初始化距離表
	InitTable();
	Table[Start].Dist = 0;

	// 遍歷所有邊
	while (true) {
		// Min:用於判斷是否需要繼續執行演算法
		int Min = INT_MAX;

		// 特別注意:
		//     此處尋找最小節點使用的方法是我自己為了方便直接寫的,如果
		// 用這種方法,時間複雜度應該比較高,達不到O(N * logN)的要求,所
		// 以正確的方法應該是把每個距離儲存在優先佇列中;
		//     當然,使用不同的優先佇列也會有不同的效果,總體來說按照時
		// 間複雜度: 
		//     斐波拉契堆(O(E + V * logV)) < 配對堆(O(E * logV)) < 二叉堆(O(E * logV + V * logV))

		// 尋找最小的,且還未確定的有向邊
		// 並將其頭結點作為本次的起始節點
		for (int i = 1; i < VexNum + 1; i++)
			if (Table[i].Known == false && Min > Table[i].Dist) {
				v = i;
				Min = Table[i].Dist;
			}

		// 起始節點已知,不用再參與運算
		Table[v].Known = true;

		// 演算法退出條件
		if (Min == INT_MAX)
			break;

		// 遍歷所有以該起始節點為頭結點的有向邊
		Position P = AdjList[v].firstArc;
		while (P != NULL) {
			w = P->adjvex;
			// 判斷尾節點是否已知
			if(Table[w].Known == false)
				if (Table[w].Dist > Table[v].Dist + P->Weight) {
					// 更新路徑及距離
					Table[w].Path = v;
					Table[w].Dist = Table[v].Dist + P->Weight;
				}

			// 指向下一個節點
			P = P->Next;
		}
	}
}

/* 排序函式:用於計算具有負邊值的圖的最短路徑
 * 返回值:無
 * 引數:Start:計算的起始節點
 */
void ALGraph::WeightNegative(int Start) {
	// Q:用於儲存節點佇列
	// v:單次排序的起始節點
	// w:單次排序的終止節點
	queue <int> Q;
	int v, w;

	// 初始化距離表
	InitTable();
	Table[Start].Dist = 0;
	Table[Start].Known = true;
	// 將起始節點壓入佇列
	Q.push(Start);

	// 遍歷所有路徑
	while (!Q.empty()) {
		v = Q.front();
		Q.pop();
		Table[v].Known = false; // 此處狀態表示沒有在佇列中

		// 遍歷所有已該節點為頭結點的有向邊
		Position P = AdjList[v].firstArc;
		while (P != NULL) {
			w = P->adjvex;

			// 更新路徑距離
			if (Table[w].Dist > Table[v].Dist + P->Weight) {
				Table[w].Dist = Table[v].Dist + P->Weight;
				Table[w].Path = v;
				// 若不在佇列中,則壓入佇列
				if (Table[w].Known = false)
					Q.push(w);
			}
		}
	}
}
        最後,最短路徑的討論我們到這裡就結束啦,如果有什麼問題歡迎大家一起討論啊~~

參考文獻:《資料結構與演算法分析——C語言描述》