参考书籍:《算法笔记》
最短路径问题介绍:
给定图G(V,E),求一条从起点到终点的路径,使这条路径上经过的所有边的权值最小。
Dijkstra算法:
1、主要特点:
Dijkstra算法使用了广度优先搜索解决带权有向图或者无向图的单源最短路径问题,即给定如G和起点s,通过算法求得s到其他每个顶点的最短距离。算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。
注意: 不适用于带负权值的图。
2、算法的思路:
Dijkstra算法采用的是一种贪心的策略,设置了:
- 集合
s
(可用数组s[ ]或者bool vis[]
)记录已经求得最短路径的顶点。初始化为0,s[i] == 1
或者vis[i] == true
表示已经访问过结点i; - 数组
dist[]
保存源点v0到其他各个顶点的当前最短路径长度,除了dist[0] =0,其他点dist[i]的初值为无穷INF(可以设置为1e9
或者0x3f3f3f
); - 数组
pre[]
,pre[i]
表示 从源点0到顶点i之间的最短路径的前驱结点,在算法结束时,可以得到从源点0到顶点i的最短路径;
3、Dijkstra算法的伪代码:
具体的做法有邻接矩阵和邻接表两种写法。区别在于获得u能到达的顶点v,邻接矩阵需要枚举,而邻接表可以直接获得。
//G为图,一般设置为全局变量,数组d为源点到各个顶点的最短路径长度,start为起点
Dijkstra(G, d[], start){
初始化;
for(循环n次){
u = d[u]取到最小值时候的u;
标记u已被访问;
for(遍历从u出发能到达的顶点v){
if(v未被访问过 && 从u出发到达v的距离比d[v]更小){
优化d[v];
令v的前驱为u;
}
}
}
}
例如:从顶点0出发(即v0 = 0),集合s最初只包含顶点0。邻接矩阵arcs
表示带权有向图,arcs[i][j]
代表有向边<i,j>的权值,若不存在边则arcs[i][j]
为∞,Dijkstra算法的步骤如下(不考虑对path[]的操作):
- 初始化:集合s初始化为{0},dist[ ]的初始值
dist[i] = arcs[0][i]
, i = 1~n-1; - 从
dist[ ]
中选择最小值dist[j]
,该值就是源点0到顶点j的最短路径,并且把该点加入到pre[ ]
中,OK,此时完成一个顶点; - 看看新加入的顶点j是否可以到达其他顶点并且看看通过j到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在
dist[ ]
中的值。 - 又从
dist[ ]
中找出最小值,重复上述动作,直到集合s中包含了图的所有顶点。
手工计算Dijkstra:
Dijkstra的代码实现:
邻接矩阵做法:
const int INF = 0x3f3f3f;
const int maxn = 1000 + 10;
int n, g[maxn][maxn]; //n为顶点
int d[maxn]; //顶点到其他点的最短路径长度
int pre[maxn]; //pre[v]表示从起点到顶点v的最短路径上v的前一个顶点
bool vis[maxn] = {0}; //对应结点有没有访问过
void dijkstra(int s){
fill(d, d + maxn, INF);
for(int i = 0; i < n; i ++ ) pre[i] = i; //初始化设置前驱结点为自身
d[0] = 0;
for(int i = 0; i < n; i ++ ){ //循环n次
int u = -1, MIN = INF; //找到d[u]最小时候的u,MIN存放最小的d[u]
for(int j = 0; j < n; j ++ ){
if(!vis[j] && d[j] < MIN){ //第一次只有源点满足
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u], 说明剩下的点和s都不连通
if(u == -1) return;
vis[u] = true; //标记u已经被访问
for(int v = 0; v < n; v ++ ){
//如果v未被访问 && u能到达v && 以u为中介可以使d[v]更优
if(!vis[v] && g[u][v] != INF && d[u] + g[u][v] < d[v]){
d[v] = d[u] + g[u][v]; //优化d[v]
pre[v] = u; //记录u是v的前驱
}
}
}
}
//递归输出路径
void dfs(int s, int v){ //s起点 v为当前访问的顶点编号
if(v == s){ //如果已经到达起点,就输出起点并返回
printf("%d ", s);
return;
}
dfs(s, pre[v]); //递归到v的前驱结点
printf("%d ", v); //从最深处return回来后输出每一层的顶点号
}
基础上的扩充:
在最短路径有多条的情况上加上第二个衡量的尺度:
- 给每条边再加一个边权(例如花费),然后要求在最短路径不止一条时,输出花费之和最小(最大)的路径;
- 给每个点增加一个点权(例如到一个城市后可以获得的物资),然后要求在最短路径不止一条时,输出能获得最大(最小)物资的路径;
- 直接问有多少条最短路径;
根据情况修改代码,都是在算法的优化d[v]那个步骤进行修改,其他地方不需要改动。以下是三种解决方法:
1、新增边权
以花费作为新增的边权为例,用cost[u][v]表示u到v的花费,并增加一个数组c[],从起点s出发到达顶点u的最小花费为c[u],初始化时c[s]为0,其余都是INF。
在d[u] + g[u][v] < d[v] (即s到v的距离d[v]能更优)时更新d[v]和c[v],而当d[u] + g[u][v] == d[v](即最短距离相同)且c[u] + cost[u][v] < c[v] (可以使s到v的最小花费更优)时更新c[v]:
for(int v = 0; v < n; v ++ ){
//如果v未被访问 && u能到达v
if(!vis[v] && g[u][v] != INF ){
if(d[u] + g[u][v] < d[v]){ //以u为中介可以使d[v]更优
d[v] = d[u] + g[u][v]; //优化d[v]
c[v] = c[u] + cost[u][v];
}
else if(d[u] + g[u][v] == d[v] && c[u] +cost[u][v] < c[v]){
c[v] = c[u] + cost[u][v]; //最短距离相同时看能否让c[v]更优
}
}
}
2、新增点权
其实和新增边权一个样。
以到每个点能获得对应物资作为新增的点权为例,用weight[u]表示到点u能获得的物资,并增加一个数组w[],从起点s出发到达顶点u的获得的最大物资为w[u], 初始化时w[s]为0,其余都是INF。
在d[u] + g[u][v] < d[v] (即s到v的距离d[v]能更优)时更新d[v]和w[v],而当d[u] + g[u][v] == d[v](即最短距离相同)且w[u] + weight[u][v] > w[v] (可以使s到v的获得物资更多)时更新w[v]:
for(int v = 0; v < n; v ++ ){
//如果v未被访问 && u能到达v
if(!vis[v] && g[u][v] != INF ){
if(d[u] + g[u][v] < d[v]){ //以u为中介可以使d[v]更优
d[v] = d[u] + g[u][v]; //优化d[v]
w[v] = w[u] + weight[v];
}
else if(d[u] + g[u][v] == d[v] && w[u] +weight[v] > w[v]){
w[v] = w[u] + weight[v]; //最短距离相同时看能否让c[v]更优
}
}
}
3、求最短路径条数
只需要增加一个数组num[],令从起点s到达顶点u的最短路径条数为num[u],初始化num[s]为1,其余为0。
在d[u] + g[u][v] < d[v]时(即能使s到v的最短距离d[v]更优),更新d[v],并让num[v]继承num[u];
当d[u] + g[u][v] == d[v]时(即最短距离相同)将num[u]加到num[v]中:
for(int v = 0; v < n; v ++ ){
//如果v未被访问 && u能到达v
if(!vis[v] && g[u][v] != INF ){
if(d[u] + g[u][v] < d[v]){ //以u为中介可以使d[v]更优
d[v] = d[u] + g[u][v]; //优化d[v]
num[v] = num[u];
}
else if(d[u] + g[u][v] == d[v]){
num[v] += num[u]; //最短距离相同时累加num
}
}
}
Dijkstra在PAT中的题目:
PAT 1003
PAT 1030
PAT 1087 All Roads Lead to Rome
PAT 1163