Dijkstra
Dijkstra(/ˈdikstrɑ/或/ˈdɛikstrɑ/)算法由荷兰计算机科学家 E. W. Dijkstra 于 1956 年发现,1959 年公开发表.是一种求解非负权图1上单源最短路径2的算法.
引入——无权图上的BFS
BFS(Breadth-First Search:广度优先搜索)
explores equally in all directions
在求解无权图上的单源最短路问题时,广搜通过一层层地向外拓展来得到源点到每个点的最短路径.
这种方法的正确性是显然的(这么说其实是因为我不会严格的证明…).
设
d
i
s
i
dis_i
disi表示从源点
s
s
s到
i
i
i点的距离.
我们把每条无权边的长度看作是1,那么一开始就可以确定从源点到源点的最短路是0(即
d
i
s
s
=
0
dis_s=0
diss=0),接下来可以确定的是源点的邻居(假设是点
i
i
i),到源点的距离是1(即
d
i
s
i
=
d
i
s
s
+
1
=
1
dis_i=dis_s+1=1
disi=diss+1=1).接下来,可以由
i
i
i点找他们的邻居(已经找过的点就不找了,比如不用找
i
i
i的邻居
s
s
s),并确定他们到源点的最短距离是2.再接着找他们的邻居到源点的最短距离是3…
就这样通过离源点最近的点层层拓展,bfs能够访问到所有与源点连通的点并计算出距离.
实现
用队列queue
一些前置芝士
当无权图变成了非负权图,BFS失去了它的正确性,这时就要用到Dijkstra.
(别紧张,先来一些前置芝士)
边松弛
边松弛操作是广泛用于最短路算法的操作(我知道的最短路算法都离不开边松弛操作)
代码表述为
//dis[i]是源点到i的距离
//w(from,to)是连接from和to的边的边权
if(dis[to]>dis[from]+w(from,to))
dis[to]=dis[from]+w(from,to);
文字表述为
对于一个节点from,若从源点经过from走到to的距离能比dis[to]的值更小
就用dis[from]+w(from,to)去更新(松弛)dis[to]
(可以理解为以from为捷径去更新to)
贪心
求解某些问题时,只需要做出在当前看来是最好的选择就能获得最好的结果,而不需要考虑整体上的最优,即使目光短浅也是没有关系的——Luogu
(好正式,好难懂)
举个栗子——找零: 为了找的钱张数最小,我们总是找不大于剩余金额且面额最大的钱.
为何Dijkstra中会涉及到贪心?因为我们需要考虑选择哪个节点作为from节点来更新 d i s [ t o ] dis[to] dis[to](在BFS中,我们总是选择内层的点去更新外层的点).所以我们需要有一种贪心策略来找from并保证算法的正确性.
Dijkstra
了解了上述知识后,Dijkstra算法可以概括为贪心地进行边松弛操作.
其贪心策略是:每次选离源点最近且还没作过为from的点进行拓展. 而且一旦一个点被选定作为from(对应课本上的永久标记),就表示它到起点的最短距离已经确定了,不会再更改.
(看不懂没关系,可以看看具体是怎么跑的)
图中圆圈里的数字代表到源点的最短路径的长度,边上的数字表示边权,加粗的线表示最短路径.
其实整个过程跟无权图上的BFS很相似:
当我们在无权图上跑 Dijkstra,可以发现这简直就是BFS.因为在无权图里先访问到的点到源点的距离天然地不小于后访问到的点,所以可以用队列找from(FIFO).采取Dijkstra的贪心策略能达到同样的效果.BFS向外拓展的过程也可看成时边松弛(此时每条边的边权都看作1),不访问访问过的点从边松弛的角度看是为了满足边松弛的条件(原因往上看几句).
非负权图?
Dijkstra中的贪心策略决定了Dijkstra只能在非负权图上跑.
当我们选中了一个符合条件的点作为from,那么在非负权图中,不可能找到一条路径能够松弛from到源点的距离.但是负权的存在让松弛有了可能.
代码实现
其实要做的事情就几件
//初始化
Init()
//进入循环体,满足一定条件后就跳出
Loop()
{
//找from
GreedyFind();
//边松弛
Relaxation();
}
具体地
朴素Dijkstra
(邻接矩阵存图,遍历地找from)
// |V|=n
// 邻接矩阵存图
// g[i][j]表示从i到j有一条权为g[i][j]的边
// 若g[i][j]=Inf,则i和j间无边
void Dijkstra(int st)
{
//初始化
int dis[n+5];
for(int i=0;i<=n;i++) dis[i]=Inf;
dis[st]=0;
//是否作为过from / 是否已经确定最短路
bool vis[n+5];
//每次都确定一个点的最短路,共要n次
for(int t=1;t<=n;t++)
{
//贪心地找from(这里我习惯写成cur)
int cur=0;
for(int i=1;i<=n;i++)
if(dis[i]<dis[cur]&&vis[i]==0)// 边松弛的条件
cur=i;
vis[cur]=1;
//边松弛
for(int i=1;i<n;i++)
if(g[cur][i]!=Inf)// 有边
if(dis[i]>dis[cur]+g[cur][i])// 满足松弛条件
dis[i]=dis[cur]+g[cur][i];
}
}
BFS(关于BFS,懒得写)
堆优化的Dijkstra(更适合cpp体质的代码)
(邻接表存图,优先队列中取from)
void Dijkstra(int st)
{
vector<int> dis(n+5,Inf);
vector<bool> vis(n+5,0);
priority_queue<pair<int,int>> q;
q.push(0,st);
while(!q.empty())
{
auto [dist,cur]=q.top();
q.pop();
if(vis[cur]) continue;
vis[cur]=1;
dist=-dist;
for(auto [to,val]:g[cur])
{
if(dis[to]>dist+val)
{
dis[to]=dist+val;
q.push({-dis[to],to});
}
}
}
}