单源最短路径:从顶点v可达顶点u表示为v->u,在所有的可达路径中存在一条权值最小的路径,则这条路径叫做v到u的最短路径。那么单源最短路径指的是:求出给定源点s到其他所有顶点的最短路径。
最短路径的性质:对于一给定的带权图G=(V,G),所定义的加权函数为w:E->R。设p=<v1,v2,v3,……,vk>是从v1到vk的最短路径。对于任意i,j,其中1<=i<=j<=k,设pij=<vi,vi+1,……,vj>为p中从顶点vi到顶点vj的子路径。那么,pij是从vi到vj的最短路径。(最短路径的子路径是最短路径)
最短路径中不存在负权回路,因为如果存在负权回路,我们总是可以顺着已经找的“最短”路径,再穿过负权回路而获得一条权值更小的路径。这样无限次的穿过,我们得到这条路径的权值为负无穷。同样,最短路径中也不存在正权回路,对于权值为零的回路可有可无。
路径松弛性质:如果p=<v0,v1,……,vk>是从s=v0到vk的最短路径,而且p的边按照(v0,v1),(v1,v2),……,(vk-1,vk)的顺序进行松弛,那么d[vk]=sp(s,vk)(表示s到vk的最短路径上的权值),这个性质的保持并不受其他松弛操作的影响,即使它们与p的边上的松弛操作混合在一起也是一样的。
下面介绍三种方法求最短路径:
(1)Bellman-Ford算法,该算法用来解决一般的单源最短路径问题(边的权值可以为负,但是不存在负权回路)。此方法可以检测是否有存在从源点可达的负权回路。
(2)在有向无环图中,我们可以根据图中顶点的拓扑顺序,在线性时间内计算出单源最短路径路径的值。
(3)Dijkstra算法,运行时间比Bellman-Ford算法时间低,但此算法有一个不足之处是:必须要求边上的权值非负。
在介绍上述三个算法之前,我们先介绍两个基本操作:初始化操作和松弛操作。
初始化操作的伪代码:Initialize-Single-Source(s)
for each vertex v∈V[G]
do d[v] <- ∞
p[v] <- NIL
d[s] <- 0;
其中d[v]表示从源点到v的最短路径上权值的上界,p[v]表示最短路径上v的前驱顶点。
松弛操作的伪代码:Relax(u,v,w)
<span style="white-space:pre"> </span>if d[v] > d[u] + w(u,v)
then d[v] <- d[u] + w(u,v)
p[v] <- u;
在松弛一条边(u,v)时,要测试是否可以通过u,对目前为止到v的最短路径进行更新。如果可以更新的话,则更新d[v]和p[v]。一次松弛操作可以减少最短路径估计值d[v],并且更新v的前驱p[v]。
方法一:Bellman-Ford算法
对于给定的带权有向图G=(V,G),其源点为s,加权函数为w:E->R,对该图运行Bellman-Ford算法后返回一个布尔值,表明图中是否存在一个从源点可达的负权回路。若存在这样的回路,算法说明该问题无解;若不存在这样的回路,算法将产生最短路径(p[v])及其权值(d[v])。
此算法对所有的边执行|v|-1次松弛操作。经过|v|-1次迭代后,对于s可达的顶点v,都有d[v]=sp(s,v),该算法的时间复杂度为o(VE)。
证明:设v为从s可达的任意顶点,p=<v0,v1,……,vk>是任一条从s到v的最短路径,其中v0=s和vk=v。路径p至多有|v|-1条边,所以k<=|v|-1。在每一次松弛所有边的操作过程中,第i次迭代被松弛的边为(vi-1,vi),由上述的路径松弛性质,可得d[v]=d[vk]=sp(s,v)。/*Bellman_Frod算法求最短路径*/
#include<iostream>
#include<vector>
#include<fstream>
using namespace std;
//边类
struct Node
{
int value;//存储边的终点
int weight;//存储边的权值
Node(){}
Node(int value,int weight)
{
this->value = value;
this->weight = weight;
}
};
vector<vector<Node>> mGraph;//图结构
int nodeNum;//图中顶点数
int edgeNum;//图中边数
vector<int> d;//从源点到顶点v的最短路径估计
vector<int> p;//当前顶点的前驱值
//存储图结构
void readGraph()
{
fstream fin("E:\\SPData\\data01.txt");//打开文件
fin>>nodeNum>>edgeNum;//读取顶点数和边数
mGraph.resize(nodeNum);
d.resize(nodeNum);
p.resize(nodeNum);
int s , t, w;
Node tmp;
while(fin>>s>>t>>w)
{
tmp.value = t;
tmp.weight = w;
mGraph[s].push_back(tmp);
}
fin.close();
}
//初始化d[v]和p[v]的值
void init_single_source(int s)
{
for(int i = 0; i < nodeNum; ++i)
{
d[i] = 100;
p[i] = -1;
}
d[s] = 0;
}
//松弛操作
void relax(int u, int v, int w)
{
if(d[v] > d[u] + w)
{
d[v] = d[u] + w;
p[v] = u;
}
}
/*Bellman-Ford算法:如果此方法返回true,则最短路径值存储在d[v],前驱值存储在p[v],我们可以根据p[v]得到最短路径所经过的顶点,如果此方法返回false,则表明该图中存在负权回路*/
bool bellman_ford(int s)
{
init_single_source(s);//初始化
for(int i = 1; i < nodeNum; ++i)//执行|v|-1次迭代
{
for(int j = 0; j < nodeNum; ++j)//对所有的边进行松弛操作
{
int count = mGraph[j].size();
for(int k = 0; k < count; ++k)
{
relax(j,mGraph[j][k].value,mGraph[j][k].weight);
}
}
}
//判断是否存在负权回路
for(int j = 0; j < nodeNum; ++j)
{
int count = mGraph[j].size();
for(int k = 0; k < count; ++k)
{
int u = j;
int v = mGraph[j][k].value;
int w = mGraph[j][k].weight;
if(d[v] > d[u] + w)
{
return false;
}
}
}
return true;
}
int main(void)
{
readGraph();
bellman_ford(0);
system("pause");
return 0;
}
我们可以对此方法进行改进:我们不必要执行|v|-1次对所有边的松弛操作。如果我们知道最短路径上边数最小值的最大值m,我们只要执行m+1次迭代操作就可以,即使m未知。主要代码如下:Bellman-Ford-(M+1)
change = true;
while(change)
do change = false;
for each edge(u,v)
do relax(u,v,w);
Relax(u,v,w)
if(d[v] > d[u] + w(u,v))
<span style="white-space:pre"> </span>then d[v] = d[u] + w(u,v);
p[v] = u;
change = true;
方法二:有向无环图中单源最短路径
按照顶点的拓扑序列对DAG图(有向无环图)G=(V,G)的边进行松弛操作后,就可以在o(V+E)时间内计算出单源最短路径。在一个DAG图中最短路径总是存在的,因为即使图中有权为负的边,也不可能存在负权回路。算法首先对DAG进行拓扑排序,以便获得顶点的线性序列。如果从u到v存在一条路径,则在拓扑序列u先于v。然后我们对所有的顶点执行一趟操作。当对每个顶点处理时,松弛从该顶点出发的所有的边。可以根据路径松弛性质证明此算法的正确性。#include<iostream>
#include<vector>
#include<fstream>
#include<queue>
using namespace std;
//边类
struct Node
{
int value;//存储边的终点
int weight;//存储边上的权值
Node(){}
Node(int value,int weight)
{
this->value = value;
this->weight = weight;
}
};
vector<vector<Node>> mGraph;//图结构
int nodeNum;//图中顶点数
int edgeNum;//图中边数
vector<int> d;//从源点到顶点v的最短路径估计
vector<int> p;//当前顶点的前驱值
vector<int> indegree;//存储当前顶点的入度数
vector<int> top_arr;//拓扑排序的序列
//读取图文件
void readGraph()
{
fstream fin("E:\\SPData\\data02.txt");//打开文件
fin>>nodeNum>>edgeNum;//读取顶点数和边数
mGraph.resize(nodeNum);
d.resize(nodeNum);
p.resize(nodeNum);
top_arr.resize(nodeNum);
indegree.resize(nodeNum);
int s , t, w;
Node tmp;
while(fin>>s>>t>>w)
{
tmp.value = t;
tmp.weight = w;
mGraph[s].push_back(tmp);
}
fin.close();
}
//初始化indegree数组
void init_indegree()
{
for(int i = 0; i < nodeNum; ++i)
{
int count = mGraph[i].size();
for(int j = 0; j < count; ++j)
{
indegree[mGraph[i][j].value]++;
}
}
}
//拓扑排序
void top_sort()
{
queue<int> q;
int index = 0;
for(int i = 0; i < nodeNum; ++i)
{
if(indegree[i] == 0)
{
q.push(i);
}
}
while(!q.empty())
{
int num = q.front();
top_arr[index++] = num;
int count = mGraph[num].size();
for(int i = 0; i < count; ++i)
{
if((--indegree[mGraph[num][i].value]) == 0)
{
q.push(mGraph[num][i].value);
}
}
q.pop();
}
}
//初始化d[v]和p[v]的值
void initialize_single_source(int s)
{
for(int i = 0; i < nodeNum; ++i)
{
d[i] = 10;
p[i] = -1;
}
d[s] = 0;
}
//松弛操作
void relax(int u, int v, int weight)
{
if(d[v] > d[u] + weight)
{
d[v] = d[u] + weight;
p[v] = u;
}
}
void DAG_Shortest_Paths(int s)
{
init_indegree();//初始化每个顶点的入度数
top_sort();//调用拓扑排序方法
initialize_single_source(s);//初始化d[v]和p[v]
for(int i = 0; i < nodeNum; ++i)//每个顶点遍历一次
{
int val = top_arr[i];
int count = mGraph[val].size();
for(int j = 0; j < count; ++j)//相应顶点的出边进行松弛操作
{
int u = val;
int v = mGraph[val][j].value;
int weight = mGraph[val][j].weight;
relax(u,v,weight);
}
}
}
int main(void)
{
readGraph();
DAG_Shortest_Paths(0);
system("pause");
return 0;
}
方法三:Dijstra算法
Dijstra算法解决了有向图G=(V,G)上带权的单源最短路径问题,但是要求所有边的权值为非负。Dijstra算法设置了一顶点集合S,从源点s到集合中的顶点的最短路径的权值均已经确定。算法反复选择具有最短路径估计的顶点u∈V-S,并将u加入到S中,然后对u的所有出边进行松弛操作。在选择最短路径估计时,我们使用的优先队列,排序的关键字为d[v]值。Dijstra算法的时间复杂度依赖于最小优先队列的实现。如果使用一般的数组存储优先队列,因为每次找最小值都要扫描一遍优先队列,总共需要执行|v|次查找最小值得操作,并且需要执行|E|次松弛操作,所以时间复杂度为o(V*V+E)。如果使用二叉堆来实现最小优先队列。每次找到最小值的时间复杂度为o(lgV),总共需要|v|次,而且每松弛一条边时时间复杂度也为o(lgV),最多需要松弛|E|次,所以时间复杂度为o((V+E)lgV)。适合于稀疏图。使用一般的数组存储优先队列,时间复杂度为o(V*V+E)的代码。#include<iostream>
#include<vector>
#include<fstream>
#include<queue>
using namespace std;
//边类
struct Node
{
int value;//存储边的终点
int weight;//存储边上的权值
Node(){}
Node(int value,int weight)
{
this->value = value;
this->weight = weight;
}
};
//源点到顶点v的距离
struct Distance
{
int dis;//到当前顶点的距离
bool flag;//标记是不是最短距离值
Distance(){}
Distance(int dis,bool flag)
{
this->dis = dis;
this->flag = flag;
}
};
vector<vector<Node>> mGraph;//图结构
int nodeNum;//图中顶点数
int edgeNum;//图中边数
vector<Distance> d;//源点到顶点v的距离
vector<int> p;//存储当前顶点的前驱值
//读取图文件
void readGraph()
{
fstream fin("E:\\SPData\\data03.txt");//打开文件
fin>>nodeNum>>edgeNum;//读取顶点数和边数
mGraph.resize(nodeNum);
d.resize(nodeNum + 1);
p.resize(nodeNum);
int s , t, w;
Node tmp;
while(fin>>s>>t>>w)
{
tmp.value = t;
tmp.weight = w;
mGraph[s].push_back(tmp);
}
fin.close();
}
//初始化d[v]和p[v]
void initialize_signal_source(int s)
{
for(int i = 0; i < nodeNum; ++i)
{
d[i].dis = 255;
d[i].flag = false;
p[i] = -1;
}
d[s].dis = 0;
d[nodeNum].dis = 255;
d[nodeNum].flag = false;
}
//松弛操作
void relax(int u, int v, int weight)
{
if(d[v].dis > d[u].dis + weight)
{
d[v].dis = d[u].dis + weight;
p[v] = u;
}
}
//Dijstra算法
void dijkstra(int s)
{
initialize_signal_source(s);//初始化
for(int i = 0; i < nodeNum; ++i)
{
int index = nodeNum;
for(int j = 0; j < nodeNum; ++j)//遍历每个顶点找到最小值
{
if(!d[j].flag && d[j].dis < d[index].dis)
{
index = j;
}
}
d[index].flag = true;
int count = mGraph[index].size();
for(int k = 0; k < count; ++k)//松弛以该顶点为出度的边
{
relax(index,mGraph[index][k].value,mGraph[index][k].weight);
}
}
}
int main(void)
{
readGraph();
dijkstra(0);
system("pause");
return 0;
}
使用二叉堆来实现最小优先队列的代码:#include<iostream>
#include<vector>
#include<fstream>
using namespace std;
/*优先队列里存储元素类型*/
struct Node
{
int ver;//源点可达的顶点
int dis;//源点到可达顶点的距离
Node(){}
Node(int ver, int dis)
{
this->ver = ver;
this->dis = dis;
}
};
//优先队列类
struct HeapMin
{
vector<Node> ve;//优先队列中存储元素的容器
vector<int> index;//顶点在优先队列中的下标index[2] = 3;表示顶点为2的顶点存储在优先队列的3号位置。
int size;//优先队列的总容量
int cur;//当前优先队列中元素的个数
//初始化优先队列
HeapMin(int size)
{
this->size = size;
ve.resize(size);
index.resize(size);
cur = 0;
}
//优先队列中的某个位置(pos)的元素值被改变了,重新调整优先队列
void siftUp(int pos)
{
while(pos > 0 && ve[pos].dis < ve[(pos - 1)/2].dis)
{
swap(index[ve[pos].ver],index[ve[(pos - 1)/2].ver]);
swap(ve[pos],ve[(pos - 1)/2]);
pos = (pos - 1)/2;
}
}
//判断优先队列是否为空
bool empty()
{
if(cur == 0)
return true;
else
return false;
}
//返回优先队列中的最小元素但是不弹出
Node top()
{
return ve[0];
}
//弹出优先队列对头元素
void pop()
{
cur--;
swap(index[ve[cur].ver],index[ve[0].ver]);
swap(ve[0],ve[cur]);
siftDown(0);
}
//从当前位置开始向下调整优先队列
void siftDown(int start)
{
int i = start;
while((2 * i + 1) < cur && (2 * i + 2) < cur)
{
if(ve[2 * i + 1].dis < ve[2 * i + 2].dis)
{
if(ve[i].dis > ve[2 * i + 1].dis)
{
index[ve[2 *i + 1].ver] = i;
index[ve[i].ver] = 2 * i + 1;
swap(ve[i],ve[2 * i + 1]);
i = 2 * i + 1;
}
else
{
break;
}
}
else
{
if(ve[i].dis > ve[2 *i + 2].dis)
{
index[ve[2 * i + 2].ver] = i;
index[ve[i].ver] = 2 * i + 2;
swap(ve[i],ve[2 * i + 2]);
i = 2 * i + 2;
}
else
{
break;
}
}
}
if((2 * i + 1) < cur && ve[i].dis > ve[2 * i + 1].dis)
{
swap(ve[i],ve[2 * i + 1]);
index[ve[2 * i + i].ver] = i;
index[ve[i].ver] = 2 * i + 1;
}
}
//创建一个优先队列
void createHeap()
{
for(int i = cur / 2 - 1; i >= 0; --i)
{
siftDown(i);
}
}
//向优先队列中插入元素,但是不调整优先队列
void insert(int v, int w)
{
Node tmp(v,w);
ve[cur] = tmp;
index[cur++] = v;
}
};
//边的类型
struct Edge
{
int value;
int weight;
Edge(){}
Edge(int value,int weight)
{
this->value = value;
this->weight = weight;
}
};
vector<vector<Edge>> mGraph;//图结构
int nodeNum;//图中顶点数
int edgeNum;//图中边数
vector<int> d;//源点到当前顶点的距离
vector<int> p;//最短路径中的前一个顶点
HeapMin qu(10);//创建一个优先队列
//读取图文件
void readGraph()
{
fstream fin("E:\\SPData\\data03.txt");//打开文件
fin>>nodeNum>>edgeNum;//读取顶点数和边数
mGraph.resize(nodeNum);
d.resize(nodeNum);
p.resize(nodeNum);
int s , t, w;
Edge tmp;
while(fin>>s>>t>>w)
{
tmp.value = t;
tmp.weight = w;
mGraph[s].push_back(tmp);
}
fin.close();
}
//初始化源点到其他顶点的距离,并且初始化通过那个顶点到达这个顶点
void initialize_signal_source(int s)
{
for(int i = 0; i < nodeNum; ++i)
{
d[i] = 255;
p[i] = -1;
}
d[s] = 0;
}
//松弛边的操作
void relax(int u, int v, int w)
{
if(d[v] > d[u] + w)
{
d[v] = d[u] + w;
p[v] = u;
int pos = qu.index[v];//得到要修改优先队列中距离值得下标
qu.ve[pos].dis = d[u] + w;//修改对应优先队列中距离值
qu.siftUp(pos);//从新调整优先队列
}
}
//迪杰斯特拉算法
void dijkstra(int s)
{
initialize_signal_source(s);//初始化
for(int i = 0; i < nodeNum; ++i)
{
qu.insert(i,d[i]);
}
qu.createHeap();//创建优先队列
while(!qu.empty())//判断优先队列是否为空
{
int val = qu.top().ver;//返回最小值
qu.pop();//队头元素出队列
int count = mGraph[val].size();
for(int i = 0; i < count; ++i)
{
int u = val;
int v = mGraph[val][i].value;
int weight = mGraph[val][i].weight;
relax(u,v,weight);
}
}
}
int main(void)
{
readGraph();
dijkstra(2);
system("pause");
return 0;
}