最短路徑問題---SPFA演算法詳解
前言
部落格編寫人:Willam
部落格編寫時間:2017/3/12
博主郵箱:2930526477@qq.com(有志同道合之人,可以加qq交流交流程式設計心得)
1、最短路徑問題介紹
問題解釋:
從圖中的某個頂點出發到達另外一個頂點的所經過的邊的權重和最小的一條路徑,稱為最短路徑
解決問題的演算法:
之前已經對Dijkstra演算法和Floyd演算法做了介紹(不懂的可以看這篇部落格:Dijkstra演算法詳解、Floyd演算法詳解),所以這篇部落格打算對SPFA演算法做詳細的的介紹。
2、SPFA演算法介紹
SPFA演算法是求解單源最短路徑問題的一種演算法,由理查德·貝爾曼(Richard Bellman) 和 萊斯特·福特 創立的。有時候這種演算法也被稱為 Moore-Bellman-Ford 演算法,因為 Edward F. Moore 也為這個演算法的發展做出了貢獻。它的原理是對圖進行V-1次鬆弛操作,得到所有可能的最短路徑。其優於迪科斯徹演算法的方面是邊的權值可以為負數、實現簡單,缺點是時間複雜度過高,高達 O(VE)。但演算法可以進行若干種優化,提高了效率。
演算法的思路:
我們用陣列dis記錄每個結點的最短路徑估計值,用鄰接表或鄰接矩陣來儲存圖G。我們採取的方法是動態逼近法:設立一個先進先出的佇列用來儲存待優化的結點,優化時每次取出隊首結點u,並且用u點當前的最短路徑估計值對離開u點所指向的結點v進行鬆弛操作,如果v點的最短路徑估計值有所調整,且v點不在當前的佇列中,就將v點放入隊尾。這樣不斷從佇列中取出結點來進行鬆弛操作,直至佇列空為止
我們要知道帶有負環的圖是沒有最短路徑的,所以我們在執行演算法的時候,要判斷圖是否帶有負環,方法有兩種:
- 開始演算法前,呼叫拓撲排序進行判斷(一般不採用,浪費時間)
- 如果某個點進入佇列的次數超過N次則存在負環(N為圖的頂點數)
3、SPFA演算法手動操作過程
下面我們採用SPFA演算法對下圖求v1到各個頂點的最短路徑,通過手動的方式來模擬SPFA每個步驟的過程
- 初始化:
首先我們先初始化陣列dis如下圖所示:(除了起點賦值為0外,其他頂點的對應的dis的值都賦予無窮大,這樣有利於後續的鬆弛)
此時,我們還要把v1如佇列:{v1}
現在進入迴圈,直到佇列為空才退出迴圈。
- 第一次迴圈:
首先,隊首元素出佇列,即是v1出佇列,然後,對以v1為弧尾的邊對應的弧頭頂點進行鬆弛操作,可以發現v1到v3,v5,v6三個頂點的最短路徑變短了,更新dis陣列的值,得到如下結果:
我們發現v3,v5,v6都被鬆弛了,而且不在佇列中,所以要他們都加入到佇列中:{v3,v5,v6}
- 第二次迴圈
此時,隊首元素為v3,v3出佇列,然後,對以v3為弧尾的邊對應的弧頭頂點進行鬆弛操作,可以發現v1到v4的邊,經過v3鬆弛變短了,所以更新dis陣列,得到如下結果:
此時只有v4對應的值被更新了,而且v4不在佇列中,則把它加入到佇列中:{v5,v6,v4}
- 第三次迴圈
此時,隊首元素為v5,v5出佇列,然後,對以v5為弧尾的邊對應的弧頭頂點進行鬆弛操作,發現v1到v4和v6的最短路徑,經過v5的鬆弛都變短了,更新dis的陣列,得到如下結果:
我們發現v4、v6對應的值都被更新了,但是他們都在佇列中了,所以不用對佇列做任何操作。佇列值為:{v6,v4}
第四次迴圈
此時,隊首元素為v6,v6出佇列,然後,對以v6為弧尾的邊對應的弧頭頂點進行鬆弛操作,發現v6出度為0,所以我們不用對dis陣列做任何操作,其結果和上圖一樣,佇列同樣不用做任何操作,它的值為:{v4}第五次迴圈
此時,隊首元素為v4,v4出佇列,然後,對以v4為弧尾的邊對應的弧頭頂點進行鬆弛操作,可以發現v1到v6的最短路徑,經過v4鬆弛變短了,所以更新dis陣列,得到如下結果:
因為我修改了v6對應的值,而且v6也不在佇列中,所以我們把v6加入佇列,{v6}
- 第六次迴圈
此時,隊首元素為v6,v6出佇列,然後,對以v6為弧尾的邊對應的弧頭頂點進行鬆弛操作,發現v6出度為0,所以我們不用對dis陣列做任何操作,其結果和上圖一樣,佇列同樣不用做任何操作。所以此時佇列為空。
OK,佇列迴圈結果,此時我們也得到了v1到各個頂點的最短路徑的值了,它就是dis陣列各個頂點對應的值,如下圖:
4、SPFA演算法的程式碼實現
核心程式碼:
bool Graph::SPFA(int begin) {
bool *visit;
//visit用於記錄是否在佇列中
visit = new bool[this->vexnum];
int *input_queue_time;
//input_queue_time用於記錄某個頂點入佇列的次數
//如果某個入佇列的次數大於頂點數vexnum,那麼說明這個圖有環,
//沒有最短路徑,可以退出了
input_queue_time = new int[this->vexnum];
queue<int> s; //佇列,用於記錄最短路徑被改變的點
/*
各種變數的初始化
*/
int i;
for (i = 0; i < this->vexnum; i++) {
visit[i] = false;
input_queue_time[i] = 0;
//路徑開始都初始化為直接路徑,長度都設定為無窮大
dis[i].path = this->node[begin-1].data + "-->" + this->node[i].data;
dis[i].weight = INT_MAX;
}
//首先是起點入佇列,我們記住那個起點代表的是頂點編號,從1開始的
s.push(begin - 1);
visit[begin - 1] = true;
++input_queue_time[begin-1];
//
dis[begin - 1].path =this->node[begin - 1].data;
dis[begin - 1].weight = 0;
int temp;
int res;
ArcNode *temp_node;
//進入佇列的迴圈
while (!s.empty()) {
//取出隊首的元素,並且把隊首元素出佇列
temp = s.front(); s.pop();
//必須要保證第一個結點不為空
if (node[temp].firstarc)
{
temp_node = node[temp].firstarc;
while (temp_node) {
//如果邊<temp,temp_node>的權重加上temp這個點的最短路徑
//小於之前temp_node的最短路徑的長度,則更新
//temp_node的最短路徑的資訊
if (dis[temp_node->adjvex].weight > (temp_node->weight + dis[temp].weight)) {
//更新dis陣列的資訊
dis[temp_node->adjvex].weight = temp_node->weight + dis[temp].weight;
dis[temp_node->adjvex].path = dis[temp].path + "-->" + node[temp_node->adjvex].data;
//如果還沒在佇列中,加入佇列,修改對應的資訊
if (!visit[temp_node->adjvex]) {
visit[temp_node->adjvex] = true;
++input_queue_time[temp_node->adjvex];
s.push(temp_node->adjvex);
if (input_queue_time[temp_node->adjvex] > this->vexnum) {
cout << "圖中有環" << endl;
return false;
}
}
}
temp_node = temp_node->next;
}
}
}
//列印最短路徑
return true;
}
上面我們給出了核心程式碼,下面我給出完整的程式碼:
- SPFA.h檔案程式碼
/************************************************************/
/* 程式作者:Willam */
/* 程式完成時間:2017/3/12 */
/* 有任何問題請聯絡:[email protected] */
/************************************************************/
//@儘量寫出完美的程式
//#pragma once是一個比較常用的C/C++雜注,
//只要在標頭檔案的最開始加入這條雜注,
//就能夠保證標頭檔案只被編譯一次。
#pragma once
#include<iostream>
#include<string>
#include<queue>
using namespace std;
/*
本演算法是使用SPFA來求解圖的單源最短路徑問題
採用了鄰接表作為圖的儲存結構
可以應用於任何無環的圖
*/
//表結構
struct ArcNode {
int adjvex; //邊的另外一邊的頂點下標
ArcNode * next; //下一條邊的表結點
int weight;
};
struct Vnode {
string data; //頂點資訊
ArcNode * firstarc; //第一條依附在該頂點的邊
};
struct Dis {
string path; //從頂點到該該頂點最短路徑
int weight; //最短路徑的權重
};
class Graph {
private:
int vexnum; //邊的個數
int edge; //邊的條數
Vnode * node; //鄰接表
Dis * dis; //記錄最短路徑資訊的陣列
public:
Graph(int vexnum, int edge);
~Graph();
void createGraph(int); //建立圖
bool check_edge_value(int , int ,int); //判斷邊的資訊是否合法
void print(); //列印圖的鄰接表
bool SPFA(int begin); //求解最短路徑
void print_path(int begin); //列印最短路徑
};
- SPFA.cpp檔案程式碼
#include"SFPA.h"
Graph::Graph(int vexnum, int edge) {
//對頂點個數和邊的條數進行賦值
this->vexnum = vexnum;
this->edge = edge;
//為鄰接矩陣開闢空間
node = new Vnode[this->vexnum];
dis = new Dis[this->vexnum];
int i;
//對鄰接表進行初始化
for (i = 0; i < this->vexnum; ++i) {
node[i].data = "v" + to_string(i + 1);
node[i].firstarc = NULL;
}
}
Graph::~Graph() {
int i;
//釋放空間,但是記住圖中每個結點的連結串列也要一一釋放
ArcNode * p, *q;
for (i = 0; i < this->vexnum; ++i) {
//一定要注意這裡,要判斷該頂點的出度到底是否為空,不然會出錯
if (this->node[i].firstarc) {
p = node[i].firstarc;
while (p) {
q = p->next;
delete p;
p = q;
}
}
}
delete [] node;
delete [] dis;
}
// 判斷我們每次輸入的的邊的資訊是否合法
//頂點從1開始編號
bool Graph::check_edge_value(int start, int end, int weight) {
if (start<1 || end<1 || start>vexnum || end>vexnum || weight < 0) {
return false;
}
return true;
}
void Graph::print() {
cout << "圖的鄰接表的列印:" << endl;
int i;
ArcNode *temp;
//遍歷真個鄰接表
for (i = 0; i < this->vexnum; ++i) {
cout << node[i].data << " ";
temp = node[i].firstarc;
while (temp) {
cout << "<"
<< node[i].data
<< ","
<< node[temp->adjvex].data
<< ">="
<< temp->weight
<< " ";
temp = temp->next;
}
cout << "^" << endl;
}
}
void Graph::createGraph(int kind) {
//kind代表圖的種類,2為無向圖
cout << "輸入邊的起點和終點以及各邊的權重(頂點編號從1開始):" << endl;
int i;
int start;
int end;
int weight;
for (i = 0; i < this->edge; ++i) {
cin >> start >> end >> weight;
//判斷輸入的邊是否合法
while (!this->check_edge_value(start, end, weight)) {
cout << "輸入邊的資訊不合法,請重新輸入:" << endl;
cin >> start >> end >> weight;
}
ArcNode *temp = new ArcNode;
temp->adjvex = end - 1;
temp->weight = weight;
temp->next = NULL;
//如果該頂點依附的邊為空,則從以第一個開始
if (node[start - 1].firstarc == NULL) {
node[start - 1].firstarc = temp;
}
else {//否則,則插入到該連結串列的最後一個位置
ArcNode * now = node[start - 1].firstarc;
//找到連結串列的最後一個結點
while (now->next) {
now = now->next;
}
now->next = temp;
}
//如果是無向圖,則反向也要新增新的結點
if (kind == 2) {
//新建一個新的表結點
ArcNode *temp_end = new ArcNode;
temp_end->adjvex = start - 1;
temp_end->weight = weight;
temp_end->next = NULL;
//如果該頂點依附的邊為空,則從以第一個開始
if (node[end - 1].firstarc == NULL) {
node[end - 1].firstarc = temp_end;
}
else {//否則,則插入到該連結串列的最後一個位置
ArcNode * now = node[end - 1].firstarc;
//找到連結串列的最後一個結點
while (now->next) {
now = now->next;
}
now->next = temp_end;
}
}
}
}
bool Graph::SPFA(int begin) {
bool *visit;
//visit用於記錄是否在佇列中
visit = new bool[this->vexnum];
int *input_queue_time;
//input_queue_time用於記錄某個頂點入佇列的次數
//如果某個入佇列的次數大於頂點數vexnum,那麼說明這個圖有環,
//沒有最短路徑,可以退出了
input_queue_time = new int[this->vexnum];
queue<int> s; //佇列,用於記錄最短路徑被改變的點
/*
各種變數的初始化
*/
int i;
for (i = 0; i < this->vexnum; i++) {
visit[i] = false;
input_queue_time[i] = 0;
//路徑開始都初始化為直接路徑,長度都設定為無窮大
dis[i].path = this->node[begin-1].data + "-->" + this->node[i].data;
dis[i].weight = INT_MAX;
}
//首先是起點入佇列,我們記住那個起點代表的是頂點編號,從1開始的
s.push(begin - 1);
visit[begin - 1] = true;
++input_queue_time[begin-1];
//
dis[begin - 1].path =this->node[begin - 1].data;
dis[begin - 1].weight = 0;
int temp;
int res;
ArcNode *temp_node;
//進入佇列的迴圈
while (!s.empty()) {
//取出隊首的元素,並且把隊首元素出佇列
temp = s.front(); s.pop();
//必須要保證第一個結點不為空
if (node[temp].firstarc)
{
temp_node = node[temp].firstarc;
while (temp_node) {
//如果邊<temp,temp_node>的權重加上temp這個點的最短路徑
//小於之前temp_node的最短路徑的長度,則更新
//temp_node的最短路徑的資訊
if (dis[temp_node->adjvex].weight > (temp_node->weight + dis[temp].weight)) {
//更新dis陣列的資訊
dis[temp_node->adjvex].weight = temp_node->weight + dis[temp].weight;
dis[temp_node->adjvex].path = dis[temp].path + "-->" + node[temp_node->adjvex].data;
//如果還沒在佇列中,加入佇列,修改對應的資訊
if (!visit[temp_node->adjvex]) {
visit[temp_node->adjvex] = true;
++input_queue_time[temp_node->adjvex];
s.push(temp_node->adjvex);
if (input_queue_time[temp_node->adjvex] > this->vexnum) {
cout << "圖中有環" << endl;
return false;
}
}
}
temp_node = temp_node->next;
}
}
}
//列印最短路徑
return true;
}
void Graph::print_path(int begin) {
cout << "以頂點" << this->node[begin - 1].data
<< "為起點,到各個頂點的最短路徑的資訊:" << endl;
int i;
for (i = 0; i < this->vexnum; ++i) {
if (dis[i].weight == INT_MAX) {
cout << this->node[begin - 1].data << "---"
<< this->node[i].data
<< " 無最短路徑,這兩個頂點不連通" << endl;
}
else
{
cout << this->node[begin - 1].data << "---"
<< this->node[i].data
<< " weight: "
<< dis[i].weight
<< " path: "
<< dis[i].path
<< endl;
}
}
}
- main.cpp檔案程式碼
#include"SFPA.h"
//檢驗輸入邊數和頂點數的值是否有效,可以自己推算為啥:
//頂點數和邊數的關係是:((Vexnum*(Vexnum - 1)) / 2) < edge
bool check(int Vexnum, int edge) {
if (Vexnum <= 0 || edge <= 0 || ((Vexnum*(Vexnum - 1)) / 2) < edge)
return false;
return true;
}
int main() {
int vexnum; int edge;
cout << "輸入圖的種類:1代表有向圖,2代表無向圖" << endl;
int kind;
cin >> kind;
//判讀輸入的kind是否合法
while (1) {
if (kind == 1 || kind == 2) {
break;
}
else {
cout << "輸入的圖的種類編號不合法,請重新輸入:1代表有向圖,2代表無向圖" << endl;
cin >> kind;
}
}
cout << "輸入圖的頂點個數和邊的條數:" << endl;
cin >> vexnum >> edge;
while (!check(vexnum, edge)) {
cout << "輸入的數值不合法,請重新輸入" << endl;
cin >> vexnum >> edge;
}
Graph graph(vexnum, edge);
graph.createGraph(kind);
graph.print();
//記得SPFA一個引數,代表起點,這個起點從1開始
graph.SPFA(1);
graph.print_path(1);
system("pause");
return 0;
}
輸入:
2
7 12
1 2 12
1 6 16
1 7 14
2 3 10
2 6 7
3 4 3
3 5 5
3 6 6
4 5 4
5 6 2
5 7 8
6 7 9
輸出:
輸入:
1
6 8
1 3 10
1 5 30
1 6 100
2 3 5
3 4 50
4 6 10
5 6 60
5 4 20
輸出: