基础
- 首先最短路问题我们要分为两类问题单源和多源问题
- 单源:
- Dijkstra O ( n 2 ) O(n^2) O(n2) /Dijkstra(堆优化) O ( m l o g n ) O(mlogn) O(mlogn) 无负权边
- bellman-ford O ( n m ) O(nm) O(nm)
- spfa O ( m ) O(m) O(m) 最坏 O ( n m ) O(nm) O(nm)
- 多源:
- floyd O ( n 3 ) O(n^3) O(n3)
n:点/m:边
- floyd O ( n 3 ) O(n^3) O(n3)
Dijkstra
算法思路(贪心):
简单来说基本思想就是从起点开始,每次找出到起点能到达的距离最短的点,然后以该点更新其他点的距离.
实现:首先我们用 d i s t dist dist存储到起点的距离,初始的时候第一个点的 d i s t dist dist值为 0 0 0其他所有点均为 + ∞ +\infty +∞ (这里运用 m e m s e t memset memset(数组名字, 0 x 3 f 0x3f 0x3f,数组大小)来实现这个函数在 c s t r i n g cstring cstring中)
模版
C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510;
int n,m;
int g[N][N];
int dist[N];
bool st[N];
int dijkstra(int s) {
memset(dist,0x3f,sizeof dist);
dist[s] = 0;
for (int i = 0;i < n;i++) {
int t = -1;
for (int j = 1;j <= n;j++) {
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]);
}
}
return dist[n];
}
int main() {
scanf("%d%d",&n,&m);
int a,b,c;
memset(g,0x3f,sizeof g);
while (m --) {
scanf("%d%d%d",&a,&b,&c);
g[a][b] = min(g[a][b],c);
}
int t = dijkstra(1);
printf("%d\n",t);
return 0;
}
Dijkstra(堆优化)
算法思想:
D i j k s t r a Dijkstra Dijkstra开堆优化主要是优化了其中每次找最短距离的点的步骤,每次把 d i s t dist dist放入小根堆中堆顶自然就是用来更新其他的点的点,这里的堆也可以用线段树代替有时候时间复杂度更低.
这里我们用STL中的优先队列( p r i o r i t y − q u e u e priority-queue priority−queue)(堆)-具体使用方式见数据结构(手写堆+STL+任意位置删除)
模版
C++
#include <iostream>
#include <cstring>
#include <functional>
#include <queue>
using namespace std;
typedef pair<int,int> PII;
const int N = 100010;
int n,m;
int e[N],h[N],w[N],ne[N],idx;//邻接表存储图不会的看链表那章
int d[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(int s) {
memset(d,0x3f,sizeof d);
d[s] = 0;
priority_queue<PII,vector<PII>,greater<PII>> hp;//这里的意思是小根堆以二元组来存储每个值
hp.push({0,s});
while (hp.size()) {
auto t = hp.top();
hp.pop();
int ver = t.second,dist = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver];i != -1;i = ne[i]) {
int j = e[i];
if (d[j] > dist + w[i]) {
d[j] = dist + w[i];
hp.push({d[j],j});
}
}
}
if (d[n] == 0x3f3f3f3f) return -1;
return d[n];
}
int main() {
scanf("%d%d",&n,&m);
int a,b,c;
memset(h,-1,sizeof h);
while (m --) {
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
int t = dijkstra(1);
printf("%d\n",t);
return 0;
}
Bellman-Ford
算法思路
进行k轮循环,每轮循环枚举每条边进行松弛操作
松弛操作 : d i s t [ b ] = m i n ( d i s t [ b ] , b a c k u p [ a ] + w ) dist[b] = min(dist[b],backup[a]+w) dist[b]=min(dist[b],backup[a]+w)
简单来说就是动态更新每条边
模版
C++
#include <cstring>
#include <iostream>
using namespace std;
const int N = 510,M = 10010;
int n,m,k;
int d[N],bk[N];
struct Edge {
int a,b,w;
}edge[M];
int bellman_ford(int s) {
memset(d,0x3f,sizeof d);
d[s] = 0;
for (int i = 0;i < k;i++) {
memcpy(bk,d,sizeof d);
for (int i = 0;i < m;i++) {
int a = edge[i].a,b = edge[i].b,w = edge[i].w;
d[b] = min(d[b],bk[a]+w);
}
}
if (d[n] > 0x3f3f3f3f / 2) return -1;
return d[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);
edge[i] = {a,b,w};
}
int t = bellman_ford(1);
if (t == -1) puts("imposible");
else printf("%d\n",t);
return 0;
}
判负环
- 那么 b e l l m a n − f o r d bellman-ford bellman−ford怎么判断负环的存在呢?
- 思路:其实在没有负环的情况下我们进行 n − 1 n-1 n−1轮松弛操作一定可以更新完所有点到起点的最短路,所以如果有负环存在那么第 n n n轮还可以继续更新因为只要过负环就可以让 d d d值变小那就可以继续更新
- 实现:那么我们只需要在 n − 1 n-1 n−1轮循环以后在循环一次如果还可以更新那么就存在负环
SPFA
算法思路
我们很容易可以发现 b e l l m a n − f o r d bellman-ford bellman−ford中的松弛操作是有冗余的.
我们只需要更新 d d d值变小的点之后的点的 d d d值就行了,因为只有一个点 d d d值变小了那他之后的点 d d d值才有可能变小.
所以我们开一个队列,用队列来维护所有被更新的点就可以了.
模版
C++
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int n,m;
int d[N];
int e[N],ne[N],h[N],w[N],idx;
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(int s) {
memset(d,0x3f,sizeof d);
d[s] = 0;
queue<int> q;
q.push(s);
st[s] = true;
while (q.size()) {
int t = q.front();
q.pop();
for (int i = h[t];i != -1;i = ne[i]) {
int j = e[i];
if (d[j] > d[t] + w[i]) {
d[j] = d[t] + w[i];
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
if (d[n] > 0x3f3f3f3f / 2) return -1;
return d[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(1);
if (t == -1) puts("imposible");
else printf("%d\n",t);
return 0;
}
判负环
- 那么 s p f a spfa spfa判负环的话我们只需要开一个 c n t cnt cnt的数组记录该点经过的边数,当有个点经过的边数 ≥ n ≥n ≥n时那么负环就存在了,因为只有存在负环才会使一个只有 n − 1 n-1 n−1条边的图中的一个点经过 n n n条边.
Floyd(动态规划)
算法思路
f l y o d flyod flyod是基于动态规划来解决最短路问题,思路很简单
- 状态表示
- 最开始我们可以想成三维
- f [ k ] [ x ] [ y ] f[k][x][y] f[k][x][y] 表示只允许经过 1 − k 1-k 1−k点时 x − > y x->y x−>y的最短路径.
- 状态计算
- 那么我们只更新 f [ k ] [ x ] [ y ] = m i n ( f [ k − 1 ] [ x ] [ y ] , f [ k − 1 ] [ x ] [ k ] + f [ k − 1 ] [ k ] [ x ] ) f[k][x][y] = min(f[k-1][x][y],f[k-1][x][k] + f[k-1][k][x]) f[k][x][y]=min(f[k−1][x][y],f[k−1][x][k]+f[k−1][k][x]) 就可以了,也就是让 x x x-> k + k k+k k+k-> y y y 的距离如果小于 x x x-> y y y 的距离那就跟新.
- 这里不难看出其实第一维并没有什么鸟用,就是拿上轮的数据跟新所以直接删了就行这样状态表示只需要二维就行.
模版
C++
#include <cstring>
#include <iostream>
#include <mmintrin.h>
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][j] = 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("imposible");
else printf("%d\n",d[a][b]);
}
return 0;
}