源点:起点。汇点:终点。
m~n2:稠密图(用邻接矩阵来存)。m~n:稀疏图(用邻接表来存)。
该问题核心是建图,如何定义边与点,如何将复杂问题抽象成最短路问题。
一、单源最短路
一般是求一个点到其他所有点的最短距离。
1. 所有边都是正权值的图
时间按复杂度m如果与n2的数量级相近,则选用朴素的Dijkstra算法处理稠密图(边较多);反之当m数量级较小则选用堆优化版处理稠密图。
方法一:朴素Dijkstra算法O(n2)
s集合:当前已确定最短距离的点。
Step1:初始化点的距离,起点距离为0,其他所有点都是正无穷。
dist[1] = 0; dist[i] = INF;
Step2:for i: 0~n
找到不在s中的距离最近的点t
将t加入集合s
用t更新其他所有点的距离,即看t节点的出边连接的节点到起点的距离能否使用t来更新,dist[x] > dist[t] + w.
该步结束后可以得到每个点到起点的距离。
#include<iostream>
#include<cstring>
#include<algorithm>
/*给定一个n个点m条边的有向图,图中可能存在重边与自环,所有边均为正值
请求出1~n的最短距离,如果不通则输出-1*/
using namespace std;
const int N = 510;
int n, m;
int g[N][N];
int dist[N];
bool st[N];
int dijkstra()
{
//初始化点的距离
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < n; i ++)
{
int t = -1;
for(int j = 1; j <= n; j ++)
//集合中没有这个点,且当前的t不是最短的就把t更新成j(t=-1直接赋值)
if(!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
st[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;
return dist[n];
}
int main()
{
scanf("%d%d", &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);//保存长度最短的那条边即可
}
int t = dijkstra();
printf("%d\n", t);
return 0;
}
方法二:堆优化版的Dijkstra算法O(mlogn)
在方法一中,用t更新其他点的距离需要m次,但如果用堆来存储所有点的距离,那么时间复杂度就会变成mlogn.
堆有两种实现方法,一种是手写堆,堆中只需要n个数;另一种是用优先队列,但是它不能修改元素值,只能通过冗余的方法,即往堆中插入新的元素,即有m个元素,时间复杂度为mlogm。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
/*给定一个n个点m条边的有向图,图中可能存在重边与自环,所有边均为正值
请求出1~n的最短距离,如果不通则输出-1*/
using namespace std;
typedef pair<int, int> PII;
const int N = 100010;
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())
{
//当前距离最小的点
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if(st[ver]) continue;//已经遍历过
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;
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 = dijkstra();
printf("%d\n", t);
return 0;
}
2. 存在边是负权值的图
SPFA算法是Bellman算法的优化,但有些情况是SPFA算法解决不了的,比如解决边数不超过k条的图的问题,就只能使用Bellman算法。
方法一:Bellman-Ford O(nm)
for n次
对dist数组进行备份。
for 所有边 a, b, w
dist[b] = min(dist[b], dist[a] + w);//松弛操作
看从起点经过a再到b的距离与从起点直接到b的距离哪个更近。
结束之后满足dist[b] <= dist[a] + w,称该式为三角不等式。
此处备份的原因:在更新这些点的距离时可能会发生串联,即随着一些点的变化,另一些点会随之改变距离,所以要进行备份保证每个点的距离都是上一次的结果。
注:如果图中存在负权回路,即有一个回路所有边总和为负值,那么可以走无数遍该回路得到距离为负无穷,故这种情况下不存在最短路。
#include<iostream>
#include<cstring>
#include<algorithm>
/*给一个n个点m条边的问题,图中可能存在重边和自环,边权可能为负数
请你求出从1号到n号点的最多经过k条边的最短距离,不同则输出impossible*/
using namespace std;
const int N = 510, M = 10010;
int n, m, k;
int dist[N], backup[N];
struct Edge
{
int a, b, w;
}edges[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 j = 0; j < m; j ++)
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
dist[b] = min(dist[b], backup[a] + w);
}
}
if(dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for(int i = 0; i < m; i ++)
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
int t = bellman_ford();
if(t == -1) puts("impossible");
else printf("%d\n", t);
return 0;
}
方法二:SPFA 一般:O(m) 最坏:O(nm)
优化了dist[b] = min(dist[b], dist[a] + w)这个式子,当dist[a]更新后变小了,dist[b]才可能会变小。
1. 这里使用宽搜来实现:
将更新后变小的点放入队列,拿出队头更新它的所有出边,更新后的出边再放入队列。(思路是,更新过谁,再拿谁来更新别人)
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
/*给一个n个点m条边的问题,图中可能存在重边和自环,边权可能为负数
请你求出从1号到n号点的最多经过k条边的最短距离,不同则输出impossible*/
using namespace std;
const int N = 100010;
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 spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
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];
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
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);
}
int t = spfa();
if(t == -1) puts("impossible");
else printf("%d\n", t);
return 0;
}
2. 判断负环的方式:
dist[x]存最短距离,cnt[x]存边数
dist[x] = dist[t] + w[i];
cnt[x] = cnt[t] + 1;
在更新最短距离的同时更新边的数量,当cnt[x] >= n时,即一共n个点却出现n+1条边,且距离更新了,故一定存在负环。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
/*给定一个n个点m条边的有向图,图中存在重边和自环,边权可能为负
请判断图中是否存在负权回路*/
using namespace std;
const int N = 10010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N], cnt[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()
{
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;
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算法 O(n3)(基于动态规划)
for( k: 1 ~ n)
for( i: 1 ~ n)
for( j: 1 ~ n)
d(i, j) = min(d(i, j), d(i, k) + d(k, j));
循环完毕之后,d(i, j)存的就是i到j的最短路。
原理:d[k, i, j]:从i开始要经过1~k个中间点到达j点。
d[k, i, j] = d[k-1, i, k] + d[k-1, k, j];将最高维优化掉后就是d[i, j] = d[i, k] + d[k, j]
#include<iostream>
#include<algorithm>
#include<cstring>
/*给一个n个点m条边的问题,图中可能存在重边和自环,边权可能为负数
在给定k个询问,每个询问包含两个整数x和y,表示查询从x到y的最短距离
不存在输出imposssible*/
using namespace std;
const int N = 210, INF = 1e9;
int n, m, 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][i] = 0;
else d[i][j] = INF;
}
while(m--)
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
//有多条边就读入最小的即可
d[a][b] = min(d[a][b], w);
}
floyd();
while(Q --)
{
int a, b;
scanf("%d%d", &a, &b);
if(d[a][b] > INF / 2) puts("impossible");
else printf("%d\n", d[a][b]);
}
return 0;
}