假如你有一张地图,地图上给出了每一对相邻城市的距离,从一个地点到另外一个地点,如何找到一条最短的路? 最短路算法要解决的就是这类问题。定义:给定一个有(无)向图,每一条边有一个权值 w,给定一个起始点 S 和终止点 T ,求从 S 出发走到 T 的权值最小路径,即为最短路径。最短路算法依赖一种性质:一条两顶点间的最短路径包含路径上其他最短路径。简单的说就是:最短路径的子路径是最短路径。这个用反证法很好证明。
一、松弛技术(Relaxation)
了解最短路算法前,必须先了解松弛技术, 为什么叫松弛,有特定原因,有兴趣可以去查查相关资料,如果简化理解松弛技术,它本质上就是一个贪心操作。松弛操作:对每个顶点v∈V,都设置一个属性d[v],用来描述从源点 s 到 v 的最短路径上权值的上界,成为最短路径估计(Shortest-path Estimate),同时π[v]代表前驱。初始化伪代码:
初始化之后,对所有 v∈V,π[v] = NIL,对v∈V – {s},有 d[s] = 0 以及 d[v] = ∞。松弛一条边(u, v),如果这条边可以对最短路径改进,则更新 d[v] 和 π[v] 。一次松弛操作可以减小最短路径估计的值 d[v] ,并更新 v 的前趋域 π[v]。下面的伪代码对边(u,v)进行了一步松弛操作:
上边的图示中,左边例子,最短路径估计值减小,右边例子,最短路径估计值不变。当发现 v 到 u 有更近的路径时,更新 d[v] 和 π[v] 。
二、Dijkstra算法
解决最短路问题,最经典的算法是 Dijkstra算法,它是一种单源最短路算法,其核心思想是贪心算法(Greedy Algorithm),Dijkstra算法由荷兰计算机科学家Dijkstra发现,这个算法至今差不多已有50年历史,但是因为它的稳定性和通俗性,到现在依然强健。另外,Dijkstra算法要求所有边的权值非负。
Dijkstra算法思想为:设 G = (V, E) 是一个带权有向图,把图中顶点集合 V 分成两组,第一组为已求出最短路径的顶点集合(用S 表示,初始时 S 中只有一个源点,以后每求得一条最短路径 , 就将其加入到集合 S 中,直到全部顶点都加入到 S 中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U 表示),按最短路径长度的递增次序依次把第二组的顶点加入 S 中。在加入的过程中,总保持从源点 v 到 S 中各顶点的最短路径长度不大于从源点 v 到 U 中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S 中的顶点的距离就是从 v 到此顶点的最短路径长度,U 中的顶点的距离,是从 v 到此顶点只包括 S 中的顶点为中间顶点的当前最短路径长度。伪代码:
第 1 行将 d 和 π 初始化,第 2 行初始化集合 S 为空集,4 ~ 8 行每次迭代,都从 U 中选取一个点加入到 S 中,然后所有的边进行松弛操作,即每次迭代,整个图的 d 和 π 都更新一遍。过程本身很简单,下边是图示:
源点 s 是最左端顶点。最短路径估计被标记在顶点内,阴影覆盖的边指出了前趋的值。黑色顶点在集合 S中,而白色顶点在最小优先队列 Q = V – S 中。a) 第 4 ~ 8 行 while 循环第一次迭代前的情形。阴影覆盖的顶点具有最小的 d 值,而且在第 5 行被选为顶点 u 。b) ~ f) while 循环在第一次连续迭代后的情形。每个图中阴影覆盖的顶点被选作下一次迭代第 5 行的顶点 u。f) 图中的 d 和 π 值是最终结果。
Dijkstra算法时间主要消耗在寻找最小权值的边,和松弛所有剩余边,所以 EXTRACT-MIN(Q) 这一步,更好的方法是使用优先队列,优先队列可以用二叉堆,斐波那契堆等来实现,下面的代码,我用库自带的优先队列,经这样改造后,效率还是很可观的。
理解最短路算法,最基础,最简单,最经典的要数这个题目:HDU 2544 最短路,纯粹的算法练习题,用Dijkstra,我写了三个代码来实现。
1)邻接矩阵 + Dijkstra,最简单的方式,当然也是最好理解的方式:
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#include<climits>
using namespace std;
const int NV = 101;
const int inf = INT_MAX >> 1;
int m, n;int map[NV][NV];
int dis[NV];bool mark[NV];
int Spfa (int src, int des)
{
for (int i = 0; i <= n; i++)
{
mark[i] = false;
dis[i] = inf;
}
queue<int> Q;
mark[src] = true;
dis[src] = 0;
Q.push(src);
while (!Q.empty())
{
int first = Q.front();
Q.pop();
mark[first] = false;
for (int i = 1; i <= n; i++)
{
if (dis[first] + map[first][i] < dis[i])
{
if (!mark[i])
{
Q.push(i);
}
dis[i] = dis[first] + map[first][i];
mark[i] = true;
}
}//for
}//while
return dis[des];
}
int main()
{
while (~scanf("%d%d", &n, &