目录
今天也是为了cc,努力奋斗的一天ヾ(≧▽≦*)o
1. 引言
什么是最短路径问题(What)
对于任意给出的图G(V,E)
和起点S
、终点T
,求从S
到T
的权值最小的路径。
解决最短路径问题的方法有哪些(How)
- Dijkstra算法——求解单源最短路径,而且不能带负数的权!!!
- Bellman-Ford算法
- SPFA算法
- Floyd算法
- 对于边没有权重的情况下,是可以使用BFS和DFS来求单源最短路径的!
2. Dijkstra算法
概述
- 解决 单源最短路径 问题:求一个顶点到图中所有顶点的最短路径;
- 保研机试经常考滴哦
基本思想
我觉得《算法笔记》上讲的太好了,哭了,那个数码宝贝,👴的最爱呀。下面的概念
有点(非常)苦涩.
设置 集合S
存放已被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数):
- 每次从集合
V-S
(即未被访问的顶点)中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S
(即代表已经被访问); - 之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点v之间的最短距离。
具体实现
Dijkstra算法的具体实现:
由于Dijkstra算法的策略比较偏重于理论化,因此为了方便编码,需要想办法来实现策略中较为关键的东西,即集合S
的实现、起点s
到达顶点
V
i
(
0
≤
i
≤
n
−
1
)
V_i(0≤i≤n-1)
Vi(0≤i≤n−1)的最短距离的实现。
- 集合
S
可以使用一个bool型数组vis[]
来实现,即当vis[i] == true
时表示顶点 V i V_i Vi已被访问,当vis[i] == false
时表示顶点 V i V_i Vi未被访问。 - 令int型数组
d[]
表示起点s到达顶点 V i V_i Vi的最短距离,初始时除了起点s的d[s]
赋值为0,其余顶点都赋值为一个很大的数(初学者可以使用1000000000,即 1 0 9 10^9 109;懂二进制的童鞋使用0x3fffffff
)来表示INF
,即不可达。注意:不要写成0x7fffffff
,这样做加法会溢出的。
伪代码
接下来看看实现Dijkstra算法的伪代码。
//G为图,一般设置为全局变量;数组d为源点到达各点的最短路径长度,s为起点
Dijkstra(G,d[],s){
初始化;
for(循环n次){ //每一次都会找到s到达一个顶点的最短路径
u = 使d[u]最小的还未被访问的顶点的标号;
记u已被访问;
for(从u出发能到达的所有顶点v){
if(v未被访问&&以u为中介点能使s到顶点v的最短距离d[v]最优){
优化d[v];
}
}
}
}
具体实现代码
常量定义
const int MAXV = 1000; //设置最大顶点数
const int INF = 1000000000; //设置INF为一个很大的数
邻接矩阵版
//邻接矩阵版
//适用于点数不大(例如V不超过1000)的情况,相对好写。代码如下:
int n,G[MAXV][MAXV]; //n为顶点数,MAXV为最大顶点数
int d[MAXV]; //起点到达各点的最短路径长度
bool vis[MAXV] = {false}; //标记数组,vis[i] == true表示已访问。初值均为false
void Dijkstra(int s){ //s为起点
fill(d,d+MAXV,INF); //fill函数将整个d数组赋值为INF(慎用memset)
d[s] = 0; //起点s到达自身的距离为0
for(int i=0;i<n;i++){ //循环n次
int u = -1,MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j=0;j<n;j++){ //找到未访问的顶点中d[]最小的
if(vis[j] == false && 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[u] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v]; //优化d[v]
}
}
}
}
从复杂度来看,主要是外层循环 O ( V ) O(V) O(V)(V就是顶点个数n)与内层循环(寻找最小的d[u]需要 O ( V ) O(V) O(V)、枚举v需要 O ( V ) O(V) O(V)产生),总复杂度为 O ( V ∗ ( V + V ) ) = O ( V 2 ) O(V * (V + V)) = O(V^2) O(V∗(V+V))=O(V2).
邻接表版
//邻接表版
struct Node{
int v,dis; //v为边的目标顶点,dis为边权
};
vector<Node> Adj[MAXV]; //图G,Adj[u]存放从顶点u出发可以到达的所有顶点
int n; //n为顶点数,图G使用邻接表实现,MAXV为最大顶点数
int d[MAXV]; //起点到达各点的最短路径长度
bool vis[MAXV] = {false}; //标记数组,vis[i]==true表示已访问,初值均为false
void Dijkstra(int s){ //s为起点
fill(d,d+MAXV,INF); //fill函数将整个d数组赋值为INF(慎用memset)
d[s] = 0; //起点s到达自身的距离为0
for(int i=0;i<n;i++){
int u=-1,MIN=INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j=0;j<n;j++){ //找到未访问的顶点中d[]最小的
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return;
vis[u] = true; //标记u为已访问
//只有下面这个for与邻接矩阵的写法不同
for(int j=0;j<Adj[u].size();j++){
int v = Adj[u][j].v; //通过邻接表直接获得u能够到达的顶点v
if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
//如果v未访问 && 以u为中介点可以使d[v]更优
d[v] = d[u] + Adj[u][j].dis; //优化d[v]
}
}
}
}
从复杂度来看,主要是外层循环O(V)与内层循环(寻找最小的d[u]
需要
O
(
V
)
O(V)
O(V)、枚举v
需要O
(
a
d
j
[
u
]
.
s
i
z
e
)
(adj[u].size)
(adj[u].size))产生的。又由于对整个程序来说,枚举v的次数总共为O(
∑
u
=
0
n
−
1
a
d
j
[
u
]
.
s
i
z
e
=
O
(
E
)
\sum_{u=0}^{n-1}adj[u].size=O(E)
∑u=0n−1adj[u].size=O(E)),因此复杂度为
O
(
V
2
+
E
)
O(V^{2}+E)
O(V2+E)。
改进
上述的两种方法的复杂度都达到了
O
(
V
2
)
O(V^2)
O(V2) 级别,但是可以通过堆优化来降低寻找最小d[u]
的过程,使其时间复杂度小于O(V)。
最简单的写法就是直接使用STL中的优先队列 priority_queue
,这样使用邻接表实现的Dijkstra算法的时间复杂度可以降为
O
(
V
l
o
g
V
+
E
)
O(VlogV + E)
O(VlogV+E)。
此外, Dijkstra算法只能应对所有边权都是非负数的情况,如果边权出现负数,那么Dijkstra算法会出错,这时最好使用SPFA算法
- 如果是无向图的怎么办呢?
直接把无向改成两个方向上的有向即可。 - 如何存储最短路径(而非仅仅是最短路径长度)?
在优化d[u]
中使用一个pre[u]
数组来存储当前节点的前驱节点。然后使用DFS
得到最优的路径。 - 如果最优路径不仅仅只有一条呢?
那么一般会出现三种情况来让你对不止一条最优路径进行判断:
① 新增边权;
② 新增点权;
③ 求最短路径条数。
以上三步也都是在优化d[u]
中进行操作。
3. 题型训练
- 最短路径——浙江大学
- 上交有一道题目不会做。。。