---------------------siwuxie095
Dijkstra 算法
這里介紹 Dijkstra 算法,它是一個應用最為廣泛的、名氣也是
最大的單源最短路徑算法
Dijkstra 算法有一定的局限性:它所處理的圖中不能有負權邊
「前提:圖中不能有負權邊」
換句話說,如果一張圖中,但凡有一條邊的權值是負值,那么
使用 Dijkstra 算法就可能得到錯誤的結果
不過,在實際生活中所解決的問題,大部分的圖是不存在負權
邊的
如:有一個路線圖,那么從一點到另外一點的距離肯定是一個
正數
所以,雖然 Dijkstra 算法有局限性,但是並不影響在實際問題
的解決中非常普遍的來使用它
看如下實例:
(1)初始
左邊是一張連通帶權有向圖,右邊是起始頂點 0 到各個頂點的
當前最短距離的列表,起始頂點 0 到自身的距離是 0
(2)將頂點 0 進行標識,並作為當前頂點
對當前頂點 0 的所有相鄰頂點依次進行松弛操作,同時更新列表
從列表的未標識頂點中找到當前最短距離最小的頂點,即 頂點 2,
就可以說,起始頂點 0 到頂點 2 的最短路徑即 0 -> 2
因為:圖中沒有負權邊,即便存在從頂點 1 到頂點 2 的邊,也不
可能通過松弛操作使得從起始頂點 0 到頂點 2 的距離更小
圖中沒有負權邊保證了:對當前頂點的所有相鄰頂點依次進行松
弛操作后,只要能從列表的未標識頂點中找到當前最短距離最小
的頂點,就能確定起始頂點到該頂點的最短路徑
(3)將頂點 2 進行標識,並作為當前頂點
(4)對當前頂點 2 的相鄰頂點 1 進行松弛操作,同時更新列表
(5)對當前頂點 2 的相鄰頂點 4 進行松弛操作,同時更新列表
(6)對當前頂點 2 的相鄰頂點 3 進行松弛操作,同時更新列表
從列表的未標識頂點中找到當前最短距離最小的頂點,即 頂點 1,
就可以說,起始頂點 0 到頂點 1 的最短路徑即 0 -> 2 -> 1
(7)將頂點 1 進行標識,並作為當前頂點
(8)對當前頂點 1 的相鄰頂點 4 進行松弛操作,同時更新列表
從列表的未標識頂點中找到當前最短距離最小的頂點,即 頂點 4,
就可以說,起始頂點 0 到頂點 4 的最短路徑即 0 -> 2 -> 1 -> 4
(9)將頂點 4 進行標識,並作為當前頂點
當前頂點 4 沒有相鄰頂點,不必進行松弛操作
從列表的未標識頂點中找到當前最短距離最小的頂點,即 頂點 3,
就可以說,起始頂點 0 到頂點 3 的最短路徑即 0 -> 2 -> 3
(10)將頂點 3 進行標識,並作為當前頂點
對當前頂點 3 的相鄰頂點 4 進行松弛操作,發現不能通過
松弛操作使得從起始頂點 0 到頂點 4 的路徑更短,所以保
持原有最短路徑不變
至此,列表中不存在未標識頂點,Dijkstra 算法結束,找
到了一棵以頂點 0 為根的最短路徑樹
Dijkstra 算法的過程總結:
第一步:從起始頂點開始
第二步:對當前頂點進行標識
第三步:對當前頂點的所有相鄰頂點依次進行松弛操作
第四步:更新列表
第五步:從列表的未標識頂點中找到當前最短距離最小
的頂點,作為新的當前頂點
第六步:重復第二步至第五步,直到列表中不存在未標
識頂點
其實 Dijkstra 算法主要做兩件事情:
(1)從列表中找最值
(2)更新列表
顯然,借助最小索引堆作為輔助數據結構,就可以非常
容易地實現這兩件事情
最后,Dijkstra 算法的時間復雜度:O(E*logV)
程序:
Edge.h:
#ifndef EDGE_H #define EDGE_H
#include <iostream> #include <cassert> using namespace std;
//邊信息:兩個頂點和權值 template<typename Weight> class Edge {
private:
int a, b; //邊的兩個頂點a和b(如果是有向圖,就默認從頂點a指向頂點b) Weight weight; //邊上的權值
public:
Edge(int a, int b, Weight weight) { this->a = a; this->b = b; this->weight = weight; }
//默認構造函數 Edge(){}
~Edge(){}
int v(){ return a; }
int w(){ return b; }
Weight wt() { return weight; }
//知道邊的一個頂點x,返回另一個頂點 int other(int x) { assert(x == a || x == b); return x == a ? b : a; }
//友元函數重載 friend ostream &operator<<(ostream &os, const Edge &e) { os << e.a << "-" << e.b << ": " << e.weight; return os; }
bool operator<(Edge<Weight> &e) { return weight < e.wt(); }
bool operator<=(Edge<Weight> &e) { return weight <= e.wt(); }
bool operator>(Edge<Weight> &e) { return weight > e.wt(); }
bool operator>=(Edge<Weight> &e) { return weight >= e.wt(); }
bool operator==(Edge<Weight> &e) { return weight == e.wt(); } };
#endif |
SparseGraph.h:
#ifndef SPARSEGRAPH_H #define SPARSEGRAPH_H
#include "Edge.h" #include <iostream> #include <vector> #include <cassert> using namespace std;
// 稀疏圖 - 鄰接表 template<typename Weight> class SparseGraph {
private:
int n, m; //n 和 m 分別表示頂點數和邊數 bool directed; //directed表示是有向圖還是無向圖 vector<vector<Edge<Weight> *>> g; //g[i]里存儲的就是和頂點i相鄰的所有邊指針
public:
SparseGraph(int n, bool directed) { this->n = n; this->m = 0; this->directed = directed; //g[i]初始化為空的vector for (int i = 0; i < n; i++) { g.push_back(vector<Edge<Weight> *>()); } }
~SparseGraph() {
for (int i = 0; i < n; i++) { for (int j = 0; j < g[i].size(); j++) { delete g[i][j]; } } }
int V(){ return n; } int E(){ return m; }
void addEdge(int v, int w, Weight weight) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
g[v].push_back(new Edge<Weight>(v, w, weight)); //(1)頂點v不等於頂點w,即不是自環邊 //(2)且不是有向圖,即是無向圖 if (v != w && !directed) { g[w].push_back(new Edge<Weight>(w, v, weight)); }
m++; }
//hasEdge()判斷頂點v和頂點w之間是否有邊 //hasEdge()的時間復雜度:O(n) bool hasEdge(int v, int w) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
for (int i = 0; i < g[v].size(); i++) { if (g[v][i]->other(v) == w) { return true; } }
return false; }
void show() {
for (int i = 0; i < n; i++) { cout << "vertex " << i << ":\t"; for (int j = 0; j < g[i].size(); j++) { cout << "{to:" << g[i][j]->w() << ",wt:" << g[i][j]->wt() << "}\t"; } cout << endl; } }
//鄰邊迭代器(相鄰,即 adjacent) // //使用迭代器可以隱藏迭代的過程,按照一定的 //順序訪問一個容器中的所有元素 class adjIterator { private:
SparseGraph &G; //圖的引用,即要迭代的圖 int v; //頂點v int index; //相鄰頂點的索引
public:
adjIterator(SparseGraph &graph, int v) : G(graph) { this->v = v; this->index = 0; }
//要迭代的第一個元素 Edge<Weight> *begin() { //因為有可能多次調用begin(), //所以顯式的將index設置為0 index = 0; //如果g[v]的size()不為0 if (G.g[v].size()) { return G.g[v][index]; }
return NULL; }
//要迭代的下一個元素 Edge<Weight> *next() { index++; if (index < G.g[v].size()) { return G.g[v][index]; }
return NULL; }
//判斷迭代是否終止 bool end() { return index >= G.g[v].size(); } }; };
#endif |
DenseGraph.h:
#ifndef DENSEGRAPH_H #define DENSEGRAPH_H
#include "Edge.h" #include <iostream> #include <vector> #include <cassert> using namespace std;
// 稠密圖 - 鄰接矩陣 template<typename Weight> class DenseGraph {
private:
int n, m; //n 和 m 分別表示頂點數和邊數 bool directed; //directed表示是有向圖還是無向圖 vector<vector<Edge<Weight> *>> g; //二維矩陣,存儲邊指針
public:
DenseGraph(int n, bool directed) { this->n = n; this->m = 0; this->directed = directed; //二維矩陣:n行n列,全部初始化為NULL for (int i = 0; i < n; i++) { g.push_back(vector<Edge<Weight> *>(n, NULL)); } }
~DenseGraph() { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (g[i][j] != NULL) { delete g[i][j]; } } } }
int V(){ return n; } int E(){ return m; }
//在頂點v和頂點w之間建立一條邊 void addEdge(int v, int w, Weight weight) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
//如果頂點v和頂點w之間已經存在一條邊,就刪掉, //之后按照傳入權值重建一條邊,即直接覆蓋 if (hasEdge(v, w)) { delete g[v][w];
//如果是無向圖,還要刪除和主對角線對稱的值 if (!directed) { delete g[w][v]; }
m--; }
g[v][w] = new Edge<Weight>(v, w, weight);
//如果是無向圖,還要在和主對角線對稱處添加值 if (!directed) { g[w][v] = new Edge<Weight>(w, v, weight); }
m++; }
//hasEdge()判斷頂點v和頂點w之間是否有邊 //hasEdge()的時間復雜度:O(1) bool hasEdge(int v, int w) { assert(v >= 0 && v < n); assert(w >= 0 && w < n); return g[v][w] != NULL; }
void show() {
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (g[i][j]) { cout << g[i][j]->wt() << "\t"; } else { cout << "NULL\t"; } } cout << endl; } }
//鄰邊迭代器(相鄰,即 adjacent) class adjIterator { private:
DenseGraph &G; //圖引用,即要迭代的圖 int v; //頂點v int index; //相鄰頂點的索引
public:
adjIterator(DenseGraph &graph, int v) : G(graph) { this->v = v; this->index = -1; }
//要迭代的第一個元素 Edge<Weight> *begin() { //找第一個權值不為NULL的元素,即為要迭代的第一個元素 index = -1; return next(); }
//要迭代的下一個元素 Edge<Weight> *next() { for (index += 1; index < G.V(); index++) { if (G.g[v][index]) { return index; } }
return NULL; }
//判斷迭代是否終止 bool end() { return index >= G.V(); } }; };
#endif |
ReadGraph.h:
#ifndef READGRAPH_H #define READGRAPH_H
#include <iostream> #include <string> #include <fstream> #include <sstream> #include <cassert> using namespace std;
//從文件中讀取圖的測試用例 template <typename Graph, typename Weight> class ReadGraph {
public: ReadGraph(Graph &graph, const string &filename) {
ifstream file(filename); string line; //一行一行的讀取 int V, E;
assert(file.is_open());
//讀取file中的第一行到line中 assert(getline(file, line)); //將字符串line放在stringstream中 stringstream ss(line); //通過stringstream解析出整型變量:頂點數和邊數 ss >> V >> E;
//確保文件里的頂點數和圖的構造函數中傳入的頂點數一致 assert(V == graph.V());
//讀取file中的其它行 for (int i = 0; i < E; i++) {
assert(getline(file, line)); stringstream ss(line);
int a, b; Weight w; ss >> a >> b >> w; assert(a >= 0 && a < V); assert(b >= 0 && b < V); graph.addEdge(a, b, w); } } };
#endif |
MinIndexHeap.h:
#ifndef MININDEXHEAP_H #define MININDEXHEAP_H
#include <iostream> #include <string> #include <cassert> #include <algorithm> using namespace std;
//最小索引堆:索引從0開始 template<typename Item> class MinIndexHeap {
private: Item *data; //指向存儲元素的數組 int *indexes; //指向存儲索引的數組 int *reverse; //指向存儲反向索引的數組 int count; int capacity;
//私有函數,用戶不能調用 void shiftUp(int k) { //如果新添加的元素小於父節點的元素,則進行交換 while (k > 0 && data[indexes[(k - 1) / 2]] > data[indexes[k]]) { swap(indexes[(k - 1) / 2], indexes[k]); reverse[indexes[(k - 1) / 2]] = (k - 1) / 2; reverse[indexes[k]] = k; k = (k - 1) / 2; } }
//也是私有函數,用戶不能調用 void shiftDown(int k) { //只要當前節點有孩子就進行循環 while (2 * k + 1 < count) { // 在此輪循環中,data[indexes[k]]和data[indexes[j]]交換位置 int j = 2 * k + 1;
// data[indexes[j]]是data[indexes[j]]和data[indexes[j+1]]中的最小值 if (j + 1 < count && data[indexes[j + 1]] < data[indexes[j]]) { j += 1; }
if (data[indexes[k]] <= data[indexes[j]]) { break; }
swap(indexes[k], indexes[j]); reverse[indexes[k]] = k; reverse[indexes[j]] = j; k = j; } }
public:
MinIndexHeap(int capacity) { data = new Item[capacity]; indexes = new int[capacity]; reverse = new int[capacity]; //初始化reverse數組 for (int i = 0; i < capacity; i++) { reverse[i] = -1; } //計數器,這里索引等於計數器減一 count = 0; this->capacity = capacity;
}
~MinIndexHeap() { delete []data; delete []indexes; delete []reverse; }
int size() { return count; }
bool isEmpty() { return count == 0; }
void insert(int i, Item item) { //防止越界 assert(count <= capacity); assert(i >= 0 && i <= capacity);
data[i] = item; indexes[count] = i; reverse[i] = count; count++;
shiftUp(count - 1); }
//取出最小的data Item extractMin() { //首先要保證堆不為空 assert(count > 0);
Item ret = data[indexes[0]]; swap(indexes[0], indexes[count - 1]); reverse[indexes[count - 1]] = -1; reverse[indexes[0]] = 0; count--; shiftDown(0); return ret; }
//取出最小的data對應的index int extractMinIndex() { assert(count > 0);
//對於外部來說,索引從0開始,所以要減一 int ret = indexes[0]; swap(indexes[0], indexes[count - 1]); reverse[indexes[count - 1]] = -1; reverse[indexes[0]] = 0; count--; shiftDown(0); return ret; }
Item getMin() { assert(count > 0); return data[indexes[0]]; }
int getMinIndex() { assert(count > 0); return indexes[0]; }
bool contain(int i){ assert(i >= 0 && i <= capacity); //reverse數組在構造函數中都初始化為-1, //所以拿-1做比較 return reverse[i] != -1; }
Item getItem(int i) { assert(contain(i)); //對於外部來說,索引從0開始, //對於內部來說,索引從1開始, //所以要加一 return data[i]; }
//修改 index 對應的 data void change(int i, Item newItem) { //防止越界和檢查i是否在堆中, //因為有可能已經取出去了 assert(contain(i));
data[i] = newItem;
// 找到indexes[j] = i, j表示data[i]在堆中的位置 // 之后嘗試着shiftUp(j)一下, 再shiftDown(j)一下 //即看看能不能向上或向下移動以保持堆的性質 int j = reverse[i]; shiftUp(j); shiftDown(j);
//先用O(1)的時間找到位置,再用O(lgn)的時間完成 //Shift Up和Shift Down,此時,該函數的時間復雜 //度就是O(lgn)級別的,如果有n個堆操作,總時間 //就是O(n*lgn) // //加入了反向查找后,性能得到了巨大的提升 }
public:
//在控制台打印測試用例 void testPrint() {
//限制:只能打印100個元素以內的堆,因為控制台一行的字符數量有限 if (size() >= 100) { cout << "Fancy print can only work for less than 100 int"; return; }
//限制:只能打印類型是int的堆 if (typeid(Item) != typeid(int)) { cout << "Fancy print can only work for int item"; return; }
cout << "The Heap size is: " << size() << endl; cout << "data in heap: "; for (int i = 0; i < size(); i++) { cout << data[i] << " "; } cout << endl; cout << endl;
int n = size(); int max_level = 0; int number_per_level = 1; while (n > 0) { max_level += 1; n -= number_per_level; number_per_level *= 2; }
int max_level_number = int(pow(2, max_level - 1)); int cur_tree_max_level_number = max_level_number; int index = 0; for (int level = 0; level < max_level; level++) { string line1 = string(max_level_number * 3 - 1, ' ');
int cur_level_number = min(count - int(pow(2, level)) + 1, int(pow(2, level)));
bool isLeft = true;
for (int index_cur_level = 0; index_cur_level < cur_level_number; index++, index_cur_level++) { putNumberInLine(indexes[index], line1, index_cur_level, cur_tree_max_level_number * 3 - 1, isLeft);
isLeft = !isLeft; } cout << line1 << endl;
if (level == max_level - 1) { break; }
string line2 = string(max_level_number * 3 - 1, ' '); for (int index_cur_level = 0; index_cur_level < cur_level_number; index_cur_level++) { putBranchInLine(line2, index_cur_level, cur_tree_max_level_number * 3 - 1); }
cout << line2 << endl;
cur_tree_max_level_number /= 2; } }
private:
void putNumberInLine(int num, string &line, int index_cur_level, int cur_tree_width, bool isLeft) {
int sub_tree_width = (cur_tree_width - 1) / 2;
int offset = index_cur_level * (cur_tree_width + 1) + sub_tree_width;
assert(offset + 1 < line.size());
if (num >= 10) { line[offset + 0] = '0' + num / 10; line[offset + 1] = '0' + num % 10; } else { if (isLeft) line[offset + 0] = '0' + num; else line[offset + 1] = '0' + num; } }
void putBranchInLine(string &line, int index_cur_level, int cur_tree_width) {
int sub_tree_width = (cur_tree_width - 1) / 2;
int sub_sub_tree_width = (sub_tree_width - 1) / 2;
int offset_left = index_cur_level * (cur_tree_width + 1) + sub_sub_tree_width;
assert(offset_left + 1 < line.size());
int offset_right = index_cur_level * (cur_tree_width + 1) + sub_tree_width + 1 + sub_sub_tree_width;
assert(offset_right < line.size());
line[offset_left + 1] = '/'; line[offset_right + 0] = '\\'; } };
#endif |
Dijkstra.h:
#ifndef DIJKSTRA_H #define DIJKSTRA_H
#include "Edge.h" #include "MinIndexHeap.h" #include <iostream> #include <vector> #include <stack> using namespace std;
//Dijkstra 算法實現最短路徑 template<typename Graph, typename Weight> class Dijkstra {
private:
Graph &G; //圖的引用,即要進行操作的圖 int s; //起始頂點 s,s 即 source Weight *distTo; //起始頂點 s 到每一個頂點的當前最短距離 bool *marked; //對已經找到最短路徑的頂點進行標識 vector<Edge<Weight>*> from; //經由哪條邊到達了當前頂點
public:
Dijkstra(Graph &graph, int s) :G(graph) {
this->s = s; distTo = new Weight[G.V()]; marked = new bool[G.V()];
for (int i = 0; i < G.V(); i++) { //由於不知道 distTo 數組中元素的具體類型, //所以使用模板類型Weight的默認構造函數, //如果指定的模板為 int,會被初始化為 0 distTo[i] = Weight(); marked[i] = false; from.push_back(NULL); }
MinIndexHeap<Weight> ipq(G.V());
// Dijkstra // //對起始頂點 s 到自身的最短距離進行初始化, //如果指定的模板為 int,會被初始化為 0 distTo[s] = Weight(); ipq.insert(s, distTo[s]); marked[s] = true;
//只要最小索引堆不為空,就進行循環 while (!ipq.isEmpty()) { //從最小索引堆中找到當前最短距離最小的頂點 v //此時,distTo[v] 就是起始頂點 s 到頂點 v 的 //最短距離 int v = ipq.extractMinIndex();
marked[v] = true;
//注意:聲明迭代器時,前面還要加 typename,表明 //adjIterator 是 Graph 中的類型,而不是成員變量 typename Graph::adjIterator adj(G, v); //對當前頂點 v 的所有相鄰頂點依次進行松弛操作 for (Edge<Weight> *e = adj.begin(); !adj.end(); e = adj.next()) { //當前頂點 v 的相鄰頂點 w int w = e->other(v); //如果頂點 w 沒有被標識,即從起始頂點 // s 到相鄰頂點 w 的最短路徑還沒有找到 if (!marked[w]) { //(1)如果還沒有邊到達相鄰頂點 w //(2)或:"經過"當前頂點 v 到相鄰頂點 w 所得到的 //路徑小於"不經過"當前頂點 v 到相鄰頂點 w 所得到 //的路徑,就進行松弛操作 if (from[w] == NULL || distTo[v] + e->wt() < distTo[w]) { distTo[w] = distTo[v] + e->wt(); from[w] = e;
//判斷最小索引堆中是否包含頂點 w, //如果包含就更新,否則直接插入 if (ipq.contain(w)) { ipq.change(w, distTo[w]); } else { ipq.insert(w, distTo[w]); } } } } } }
~Dijkstra() { delete []distTo; delete []marked; }
//頂點 s 到頂點 w 的最短距離 Weight shortestPathTo(int w) { assert(w >= 0 && w < G.V()); return distTo[w]; }
//判斷頂點 s 到頂點 w 是否有路徑, //即判斷二者是否連通即可 bool hasPathTo(int w) { assert(w >= 0 && w < G.V()); return marked[w]; }
//找到從頂點 s 到頂點 w 的最短路徑的邊的組成:通過from數組 //從頂點 w 倒推回去,並存儲在棧中,最后再從棧中轉存到向量中 void shortestPath(int w, vector<Edge<Weight>> &vec) {
assert(w >= 0 && w < G.V());
stack<Edge<Weight>*> s;
Edge<Weight> *e = from[w];
//直到倒推到起始頂點,對於有向圖 //來說,e->v() 即一條邊的起點 while (e->v() != this->s) { s.push(e); e = from[e->v()]; } s.push(e);
//只要棧不為空,就將棧頂元素放入 //向量中,並出棧 while (!s.empty()) { e = s.top(); vec.push_back(*e); s.pop(); } }
//打印從頂點 s 到頂點 w 的最短路徑 void showPath(int w) {
assert(w >= 0 && w < G.V());
vector<Edge<Weight>> vec; shortestPath(w, vec); for (int i = 0; i < vec.size(); i++) { cout << vec[i].v() << " -> "; if (i == vec.size() - 1) { cout << vec[i].w() << endl; } } } };
#endif |
main.cpp:
#include "SparseGraph.h" #include "DenseGraph.h" #include "ReadGraph.h" #include "Dijkstra.h" #include <iostream> using namespace std;
int main() {
string filename = "testG1.txt"; int V = 5;
//稀疏圖:通常在實際生活中所處理的圖,稀疏圖相對更多 SparseGraph<int> g = SparseGraph<int>(V, true); //SparseGraph<int> g = SparseGraph<int>(V, false); ReadGraph<SparseGraph<int>, int> readGraph(g, filename);
cout << "Test Dijkstra:" << endl << endl; Dijkstra<SparseGraph<int>, int> dij(g, 0); for (int i = 1; i < V; i++) { cout << "Shortest Path to " << i << " : " << dij.shortestPathTo(i) << endl;
dij.showPath(i);
cout << "----------" << endl; }
system("pause"); return 0; }
//每一次插入和更新,都使用 logV 的時間復雜度,而整個 //Dijkstra 算法過程中,要對所有的邊進行一次遍歷,最 //終使得算法的時間復雜度是 O(E*logV) 這個級別的 |
運行一覽:
其中,testG1.txt 的內容如下:
該文件可以分成兩個部分:
(1)第一行:兩個數字分別代表頂點數和邊數
(2)其它行:每一行的前兩個數字表示一條邊,第三個數字表示權值
【made by siwuxie095】