上次我们介绍了 D i j k s t r a Dijkstra Dijkstra算法,那是一个非常高效的单源最短路算法.然而有些遗憾, D i j k s t r a Dijkstra Dijkstra并不能处理带有负权值的图,这次我们要介绍的这个算法,是一个无论从思想还是实现上来说都堪称完美的算法,这个算法叫做 B e l l m a n − F o r d Bellman-Ford Bellman−Ford 算法.它也要用到"松弛"边的思想.是一个适用范围比 D i j k s t r a Dijkstra Dijkstra更广的算法.
对于一个有 m m m条边, n n n个顶点的图,我们先定义如下描述,以表示一个图的某些关系:
源点 s t st st,表示算法所求最短路的出发顶点;
点集 V V V,表示图中所有点构成的集合,其中 ∣ V ∣ = n |V| = n ∣V∣=n;
边集 E E E,表示途中所有边构成的集合,其中 ∣ E ∣ = m |E| = m ∣E∣=m;
数组 d i s [ ] dis[] dis[],表示源点 s t st st到其余点的最短路径,数组大小为 n n n;
三元组 < u , v , w > <u,v,w> <u,v,w>,表示有一条从 u u u到 v v v,权值为 w w w的边,即 u → v = w u \to v = w u→v=w;
我们选择一个源点 s t st st,求它到其余所有点的最短路径,算法描述如下:
以一个具体的例子来说明(来源:啊哈算法),对于一个如下的图:
下面的图给出了 B e l l m a n − F o r d Bellman-Ford Bellman−Ford算法详细的步骤(注意,每次对所有边松弛都是按照边给定的顺序进行的):
通过上面的模拟,我们知道: 第四轮松弛和第三轮松弛的结果一致,并没有发生变化!据此,我们先给出结论:
对所有边的松弛最多只需进行 n − 1 n-1 n−1轮.
至于原因,非常简单,解释一下就是:
对于有 n n n个顶点的图,任意两点之间的最短路径最多包含 n − 1 n-1 n−1条边.对这句话的(描述性)证明如下:
若两个点之间有 > n − 1 \gt n-1 >n−1条边,则说明这两点之间存在环,环分为正环和负环(按环上路径和的符号分),讨论如下:
综上,对于有 n n n个顶点的图,任意两点之间的最短路径最多包含 n − 1 n-1 n−1条边,证毕.
因为上述结论成立,所以源点 s t st st到任意一点 i i i所需要中间点的个数 ≤ n − 2 \le n-2 ≤n−2,因此最多只需松弛所有边 n − 1 n-1 n−1轮.
接下来我们说说图中的负环,以及为何存在负环就会无解,如图,将上面的例子修改为以下形式:
此时 B e l l m a n − F o r d Bellman-Ford Bellman−Ford算法的松弛操作如下:
可以看出,存在负环的图,只要从源点 s t st st反复地经过负环多次,就可以松弛负环上所有的边,从而使得 s t st st到负环上的点的最短路径缩短,实际上,任何和负环相连的顶点,都可以通过多次经过负环不断缩小这个最短路径,因此存在负环的图,只要源点 s t st st经过无限次负环,最后就会使得源点 s t st st到一些点的最短路径变为 − ∞ -\infin −∞,此时我们称这些点是不收敛的,而此时这个图是不存在源点到其余点的最短路径的.
因为我们要用到所有的边进行松弛,因此在存图时,选用一个比较合适的数据结构–邻接表.用两种存图方式实现:
#include
#include
#define maxn 100010
#define inf 0x3f3f3f3f
using namespace std;
typedef pair<int,int> pii;
int dis[maxn];
vector<pii> e[maxn];
void init(int n) {
for(int i = 0; i <= n; i++)
e[i].clear();
}
bool Bellman_Ford(int n,int st) {
for(int i = 0; i <= n; i++) { //初始化dis[];
dis[i] = inf;
}
dis[st] = 0;
int len;
for(int k = 0; k < n-1; k++) { //最多松弛n-1次;
for(int i = 0; i <= n; i++) { //对于每个点;
len = e[i].size();
for(int j = 0; j < len; j++) { //松弛该点的每条出边;
if(dis[e[i][j].first] > dis[i] + e[i][j].second) {
dis[e[i][j].first] = dis[i] + e[i][j].second;
}
}
}
}
//测负环;
for(int i = 0; i <= n; i++) { //进行了n-1次松弛后,再测试是否能松弛;
len = e[i].size();
for(int j = 0; j < len; j++) {
if(dis[e[i][j].first] > dis[i] + e[i][j].second) { //能松弛边,则出现负环;
return false;
}
}
}
return true;
}
int main() {
int m,n,st,u,v,w;
while(cin>>n>>m>>st) {
init(n);
for(int i = 0; i < m; i++) {
cin>>u>>v>>w;
e[u].push_back(make_pair(v,w));
// e[v].push_back(make_pair(u,w)); //无向图;
}
if(Bellman_Ford(n,st)) {
for(int i = 0; i <= n; i++) {
cout<<dis[i]<<" ";
}
cout<<endl;
}
else
cout<<"No Solution!"<<endl;
}
return 0;
}
#include
#include
#define maxn 100010
#define maxm 10000010
#define inf 0x3f3f3f3f
using namespace std;
struct node {
int v,w,nxt;
}e[maxm];
int dis[maxn],head[maxn],tot = 0;
void init() {
tot = 0;
memset(head,-1,sizeof head);
}
void addedge(int u,int v,int w) {
e[tot].v = v;
e[tot].w = w;
e[tot].nxt = head[u];
head[u] = tot++;
}
bool Bellman_Ford(int n,int st) {
for(int i = 0; i <= n; i++) { //初始化dis[];
dis[i] = inf;
}
dis[st] = 0;
for(int k = 0; k < n-1; k++) { //最多松弛n-1次;
for(int i = 0; i <= n; i++) { //对于每个点;
for(int j = head[i]; j != -1; j = e[j].nxt) { //松弛该点的每条出边;
if(dis[e[j].v] > dis[i] + e[j].w) {
dis[e[j].v] = dis[i] + e[j].w;
}
}
}
}
//测负环;
for(int i = 0; i <= n; i++) { //进行了n-1次松弛后,再测试是否能松弛;
for(int j = head[i]; j != -1; j = e[j].nxt) {
if(dis[e[j].v] > dis[i] + e[j].w) { //能松弛边,则出现负环;
return false;
}
}
}
return true;
}
int main() {
int m,n,st,u,v,w;
while(cin>>n>>m>>st) {
init();
for(int i = 0; i < m; i++) {
cin>>u>>v>>w;
addedge(u,v,w);
// addedge(v,u,w); //无向图;
}
if(Bellman_Ford(n,st)) {
for(int i = 0; i <= n; i++) {
cout<<dis[i]<<" ";
}
cout<<endl;
}
else
cout<<"No Solution!"<<endl;
}
return 0;
}
注意vector存图时要先清空其中信息再使用,数组存图时边计数器tot要清零,head[]要初始化为-1,再使用.
不难看出 B e l l m a n − F o r d Bellman-Ford Bellman−Ford的时间复杂度是 O ( V E ) O(VE) O(VE),其中 V V V表示顶点数, E E E表示边数.
从上面的第一个例子,我们可以看出一个特点:
不存在负环的图中,在一轮松弛操作中最短路被改变的顶点,在接下来一轮松弛中的最短路保持不变,如(例1):
第一轮松弛,最短路改变的点:2,5;
第二轮松弛,最短路改变的点:3,4;
第三次轮弛,最短路改变的点:5;
第四次轮弛,最短路改变的点:无,算法结束.
这个特点并不是特例,而是普遍规律,适用于无负环的图,可以进行证明,篇幅所限,故省略.据此我们提出优化思路:
我们为什么可以这么优化?
因为上一轮松弛过最短路的顶点,其最短路估计值可以被认为是在下一轮松弛中实际的"最短路"(反正这个值不会变,它是目前(下一轮松弛时)最优的).借助 D i j k s t r a Dijkstra Dijkstra的思想,确定最短路的点可以作为中间点松弛别的点,因而我们可以用这个假定的"最短路"松弛其他的点.
为了实现这个优化,显然只需要维护一个队列即可,每次最短路被松弛过的点入队,下一次再从队首取点,对整个图的点进行松弛,这里为了避免重复入队,限定队列中不允许有相同的点,那么什么时候说明图中存在负环呢? 很简单: 一个点入队次数 ≥ n \ge n ≥n,即说明图中存在负环,因为一个点如果 ≥ n \ge n ≥n次进入队列,则说明这个点的最短路径被松弛过 ≥ n \ge n ≥n次,于是最短路包含的边 ≥ n \ge n ≥n,因而得出这个图存在负环.实现如下:
#include
#include
#include
#define maxn 100010
#define inf 0x3f3f3f3f
using namespace std;
typedef pair<int,int> pii;
vector<pii> e[maxn];
int dis[maxn],vis[maxn],cnt[maxn];
void init(int n) {
for(int i = 0; i <= n; i++)
e[i].clear();
}
bool SPFA(int n,int st) {
for(int i = 0; i <= n; i++) {
dis[i] = inf,vis[i] = 0,cnt[i] = 0;
}
dis[st] = 0;
int cur,len;
queue<int> q;
q.push(st);
vis[st] = 1;
cnt[st]++;
while(!q.empty()) {
cur = q.front();
q.pop();
vis[cur] = 0;
len = e[cur].size();
for(int i = 0; i < len; i++) { //用这个点所有的边松弛其余点;
if(dis[e[cur][i].first] > dis[cur] + e[cur][i].second) {
if(!vis[e[cur][i].first]) {
cnt[e[cur][i].first]++;
q.push(e[cur][i].first);
vis[e[cur][i].first] = 1;
if(cnt[e[cur][i].first] >= n)
return false; //入队次数 >= n,说明存在负环;
}
dis[e[cur][i].first] = dis[cur] + e[cur][i].second;
}
}
}
return true;
}
int main() {
int m,n,st,u,v,w;
while(cin>>n>>m>>st) {
init(n);
for(int i = 0; i < m; i++) {
cin>>u>>v>>w;
e[u].push_back(make_pair(v,w));
// e[v].push_back(make_pair(u,w)); //无向图;
}
if(SPFA(n,st)) {
for(int i = 0; i <= n; i++) {
cout<<dis[i]<<" ";
}
cout<<endl;
}
else
cout<<"No Solution!"<<endl;
}
return 0;
}
#include
#include
#include
#define maxn 100010
#define maxm 10000010
#define inf 0x3f3f3f3f
using namespace std;
struct node {
int v,w,nxt;
}e[maxm];
int dis[maxn],vis[maxn],cnt[maxn],head[maxn],tot = 0;
void init() {
tot = 0;
memset(head,-1,sizeof head);
}
void addedge(int u,int v,int w) {
e[tot].v = v;
e[tot].w = w;
e[tot].nxt = head[u];
head[u] = tot++;
}
bool SPFA(int n,int st) {
for(int i = 0; i <= n; i++) {
dis[i] = inf,vis[i] = 0,cnt[i] = 0;
}
dis[st] = 0;
int cur;
queue<int> q;
q.push(st);
vis[st] = 1;
cnt[st]++;
while(!q.empty()) {
cur = q.front();
q.pop();
vis[cur] = 0;
for(int i = head[cur]; i != -1; i = e[i].nxt) { //用这个点所有的边松弛其余点;
if(dis[e[i].v] > dis[cur] + e[i].w) {
if(!vis[e[i].v]) {
cnt[e[i].v]++;
q.push(e[i].v);
vis[e[i].v] = 1;
if(cnt[e[i].v] >= n)
return false; //入队次数 >= n,说明存在负环;
}
dis[e[i].v] = dis[cur] + e[i].w;
}
}
}
return true;
}
int main() {
int m,n,st,u,v,w;
while(cin>>n>>m>>st) {
init();
for(int i = 0; i < m; i++) {
cin>>u>>v>>w;
addedge(u,v,w);
// addedge(v,u,w); //无向图;
}
if(SPFA(n,st)) {
for(int i = 0; i <= n; i++) {
cout<<dis[i]<<" ";
}
cout<<endl;
}
else
cout<<"No Solution!"<<endl;
}
return 0;
}
另外说一下,这个优化算法是由西南交通大学的段凡丁在1994年提出来的(虽然国际上不承认,并且国际上提出这个算法的时间远远早于段凡丁(1957年)),名字叫做 S P F A SPFA SPFA算法,其本质就是对 B e l l m a n − F o r d Bellman-Ford Bellman−Ford算法的队列优化,全称叫做 S h o r t e s t P a t h F a s t e r A l g o r i t h m Shortest\ Path\ Faster\ Algorithm Shortest Path Faster Algorithm.通过队列优化,显然我们减少了比较次数,只用对松弛有贡献的点进行松弛比较,这个算法的平均时间复杂度是我们尚且难下定论,但是显然大多数情况下都是比 B e l l m a n − F o r d Bellman-Ford Bellman−Ford要快的,虽然最坏情况下的时间复杂度也和 B e l l m a n − F o r d Bellman-Ford Bellman−Ford相同,为 O ( V E ) O(VE) O(VE).