Dijkstra 算法
迪杰斯特拉算法(Dijkstra)
是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。
是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。算法
主要特点
是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止
朴素Dijkstra算法
变量解释
变量名 | 含义 |
---|---|
int g[N][N] | g[i][j] i->j 的距离 |
int dis[N] | dis[i] 1->i 的最短距离 |
bool vis[N] | vis[i] 当前1->i 的最短距离是否确定 |
int n,m | n个点,m条边 |
算法流程
- 初始化最短距离。起点到自己的最短距离为0,到其他点的距离为
INF(0x3f3f3f3f)
- 找距离起点最近的点。在没有确定最短距离的点中,找到距离起点距离最近的一个点
- 标记。将2找到的最近的点标记为已经确定最短路径的点
- 松弛。用2找到的点,更新从起点到每一个点的最短距离,
min(dis[j], dis[t] + g[t][j])
- 重复第2步,直至所有存在最短路径的点更新完毕
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int g[N][N]; // g[i][j] i->j 的距离
int dis[N]; // dis[i] 1->i 的最短距离
bool vis[N]; // vis[i] 当前1->i 的最短距离是否确定
int n,m;
int dijkstra() {
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
for(int i = 0; i < n; i++) { // 访问 n 次
int t = -1;
for(int j = 1; j <= n; j++) { // 访问 n 个点
if(!vis[j] && (t == -1 || dis[t] > dis[j]))
t = j;
}
vis[t] = true;
for(int j = 1; j <= n; j++) //依次更新每个点所到相邻的点路径值
dis[j] = min(dis[j], dis[t] + g[t][j]);
}
//如果第n个点路径为无穷大,不存在 1->n 最短路径
if(dis[n] == 0x3f3f3f3f) return -1;
return dis[n];
}
int main() {
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for(int i = 0; i < m ; i++) {
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
g[x][y] = min(g[x][y],z); //如果发生重边的情况则保留最短的一条边
}
cout << dijkstra() << endl;;
return 0;
}
堆优化版本
分析
为什么说是用堆进行优化,因为在算法的第一步中,需要在没有确定最短路径的点中,找到从起点到该点距离最短的一个点。
如果不加优化,这一步是O(n)
的时间复杂度,但是如果使用小根堆的话,就可以优化为O(1),随后的一个pop
操作,这是O(log n)
的。整体来看,第一步可以优化为O(log n)
随后,在进行松弛的过程中,最多有m条
边,而入队操作的时间复杂度为O(log n)
,总体时间复杂度为O(m log n)
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 5e5 + 10;
int h[N], e[N], W[N], ne[N], idx;
int dist[N];
bool vis[N];
int n,m;
typedef pair<int,int> PII;
void add(int u,int v,int k) {
e[idx] = v, W[idx] = k, ne[idx] = h[u], h[u] = idx++;
}
int dijkstra() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII> > pq; // 小根堆
pq.push({0,1});
while(!pq.empty()) {
PII t = pq.top();
pq.pop();
int distance = t.first, ver = t.second;
if(vis[ver]) continue; // 排除当前节点已经找到最短路的情况
vis[ver] = true;
for(int i = h[ver]; i != -1 ; i = ne[i]) {
int v = e[i];
if(dist[v] > distance + W[i]) {
dist[v] = distance + W[i];
pq.push({dist[v],v});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
Dijkstra 算法 操作负权边的问题
因为Dijkstra算法每次都是在只在可以到达的没有确定最短路径的点中,找到一个最短的边,然后通过这个边,再去更新其他的点。
就上面这个图来说,当存在负权边时,得到的最短路径为[0,-1,3]
,但显然应该是[0,-1,1]
。
belloman-ford算法
Bellman - ford
算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。
其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果
算法执行流程
在松弛操作之前,需要对原本的数组进行拷贝,确保我们在松弛操作的时候,使用的是上一步松弛操作的结果,负责就会出现串联效应
为什么说存在负环可能不存在最短路径
因为belloman-ford算法
,求的是单源
的最短路径问题,那么对于负环的问题,只要负环不能到达终点,那么负环其实是对结果没有影响的。因为从起点到终点,已经确定了最多走k条
边
但如果负环可以到达终点,那么任由负环一直循环下去,一定会把最短路径变为 -INF
Belloman-ford算法判断负环的方式
我们一共有n个点
,那么对于一个点来说,我最多松弛n-1
次就可以找到一个最短路径,那么当我们继续第n次
松弛的时候,如果还可以进行松弛,那就说明存在负权的回路
判断路径是否可达
/2
的原因是,0x3f3f3f3f
是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]
大于某个与0x3f3f3f3f
相同数量级的数即可。
另外,根据题目的数据范围,负权边最多可以影响500 * 10000
次,但是0x3f3f3f3f
远远大于这个数,所以说以此来判断是否存在最短路
源程序
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 1e4 + 10;
int n,m,k; // n个点,m条边,k 最多经过k条边
int dist[N], backup[N]; // dist[i] 从1->i 的最短距离,backup dist的一个副本
struct edges{
int u,v,w;
}e[M];
int bellman_ford() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < k; i++) {
// 副本
memcpy(backup,dist,sizeof dist);
for(int i = 0; i < m; i++) {
int u = e[i].u, v = e[i].v, w = e[i].w;
dist[v] = min(dist[v], backup[u] + w);
}
}
if(dist[n] >= 0x3f3f3f3f / 2) return -1;
return dist[n];
}
int main() {
cin >> n >> m >> k;
for(int i = 0; i < m; i++) {
cin >> e[i].u >> e[i].v >> e[i].w;
}
int t = bellman_ford();
if(t == -1) puts("impossible");
else cout << t << endl;
return 0;
}
spfa算法
在belloman-ford算法中,对于遍历的操作其实是有些多余的。因为对于那些没有更新的边,还是进行了一次判断,也就是判断了一些0x3f3f3f3f + w
的操作,显然是没有必要的
spfa算法在这个的基础上,对于松弛操作,利用宽度优先搜索做了一次优化,确保我们每次松弛的操作不会做一些无用功
时间复杂度 :n为点数,m为边数
一般:O(m)
最坏:O(nm)
(网格图的形式时,边权为特殊数值)
#include <cstring>
#include <algorithm>
#include <queue>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N], w[N], ne[N], idx;
int dist[N];
bool vis[N];
int n,m;
void add(int u,int v,int k) {
e[idx] = v, w[idx] = k, ne[idx] = h[u], h[u] = idx++;
}
int spfa() {
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
queue<int> que;
que.push(1);
vis[1] = true;
while(!que.empty()) {
int f = que.front();
que.pop();
vis[f] = false;
for(int i = h[f]; ~i ; i = ne[i]) {
int v = e[i];
if(dist[v] > dist[f] + w[i]) {
dist[v] = dist[f] + w[i];
if(!vis[v]) {
que.push(v);
vis[v] = true;
}
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main() {
cin >> n >> m;
memset(h,-1,sizeof h);
for(int i = 0; i < m; i++) {
int a,b,c;
cin >> a >> b >> c;
add(a,b,c);
}
int t = spfa();
if(t == -1) puts("impossible");
else cout << t << endl;
return 0;
}
判断负环
在判断负环的过程中,依据抽屉原理,我们统计每个点的访问次数,对于一个点,当他被访问了 n
次的时候,也就是访问了n
个边,即n + 1
个点,这些点中肯定存在两个相同的点,所以说存在负环
判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点
返回
true
表示存在负环,false
表示不存在负环
bool spfa() {
memset(dist,0x3f,sizeof dist);
queue<int> que;
for(int i = 1; i <= n; i++) {
que.push(i);
vis[i] = true;
}
while(!que.empty()) {
int f = que.front();
que.pop();
vis[f] = false;
for(int i = h[f]; ~i ; i = ne[i]) {
int v = e[i];
if(dist[v] > dist[f] + w[i]) {
dist[v] = dist[f] + w[i];
cnt[v] = cnt[f] + 1;
if(cnt[v] >= n) return true;
if(!vis[v]) {
vis[v] = true;
que.push(v);
}
}
}
}
return false;
}
Floyd算法
f[i, j, k]
表示从``i走到j
的路径上除i
和j
点外只经过1
到k
的点的所有路径的最短距离
那么f[i, j, k] = min(f[i, j, k - 1), f[i, k, k - 1] + f[k, j, k - 1]
因此在计算第k
层的f[i, j]
的时候必须先将第k - 1层
的所有状态计算出来,所以需要把k
放在最外层
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f;
int dist[N][N];
int n,m,q;
void floyd() {
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
int main() {
cin >> n >> m >> q;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(i == j) dist[i][i] = 0;
else dist[i][j] = INF;
while(m --) {
int x,y,z;
cin >> x >> y >> z;
dist[x][y] = min(dist[x][y],z);
}
floyd();
while(q--) {
int x,y;
cin >> x >> y;
if(dist[x][y] >= INF / 2) puts("impossible");
else cout << dist[x][y] << endl;
}
return 0;
}