最短路——Dijkstra(朴素法、堆优化)、Bellman-Ford、SPFA(求负环)、Floyd算法
处理负权边我们是采取Bellman-Ford算法或者是SPFA,SPFA算法的适用处更广,但是当有边数权限(在多少步下完成)的时候,我们只能适用Bellman-Ford算法,但在其他情况下我们都可以使用SPFA算法;我们上面知道Bellman_Ford算法会遍历每条边,但是有些边遍历是没有意义的,我们只需要遍历那些到源点距离变小的点才能找到最短路径;SPFA中松弛的概念:一个点到另一个点的路径选择
最短路问题是图论中一个很重要的问题

朴素Dijkstra(迪杰斯特拉)算法求最短路
迪杰斯特拉采取的是贪心算法,将所有顶点分为已标记(下图中绿色打钩的)和未标记的两个集合。从起点开始,不断在未标记点中寻找距离起始点路径最短的顶点,并将其标记(一次找一个),直至所有顶点都被标记为止。
注意1:迪杰斯特拉算法不可以处理存在负权边的图。
注意2:迪杰斯特拉算法我们是用邻接矩阵存储图的

例题:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int n, m;
int g[N][N];//邻接矩阵存储图
int dist[N];//记录每一个点到起始点的距离
bool state[N];//记录该点的最短路径是否已经确认
int Dijkstra()
{
memset(dist, 0x3f, sizeof dist);//把距离都初始化为无限大
dist[1] = 0;//第一个点到自身距离为0
for(int i = 0; i < n; i++)//n个点要迭代n次
{
int t = -1;//t表示当前访问的点,之所以设置-1是因为没有负权边
for(int j = 1; j <= n; j++)//j表示从1号点开始
{
//该步骤说明还未确定最短路的点
if(!state[j] && (t == -1 || dist[t] > dist[j]))
t = j;
}
state[t] = true;
for(int j = 1; j <= n; j++)//依次更新每个点所到相邻点路径值
{
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
if(dist[n] == 0x3f3f3f3f) return -1;//第n个点路径值为无穷大,说明不存在最小路径
return dist[n];
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);//初始化邻接矩阵,把每个点到原点的距离都初始化为∞,即每个点都不是通的
while(m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c);//如果有重边,则保留最短的那一条边
}
cout << Dijkstra() << endl;
return 0;
}
堆优化Dijkstra算法
在朴素的Dijkstra算法中,我们每次都需要用for循环先找到距离最短的点,时间复杂度是O(n^2);堆优化Dijkstra算法就是在这里进行优化,利用堆(最小堆)可以直接找到距离最短的点;最小堆我们可以手写一个堆,当然我们可以使用优先队列更快的完成;我们要注意堆优化Dijkstra是应用于稀疏图,稀疏图我们采取的是邻接表来存储边的。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 150010;
int n, m;
int h[N], e[N], ne[N], idx;//邻接表
int w[N];//权重
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;//优先队列定义小根堆
//heap中存的是pair,first是距离,second是哪一个点
heap.push({0, 1});
while(heap.size())
{
PII t = heap.top();//取出队头元素
heap.pop();
int distance = t.first, ver = t.second;
if(st[ver]) continue;//如果这个点已经确定最短距离了,我们就不需要进行下面了
st[ver] = true;
for(int i = h[ver]; i != -1; i = ne[i])//遍历邻接表
{
int j = e[i];
if(dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
memset(h, -1, sizeof h);//初始化邻接表
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i++)//读取边
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);//不需要担心有重边,最后都会放进堆里,但是弹出的都是最小的
}
cout << dijkstra() << endl;
return 0;
}

Bellman-Ford算法

Dijkstra算法是无法处理存在负权边的;处理负权边我们是采取Bellman-Ford算法或者是SPFA,SPFA算法的适用处更广,但是当有边数权限(在多少步下完成)的时候,我们只能适用Bellman-Ford算法,但在其他情况下我们都可以使用SPFA算法;
Bellman-Ford算法基本原理:
连续进行松弛操作,在每次松弛时把每条边都更新一下,若在n-1次(n-1条边有n个点)松弛后还能更新,则说明图中存在负环,不能计算最短路径
Bellman-Ford的算法效率比较低;其基本步骤是:
for n次
for所有边 a,b,w(松弛操作)
dist[b] = min(dist[b], back[a] + w)
其中back数组是上一次迭代后dist数组的备份;因为是每个点同时向外出发,因此要对dist数组进行备份,如果不进行备份,会发生串联效应,影响下一个点
Bellman-Ford算法存边可以随便存,一般使用结构体来存储边
Bellman_Ford算法是遍历边的

#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 10010;
struct Edge
{
int a;
int b;
int w;
}e[M];//把所有边保存起来
int n, m, k;
int dist[N];
int back[N];//备份数组
int Bellman_Ford()
{
memset(dist, 0x3f, sizeof dist);//初始化
dist[1] = 0;
for(int i = 0; i < k; i++)//k次循环
{
memcpy(back, dist, sizeof dist);
for(int j = 0; j < m; j++)//遍历所有边
{
int a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], back[a] + w);
}
}
if(dist[n] > 0x3f3f3f3f / 2) return 0x3f3f3f3f;
else return dist[n];
}
int main()
{
cin >> n >> m >> k;
for(int i = 0; i < m; i++)
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
e[i] = {a, b, w};
}
int res = Bellman_Ford();
if(res == 0x3f3f3f3f) puts("impossible");
else cout << res << endl;
return 0;
}
SPFA算法
SPFA算法就是对Bellman_Ford算法进行优化的;我们上面知道Bellman_Ford算法会遍历每条边,但是有些边遍历是没有意义的,我们只需要遍历那些到源点距离变小的点才能找到最短路径 ;
SPFA中松弛的概念:一个点到另一个点的路径选择有很多,我们要想找到最优路径,我们肯定要找到中间那个点距离源点距离变小的点,那样这两点之间的路径才会变小

#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int h[N], e[N], ne[N], idx, w[N];//邻接表存边
int dist[N];//保存最短路径的值
bool st[N];//标记点是否在队列中
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist);//初始化距离
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;//1入队
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;//t出队了
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if(!st[j])//j不在队列中,入队
{
q.push(j);
st[j] = true;//标记一下,j已入队
}
}
}
}
return dist[n];
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);//邻接表初始化
while(m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t = spfa();
if(t == 0x3f3f3f3f) puts("impossible");
else cout << t << endl;
return 0;
}
SPFA算法求负环
在解决是否存在负权回路的问题上,我们一般采取的算法是SPFA算法;
原理:从1 ~ n 点的距离,最多会有 n-1 条边,如果出现 >= n条边的话,说明出现了负环(抽屉原理)
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], e[N], ne[N], idx, w[N];//邻接表
int dist[N], cnt[N];//cnt数组记录到起点的边数和
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
bool spfa()
{
queue<int> q;
for(int i = 1; i <= n; i++)
{
st[i] = true;
q.push(i);
}
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
//如果超过了n+1次,根据抽屉原理,说明经过某个节点两次,说明有负权回路
if(cnt[j] >= n) return true;
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while(m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
if(spfa()) puts("Yes");
else puts("No");
return 0;
}
Floyd(弗洛伊德)算法
Floyd算法的本质是动态规划;是解决多源汇最短路径的主要方法
Floyd算法的思想很简单,就是三层循环,

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;//INF相当于无穷大--0x3f3f3f3f
int n, m, Q;//Q个询问
int d[N][N];//邻接矩阵
void floyd()
{
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main()
{
scanf("%d%d%d", &n, &m, &Q);
//邻接矩阵初始化
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(i == j) d[i][j] = 0;
else d[i][j] = INF;
while(m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
d[a][b] = min(d[a][b], c);
}
floyd();
while(Q--)
{
int a, b;
scanf("%d%d", &a, &b);
int t = d[a][b];
if(t > INF / 2) puts("impossible");//因为可能存在负权路径
else cout << t << endl;
}
return 0;
}
更多推荐

所有评论(0)