基本思想
- 问题:有 n 个点,m 条边的正权图有向图中,求源点 s 到终点 end 的最短路径
先给出算法流程
- 首先定义distance数组,让 distance[s] = 0 ,其余节点的值赋为正无穷。其意义是源点 s 到其他节点的初始距离(点s 自己到自己的距离 == 0)。
- 找出一个未被标记且distance最小的点 t ,然后标记点 t 。
- 遍历一遍 t 的所有出边,并distance[t] 来 松弛 其余节点。
- 重复 2、3 步,直至所有的点被标记完。
dij算法是基于贪心思想,当图不含负权边时,该图就拥有最优子结构,既“最短路的子路径还是最短路”。可以看出,在不含负权边的图中,被标记后的节点就是 distance[t] 最小值,不会再被其他节点更新。
朴素模板【Code】
朴素Dij模板题: 洛谷P3371
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int inf = 0x3f3f3f3f;//正无穷大
const int N = 1e4 + 10;//点的最大值
const int M = 5e5 + 10;//边的最大值
//数组模拟邻接表存图
int h[N],e[M],w[M],ne[M],idx = 1;
int n,m,s;
int dis[N];//distance数组
bool st[N];//标记数组
// a->b 的边权值为 c 存入邻接表
void add(int a,int b,int c){
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}
//核心模板
void dij(int s){
//第一步,给distance数组赋值
fill(dis + 1,dis + n + 1,inf);
dis[s] = 0;
//第四部,重复 n - 1 次
for(int i = 0;i < n - 1; ++ i){
//第二步,找出未标记且distance最小的节点 t ,并标记
int t = -1;
for(int j = 1;j <= n; ++ j)
if(!st[j] && (t == -1 || dis[j] < dis[t]))
t = j;
st[t] = 1;//标记操作
//第三步,遍历节点 t 的所有出边,进行“松弛”
for(int j = h[t]; j ;j = ne[j]){
int k = e[j];
dis[k] = min(dis[k],dis[t] + w[j]);//松弛操作
}
}
}
int main(){
scanf("%d%d%d",&n,&m,&s);
while(m -- ){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
dij(s);
for(int i = 1;i <= n; ++ i)
if(dis[i] == inf) printf("%d ",(int)(pow(2,31) - 1));
else printf("%d ",dis[i]);
return 0;
}
这个朴素模板的时间复杂度是O(n ^ 2),主要的瓶颈在于第二步寻找最小值的过程是O(n)的。这个寻找最小值的过程可以用二叉堆来实现,将O(n)的复杂度降低至O(logn)。最终总体实现O(mlogn)。
这里用STL的priority_queue容器来代替手写堆。
堆优化模板【Code】
堆优化Dij模板题: 洛谷P4779
#include<iostream>
#include<cstdio>
#include<cmath>
#include<queue>
using namespace std;
typedef pair<int,int> PII;
const int inf = 0x3f3f3f3f;//正无穷大
const int N = 1e5 + 10;//点的最大值
const int M = 2e5 + 10;//边的最大值
//数组模拟邻接表存图
int h[N],e[M],w[M],ne[M],idx = 1;
int n,m,s;
int dis[N];//distance数组
bool st[N];//标记数组
// a->b 的边权值为 c 存入邻接表
void add(int a,int b,int c){
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}
//核心模板
void dij(int s){
//第一步,给distance数组赋值
fill(dis + 1,dis + n + 1,inf);
dis[s] = 0;
priority_queue<PII,vector<PII>,greater<PII> > q;
q.push({0,s});//first代表距离,second代表点
while(!q.empty()){
//取出堆顶(最小值),并删除堆顶
int t = q.top().second;
q.pop();
//如果已经被标记了就跳过
if(st[t]) continue;
st[t] = 1;//标记
for(int i = h[t]; i ;i = ne[i]){
int j = e[i];
if(dis[j] > dis[t] + w[i]){
dis[j] = dis[t] + w[i];
q.push({dis[j],j});
}
}
}
}
int main(){
scanf("%d%d%d",&n,&m,&s);
while(m -- ){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
dij(s);
for(int i = 1;i <= n; ++ i)
if(dis[i] == inf) printf("%d ",(int)(pow(2,31) - 1));
else printf("%d ",dis[i]);
return 0;
}
关于Dij的部分问题
为什么不能用于含负权边的图?
Dij算法中已经被标记过的点就是最短路径点,如果拓展到负权边,则无法保证已经被标记的点是最短路径点,会破坏已经标记的点的路程不会改变的性质。
为什么不能用Dij的思想求解最长路径?
首先,如果图中含有正权环的话,是不存在最长路径的,就像图中含负权环就不存在最短路径一样。
- 那在仅含正权边的DAG(有向无环图)中呢?
Dij之所以能求解最短路径,是因为最短路径有 最优子结构 的性质,既“最短路的子路径还是最短子路”,而最长路径则不含有这种子结构,既“最长路径的子路径不一定是最长子路”。所以不行。
- 那要如何求解最长路径呢?
- 将所有边乘以 -1 并建图,用Bellman-Ford 算法 或 SPFA算法求其最短路,这个最短路的答案再乘以-1就是最长路。
- 拓扑排序 + 关键路径。
最长路模板题: 洛谷P1807
如何求最短路径中的最长边呢?
改变一下distance数组的意义即可,将意义从距离源点 s 的最短距离 改成 距离源点 s 的最短路径的最长边距离,既 if( dis[j] > max(dis[t],w[i]) ) dis[j] = max(dis[t],w[i]);。
求最长边: 洛谷P1396
除了这样做还可以用kruskal算法求其最小生成树 or 二分 + dfs。
2020.08.05
本文介绍了Dijkstra算法的基本思想,包括朴素模板和堆优化模板,并讨论了在含负权边图中为何不能使用Dijkstra算法及其解决办法。此外,还解答了如何在最短路径中求最长边的问题。

284

被折叠的 条评论
为什么被折叠?



