常见的最短路问题,一般来说可以分成两大类,第一大类是单源最短路问题,第二大类就是多源汇最短路。单源最短路,一般来说是求从一个点到其他所有点的最短距离,比方说最常见的一种题型是从一号点到n号点的一个最短路径,就是求出来从从一号点到其他所有点的最短路之后,那么从一号点到n号点的最短路也就求出来了,多源汇最短路,源点就是起点,一般来说在图论里边,源点就是起点。汇点一般来说就是终点。多源汇的最短路问题就是说可能不止一个起点,可能会有很多个询问,每个询问是任选两个点,然后求从其中一个点走到另外一个点的最短距离,起点和终点都是不确定的,这两大类里面可以再细分,首先单源最短路里面,可以再细分成两大类。第一类是所有边都是正权值的图,第二类就是存在负权边,就是说图里边某些的权重可能是负数,首先,所有边的权重都是正数,图论里边有个很经典的算法,这个算法的不同的实现方式是适用于不同场景的。首先第一个是朴素的迪杰斯拉算法,第二个是堆优化版的迪杰斯拉算法,约定n表示图里面点的数量,m表示边的数量。朴素版的狄耶斯拉算法的时间复杂度就是n平方的,堆优化版的迪杰斯拉算法的时间复杂度就是 logn的。稠密图的一般来说是指边数m和n方是一个级别的,如果m和n是一个级别的叫稀疏图,这种数据范围一般在算法题里面很明显。比方说n和m都是十万,显然就是一个稀疏图,如果n是100,m是一万,显然就是一个稠密图。可以发现朴素版的狄杰斯拉算法的时间复杂度和边数是没有关系的,所以说朴素版的狄耶斯拉算法比较适合于稠密图,用邻接矩阵来存,就是当边数比较多的时候,比方说边数和on方是一个级别的时候,那么应该用朴素版的迪杰斯拉算法,它的时间复杂度是平方的。如果说稠密图用堆优化版的迪杰斯拉算法,那么它的时间复杂度就是m log n的,m和n方是差不多一个级别的。那么它的一个时间复杂度就会变成n方log n,它的时间复杂度比朴素版的要稍微高一些。因此如果是稠密图,那么就要尽量使用朴素的迪杰斯拉算法,那反之的话,如果说是一个稀疏图,比方说m和n是一个级别,都是十万的级别。此时就不能用朴素版的迪杰斯拉了,因为n平方就是一个十的十次方,就会超过时间限制。但是可以用堆优化版的迪杰斯拉,用邻接表来存,它的时间复杂度只需要m log n,大概也就十万log十万,就可以很快的解决。该算法仍只需要考虑有向图,因为无向图是一种特殊的有向图,连两条路就行了。
另外一种图就是图里面存在一些负权边,也有两种实现方式,首先第一种就是贝尔曼ford算法,它的时间复杂度是on*m,就是点数乘边数。还有另外一个算法叫spfa,它是对贝尔曼ford算法进行了优化,它的时间复杂度基本上可以看成om。平均的时间复杂度是线性的,如果出题人比较变态的话,时间复杂度是Onm的,但是它的效率一般来说都会比贝尔曼ford的算法要高的,虽然spfa算法是贝尔曼ford算法的一个优化,但是并不是所有情况下用spfa算法都可以做,比方说举一个例子,就是如果说想求一下经过不超过k条边的最短路,就是对最短路的经过的边数做一个限制,如果要是限制经过的边数小于等于k,那么此时就只能用贝尔曼ford来做。
最后一种情况是多源汇最短路,只有一个是经典的弗洛伊德算法。然后它的时间复杂度是on的 3次方。
最短路算法在未来遇到的问题中有一个很大的特点,考察的侧重点是建图,就是如何把原问题抽象成一个最短路的问题。如何来定义点和边?使得这个问题变成一个最短路的问题,然后套用模板来做,狄杰斯拉算法基于贪心,弗洛伊德基于动态规划,贝尔曼ford算法基于离散数学的一些知识,
朴素版迪杰斯拉算法
首先迪杰斯拉算法是单源最短路,就是它求的是从一号点到其他所有点的一个最短距离。
算法步骤是这样的,首先第一步先初始化距离让dist1等于零,就是一号点到起点的距离是零,然后其他所有的点都等于正无穷。就是第一步的时候,只有起点被遍历到了,只有起点的距离是确定的,其余所有点距离都是没有确定的。
例题:Dijkstra求最短路 I
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n≤500,
1≤m≤10的5次方,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
朴素dijkstra算法
时间复杂是 O(n2+m), n表示点数,m表示边数
int g[N][N]; // 存储每条边
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否已经确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i ++ )
{
int t = -1; // 在还未确定最短路的点中,寻找距离最小的点
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 用t更新其他点的距离
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
st[t] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
堆优化版dijkstra —— 模板题 AcWing
作者:yxc
链接:https://www.acwing.com/blog/content/405/
来源:AcWing
#include<iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int h[N], e[M], ne[M], w[M], idx;//邻接表存储图
int state[N];//state 记录是否找到了源点到该节点的最短距离
int dist[N];//dist 数组保存源点到其余各个节点的距离
int n, m;//图的节点个数和边数
void add(int a, int b, int c)//插入边
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void Dijkstra()
{
memset(dist, 0x3f, sizeof(dist));//初始化dist 数组的各个元素为无穷大
dist[1] = 0;//源点到源点的距离为置为 0
for (int i = 0; i < n; i++)
{
int t = -1; //表示还没有确定
for (int j = 1; j <= n; j++)//遍历 dist 数组,找到没有确定最短路径的节点中距离源点最近的点t
{
if (!state[j] && (t == -1 || dist[j] < dist[t])) //
t = j;
}
state[t] = 1;//state[i] 置为 1。
for (int j = h[t]; j != -1; j = ne[j])//遍历 t 所有可以到达的节点 i
{
int i = e[j];
dist[i] = min(dist[i], dist[t] + w[j]);//更新 dist[j]
}
}
}
int main()
{
memset(h, -1, sizeof(h));//邻接表初始化
cin >> n >> m;
while (m--)//读入 m 条边
{
int a, b, w;
cin >> a >> b >> w;
add(a, b, w);
}
Dijkstra();
if (dist[n] != 0x3f3f3f3f)//如果dist[n]被更新了,则存在路径
cout << dist[n];
else
cout << "-1";
}
作者:Hasity
链接:https://www.acwing.com/solution/content/38318/
来源:AcWing
堆优化版的迪杰斯拉算法
如果说是一个稀疏图,比方说n是十万,那么如果两重循环,就会爆掉,然后要对它进行优化。
看一下朴素算法的时间复杂度:
for(i:1 ~ n)//n次
{
t <- 没有确定最短路径的节点中距离源点最近的点;//每次遍一遍历dist数组,n次的复杂度是O(n^2)
state[t] = 1;
更新 dist;//每次遍历一个节点的出边,n次遍历了所有节点的边,复杂度为O(e)
}
算法的主要耗时的步骤是从dist 数组中选出:没有确定最短路径的节点中距离源点最近的点 t。只是找个最小值而已,没有必要每次遍历一遍dist数组。
在一组数中每次能很快的找到最小值,很容易想到使用小根堆。可以使用库中的小根堆(推荐)或者自己编写。
例题: Dijkstra求最短路 II
给定一个 n 个点 m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n,m≤1.5×10的5次方
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 10的9次方。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>//堆的头文件
using namespace std;
typedef pair<int, int> PII;//堆里存储距离和节点编号
const int N = 1e6 + 10;
int n, m;//节点数量和边数
int h[N], w[N], e[N], ne[N], idx;//邻接表存储图
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 dijkstra()
{
memset(dist, 0x3f, sizeof dist);//距离初始化为无穷大
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;//定义小根堆,背过
heap.push({0, 1});//插入距离和节点编号
while (heap.size()) //while堆不空
{
auto t = heap.top();//取距离源点最近的点
heap.pop();
int ver = t.second, distance = t.first;//ver:节点编号,distance:源点距离ver 的距离
if (st[ver]) continue;//如果距离已经确定,则跳过该点
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])//更新ver所指向的节点距离
{
int j = e[i];
if (dist[j] > dist[ver] + w[i]) //j大于从t过来的距离
{
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j});//距离变小,则入堆
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
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);
}
cout << dijkstra() << endl;
return 0;
}
作者:Hasity
链接:https://www.acwing.com/solution/content/38323/
来源:AcWing
使用小根堆后,找到 t 的耗时从 O(n^2) 将为了 O(1)。每次更新 dist 后,需要向堆中插入更新的信息。所以更新dist的时间复杂度有 O(e) 变为了 O(e*logn)。总时间复杂度有 O(n^2) 变为了 O(n + e*longn)。适用于稀疏图。
总结
迪杰斯特拉算法适用于求正权有向图中,源点到其余各个节点的最短路径。注意:图中可以有环,但不能有负权边。
例如:如下图就不能使用迪杰斯特拉算法求节点 1 到其余各个节点的最短距离。
{:weith=150 height=150}
bellman-ford
贝尔曼ford算法的存储方式不一定需要写成邻接表,可以用最傻瓜式的算术方式,就开个结构体。
求最短路的时候,如果有负权回路的话,那么最短路是不一定存在的,如果能求出来最短路,这个图里边是没有负权回路的。比方说想去某地旅游,比方说想从一号城市到n号城市旅游,但是这个飞机的航线没有从一到n的直飞的,但是可以中转,可以从一号城市先到二号城市,然后从二号城市到三号城市,然后再从三号城市到n号城市,就可以这样周转一下,然后每次周转,都有一个价钱。然后想求一下,从一号点到n号点最少需要多少钱,但是,每换乘一次,旅客的心情就会变差一些。所以可能会对做一个限制,就是最多只能换k次。所以这个问题其实是有实际含义的。
例题:有边数限制的最短路
给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1号点走到 n号点,输出 impossible
。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
点的编号为 1∼n。
输出格式
输出一个整数,表示从 1 号点到 n号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出 impossible
。
数据范围
1≤n,k≤500,
1≤m≤10000
1≤x,y≤n
任意边长的绝对值不超过 10000。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
#include<iostream>
#include<cstring>
using namespace std;
const int N = 510, M = 10010;
struct Edge { //使用结构体存储边,不用定义一大堆数组去加边
int a; //起点
int b; //终点
int w; //权重
} e[M];//把每个边保存下来即可
int dist[N]; //距离
int back[N];//备份数组防止串联
int n, m, k;//k代表最短路径最多包涵k条边
void bellman_ford() {
memset(dist, 0x3f, sizeof dist); //dist初始化位无穷,八字节3f3f3f3f大于1e9
dist[1] = 0;
for (int i = 0; i < k; i++) {//k次循环
memcpy(back, dist, sizeof dist);//备份,存一下上一次迭代的结果
for (int j = 0; j < m; j++) {//遍历所有边,而dijkstra是遍历所有顶点n*n
int a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], back[a] + w);
//使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
}
}
}
int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i++) { //读入m条边
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
e[i] = {a, b, w};
}
bellman_ford();
if(dist[n]>0x3f3f3f3f/2) puts("impossible");
else printf("%d",dist[n]);
return 0;
}
作者:松鼠爱葡萄
链接:https://www.acwing.com/solution/content/14088/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
SPFA求最短路
spfa算法是对贝尔曼ford算法做一个优化。
spfa算法文字说明:
建立一个队列,初始时队列里只有起始点。
再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。
再建立一个数组,标记点是否在队列中。
队头不断出队,计算始点起点经过队头到其他点的距离是否变短,如果变短且被点不在队列中,则把该点加入到队尾。
重复执行直到队列为空。
在保存最短路径的数组中,就得到了最短路径。
例题: spfa求最短路
给定一个 n个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible
。
数据保证不存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y的有向边,边长为 z。
输出格式
输出一个整数,表示 1号点到 n 号点的最短距离。
如果路径不存在,则输出 impossible
。
数据范围
1≤n,m≤10的5次方
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100010;
int h[N], e[N], w[N], ne[N], idx;//邻接表,存储图
int st[N];//标记顶点是不是在队列中
int dist[N];//保存最短路径的值
int q[N], hh, tt = -1;//队列
void add(int a, int b, int c){//图中添加边和边的端点
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa(){
q[++tt] = 1;//从1号顶点开始松弛,1号顶点入队
dist[1] = 0;//1号到1号的距离为 0
st[1] = 1;//1号顶点在队列中,防止存储重复的点
while(tt >= hh){//不断进行松弛
int a = q[hh++];//取对头记作a,进行松弛
st[a] = 0;//取完队头后,a不在队列中了
for(int i = h[a]; i != -1; i = ne[i])//更新a的所有邻边,遍历所有和a相连的点
{
int b = e[i], c = w[i];//获得和a相连的点和边
if(dist[b] > dist[a] + c){//如果可以距离变得更短,则更新距离
dist[b] = dist[a] + c;//更新距离
if(!st[b]){//如果没在队列中
q[++tt] = b;//入队
st[b] = 1;//打标记
}
}
}
}
}
int main(){
memset(h, -1, sizeof h);//初始化邻接表
memset(dist, 0x3f, sizeof dist);//初始化距离
int n, m;//保存点的数量和边的数量
cin >> n >> m;
for(int i = 0; i < m; i++){//读入每条边和边的端点
int a, b, w;
cin >> a >> b >> w;
add(a, b, w);//加入到邻接表
}
spfa();
if(dist[n] == 0x3f3f3f3f )//如果到n点的距离是无穷,则不能到达
cout << "impossible";
else cout << dist[n];//否则能到达,输出距离
return 0;
}
作者:Hasity
链接:https://www.acwing.com/solution/content/105508/
来源:AcWing
SPFA判断负环
例题:spfa判断负环
给定一个 n 个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
输出格式
如果图中存在负权回路,则输出 Yes,否则输出 No。
数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int N = 2e3 + 10, M = 1e4 + 10;
int n, m;
int head[N], e[M], ne[M], w[M], idx;
bool st[N];
int dist[N];
int cnt[N]; //cnt[x] 表示 当前从1-x的最短路的边数
void add(int a, int b, int c)
{
e[idx] = b;
ne[idx] = head[a];
w[idx] = c;
head[a] = idx++;
}
bool spfa(){
// 这里不需要初始化dist数组为 正无穷/初始化的原因是, 如果存在负环, 那么dist不管初始化为多少, 都会被更新
queue<int> q;
//不仅仅是1了, 因为点1可能到不了有负环的点, 因此把所有点都加入队列
for(int i=1;i<=n;i++){
q.push(i);
st[i]=true;
}
while(q.size()){
int t = q.front();
q.pop();
st[t]=false;
for(int i = head[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;
if(cnt[j]>=n){
return true;
}
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
return false;
}
int main()
{
cin >> n >> m;
memset(head, -1, sizeof head);
for (int i = 0; i < m; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
if (spfa()) {
cout << "Yes" << endl;
}
else {
cout << "No" << endl;
}
return 0;
}
作者:Bug-Free
链接:https://www.acwing.com/solution/content/42308/
来源:AcWing
Floyd求最短路
例题: Floyd求最短路
给定一个 n个点 m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k个询问,每个询问包含两个整数 x 和 y,表示查询从点 x到点 y的最短距离,如果路径不存在,则输出 impossible
。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m行,每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y表示询问点 x到点 y的最短距离。
输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible
。
数据范围
1≤n≤200
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, Q; //Q为询问
int d[N][N]; //d是邻接矩阵,要处理的矩阵
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];
//由于有负权边存在所以约大过INF/2也很合理
if (t > INF / 2) puts("impossible");
else printf("%d\n", t);
}
return 0;
}
作者:半瓶可乐
链接:https://www.acwing.com/solution/content/92654/
来源:AcWing
end
———————————————————————————————————————————