最近在看《挑战程序设计竞赛》,边看边整理一些知识,这篇内容是有关最短路径的几个主要的算法:Bellman-Ford Algorithm、Dijkstra Algorithm、Floyd-Warshall Algorithm
前两者主要是针对单源最短路问题,后者用来解决任意两点之间的最短路径以及求传递闭包问题的
1.Bellman-Ford Algorithm这个算法的原理可以用一个公式来概括:distance[i] = distance[j] + cost(j, i)
distance[i]:用来表示顶点 i 到源点的最短路径长
cost(j, i):用来表示顶点 j 到顶点 i 之间的边的权值
需要注意的几点是:如果处理的图是DAG(有向无环图)的话,用这个公式考虑就足够了;每次更新都是针对整个图的所有边而言更新是从源点开始的下面是C++代码
#include <iostream>
#include <algorithm>
using namespace std;
struct Edge{
int from;
int to;
int cost;
};
int dis[maxn]; //最短距离
Edge edge[maxn]; //边集
//初始化每个点的最短路径长
void initial(int vertex_num) {
for (int i = 0; i < vertex_num; i++) dis[i] = INF;
}
void Bellman-Ford(int s, int vertex_num, int edge_num) {
dis[s] = 0; //设置源点的最短路为0
while (true) {
bool is_update = false;
//查询每条边的始点和终点
for (int i = 0; i < edge_num; i++) {
Edge temp = edge[i];
//如果始点已经找到最短路,而且终点的最短路值大于始点值+边权,更新
if (dis[temp.from] != INF && dis[temp.to] > dis[from] + temp.cost) {
dis[temp.to] = dis[temp.from] + temp.cost;
is_update = true;
}
}
if (!is_update) break; //如果没有再更新,退出循环
}
}
由于该算法在原则上最短路不会经过同一个顶点两次,所以最外层循环最多执行vertex_num – 1次,所以如果更新进行到第vertex_num次,一定出现了环
//如果返回true则存在圈
bool find_loop(int vertex_num, int edge_num, int s) {
memset(dis, INF, sizeof(dis));
dis[s] = 0;
for (int i = 0; i < vertex_num; i++) {
for (int j = 0; j < edge_num; j++) {
Edge temp = edge[j];
if (dis[temp.to] > dis[temp.from] + temp.cost) {
dis[temp.to] = dis[temp.from] + temp.cost;
if (i == vertex_num - 1) return true;
}
}
}
}
*注意到如果有些点没有确定是真正的最短路径点,但是在求解的时候却认为是最短路径点,而且还使用这些点来求和它相邻的那些点的最短路径,这样便会造成一个连环的错误。所以可以对以上算法进行修改:
1)找到最短距离已经确定的点,从该点出发更新相邻点的最短距离;
2)此后不需要在关心1)中已经确定好的点;
改进后最主要解决的是如何知道哪些点是已经确定好的?
2.Dijkstra Algorithm
算法原理:dis[u] = min{ dis[u], dis[v] + cost(v, u) }
以顶点v为核心,更新从v 点出发的所有邻接点,而且在这之后,v点不需要再使用了
#include <iostream>
#include <algorithm>
using namespace std;
int dis[maxn];
bool is_used[maxn];
void Dijkstra(int v, int s, int e) {
fill(dis, dis + v, INF);
fill(is_used, is_used + e, false);
dis[s] = 0;
while (true) {
int temp_v = -1;
//选出最短的最短路径
for (int u = 0; u < v; u++) {
if (!is_used[u] && (temp_v == -1 || dis[u] < dis[temp_v])) {
temp_v = u;
}
}
//如果所有的点都已经处理完了
if (temp_c == -1) break;
//更新
for (int u = 0; u < v; u++) {
dis[u] = min(dis[u], dis[temp_v] + cost[temp_v][u]);
}
//状态更新
is_used[temp_v] = true;
}
}
但是这样写的话,其实有点浪费,因为每次选取最短的最短路径都要遍历所有的点,选取邻接点也要遍历所有的点,但是其实这是没有必要的,如果采用优先队列来管理的话会快得多,关于下面那句:
if (dis[temp_v] < p.first) continue;
需要这样理解:
当前取出来的所谓最小路径点不是真的最小路径点,产生原因会是这样:与当前点相连的顶点不止一个,被压入队列中的这对“最短路径-点”是由其中一个已经确定是最短路径的点产生的,而之后又有一个或者几个与之相连的点对这个点的最小路径值进行了更新,也就是说之前压入的“最小路径-点”已经不是真正的最小路径了,所以才会有上面那个判断
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
#include <utility>
using namespace std;
typedef pair<int, int> R; //最短路->顶点
int dis[maxn];
bool is_used[maxn];
struct Edge{
int to, cost;
};
vector<Edge> G[maxn];
void Dijkstra(int v, int s, int e) {
priority_queue<R, vector<R>, greater<R> > que;
fill(dis, dis + v, INF);
dis[s] = 0;
que.push(R(0, s));
while (!que.empty()) {
//取出最小的最短路
R p = que.top();
que.pop();
int temp_v = p.second;
if (dis[temp_v] < p.first) continue;
for (int i = 0; i < G[temp_v].size(); i++) {
Edge e = G[temp_v][i];
if (dis[e.to] > dis[e.from] + dis[e.cost]) {
dis[e.to] = dis[e.from] + dis[e.cost];
que.push(R(dis[e.to], e.to));
}
}
}
}
3.Floyd-Warshall Algorithm
这个比较简单,贴上代码
#include <iostream>
#include <algorithm>
using namespace std;
int dis[maxn][maxn];
void warshall_floyd() {
for (int k = 0; k < v; k++) {
for (int i = 0; i < v; i++) {
for (int j = 0; j < v; j++) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
}