图论(6+): Dijkstra算法详解和拓展优化
先上伪码
伪码在于指出算法的思路, 下面我们用伪码来分析迪杰斯塔特拉的思路
void Dijkstra(table T)
{
vertex v, w;
for (;;)
{
v = smallest unknown distance vertex;
if (v == notavrtex)
break;
T[v].known = true;
for each w adjacent v
if (!T[w].known)
if (T[v].dist + cvw < T[w].dist)
{
T[w].dist = T[v].dist + cvw;
T[w].path = v;
}
}
}
Dijkastra算法的本质就是贪心策略的问题, 贪心算法一般的策略就是分段求解, 在当前的情况下最优。
Dijkstra算法就像是无权最短路径算法一样, 按阶段进行, 每个阶段, Dijkstra算法选择一个顶点V, 他是所有未知顶点中具有最小的dv , 同时算法声明中提到, 从v到w的道路是已知的(不存在是距离就是INF)
我们可以类比判断一下, 在无权图中, 我们使用转移方程 dw = dv + 1(因为在无权图总之我们默认每条边的权值为1). 现在在有权图中我们使用相同的逻辑 dw = dv + cvw;不过这里唯一不同的是不同边的权值不同 所以只有最小的一个顶点才是真正的确定的最短路径的点 (详见图论6的 Djkstra部分)
上面 的伪码列出了主要的算法核心, 他就是一个使用贪婪算法选取最小的确定的最短路径进行更新的for循环。 利用反证法可以证明在不存在负环的情况下, 没有边的值会变为负数, 该算法总能顺利完成。
·
·
·
优先队列和邻接链表的优化
关于他的时间复杂度取决于图的存储方式和贪心策略中数据的搜寻方式:如果使用遍历来寻找最小值这一步的时间复杂度将变成O (V), 而V次循环的结果就最终的时间复杂度将变为O(V2);
但是要注意到如果我们更新图的存储方式和贪心搜寻的方式我们将获得更好的时间复杂度:
int Dijktra(int S, int T)
{
minDist[S] = 0;
priority_queue<node>Q;
for (int i = 0; i < G[S].size(); i++)
{
int vex = G[S][i].v;
Q.push(G[S][i]);
minDist[vex] = min(G[S][i].len, minDist[vex]);
inqueue[vex] = 1;
}
while (!Q.empty())
{
node now = Q.top();
Q.pop();
inqueue[now.v] = 0;
for (int i = 0; i < G[now.v].size(); i++)
{
int vex = G[now.v][i].v;
int len = G[now.v][i].len;
if (len + minDist[now.v] < minDist[vex])
{
minDist[vex] = len + minDist[now.v]; //先更新最短路径
if (!inqueue[vex])
{ //如果没在队列,再加入队列
Q.push(G[now.v][i]);
}
}
}
}
return minDist[T];
}
分析一下上述源码:
首先确定顶点S, 然后将于S直接相连的顶点更新, 压入队列, 注意这里我们新建的是优先队列(优先的策略是边点的相对于起始点S的最短距离的大小)(注意游戏那队列插入和寻找使用的二分搜索 时间复杂度为logN, 远远优于遍历的N的时间复杂度);
然后我们依次取出队列中的第一个元素, 然后以它为基点更新与他直接相连的顶点, 如果这个顶点不在队列中, 压入队列;
重复上述步骤, 直到队列为空。
考虑到优先队列的排序的时间复杂度为logN, 共有V个顶点经历进队和出队的过程总共的时间复杂度为O((E + V) LOG V) ≈ O(E log V)
拓展: 对于负环的处理问题
直接代码理解(详见 图论6 Ford算法)
帮助
int Dijktra(int S, int T)
{
minDist[S] = 0;
cnt[S]++;//
priority_queue<node>Q;
for (int i = 0; i < G[S].size(); i++)
{
int vex = G[S][i].v;
Q.push(G[S][i]);
cnt[vex]++;//
minDist[vex] = min(G[S][i].len, minDist[vex]);
inqueue[vex] = 1;
}
while (!Q.empty())
{
node now = Q.top();
Q.pop();
inqueue[now.v] = 0;
for (int i = 0; i < G[now.v].size(); i++)
{
int vex = G[now.v][i].v;
int len = G[now.v][i].len;
if (len + minDist[now.v] < minDist[vex])
{
minDist[vex] = len + minDist[now.v]; //先更新最短路径
if (!inqueue[vex])
{ //如果没在队列,再加入队列
cnt[vex]++;//
if (cnt[vex] == n)//
return INF;//
inqueue[vex] = 1;
Q.push(G[now.v][i]);
}
}
}
}
return minDist[T];
}
我们注意到在这里仅有3处不同, 就是加了cnt数组进行每个顶点的入队次数的统计, 我们用反证法可以证明在不存在负环的情况下, 全图的任意顶点的最大更新次数时v- 1次。 证明详见(图论6 Ford算法)
所以在每次更新节点i的时候我们将cnt[i]++;
如果存在cnt[i] == V 则证明负环存在