原图来自于wmy0217_大佬的文章,本文很多部分都参考了他的文章,特此注明。
01 存储
稀疏图:n和m的数量级相等,用邻接矩阵来存储;
稠密图:n 2 和m的数量级相同,用邻接表存储;
02 Dijkstra(朴素版)
核心思想:两层循环,第一层循环找离源点最近的点,第二层循环更新所有点到源点的距离;
核心代码:
for (int i = 1; i <= n; ++i)
{
int t = -1;
for (int j = 1; j <= n; ++j)
{
if (!st[j] && (t==-1 || dist[j] < dist[t]))
t = j;
}
st[t] = true; // 已找到
// 更新距离
for (int j = 1; j <= n; ++j)
{
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
注意事项:不能处理负环,并且g和dist都要初始化为正无穷(用0x3f来代替)。
样板题
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
参考代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int g[N][N];
bool st[N];
int dist[N];
int n, m;
int main()
{
memset(g, 0x3f, sizeof g);
memset(dist, 0x3f, sizeof dist);
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++i)
{
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
g[x][y] = min(z, g[x][y]);
}
dist[1] = 0;
for (int i = 1; i <= n; ++i)
{
int t = -1;
for (int j = 1; j <= n; ++j)
{
if (!st[j] && (t==-1 || dist[j] < dist[t]))
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] == 0x3f3f3f) printf("-1\n");
else printf("%d", dist[n]);
return 0;
}
03 Dijkstra(堆优化)
适用于稀疏图,用到了邻接表,个人使用了数组表示邻接表的方法,整理在了附录,可以先去看看。
优化方法:用堆排序,每次取出一个dis值最小的pair,second为点,first为第一个点的距离,也就是前面的求取最近点的方法,然后根据该点更新所有的点的距离,直到所有点遍历完(heap为空)。
和朴素的题目一样,但稍微修改一下数据,例如把N修改成1e5,这样朴素版必定超时(1e5 * 1e5 = 1e10);
而堆优化版本的时间复杂度为O(mlogn),则不会超时。
示例代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 2e5+10;
int h[N], ne[N], e[N], w[N], idx;
bool st[N];
int dist[N];
typedef pair<int, int> PII;
int n, m;
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.push({0, 1}); // first是距离,second是点
while(heap.size())
{
PII t = heap.top();
heap.pop();
int dis = t.first , ver = t.second;
if (st[ver]) continue; // 因为堆优化,弹出来的一定是dis最小的,所以重边只看第一个,后面直接跳过就行了
st[ver] = true;
// 根据最短边更新距离
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if (dis+w[i] < dist[j])
{
dist[j] = dis + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
memset(h, -1, sizeof h); // 需要在add()之前初始化,否则会覆盖h的值
cin >> n >> m;
while(m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
cout << "shortest distance of 1 to n: " << dijkstra() << endl;
return 0;
}
运行结果如图:
04 Bellman-ford算法
主要算法:循环N次,每次循环遍历每条边;
主要代码:
for(int i=0; i<n; i++)
for(int j=0; j<m; j++)
{
if(dist[a]+w<dist[b])
dist[b] = dist[a] + w; //w是a->b的权重
}
参考代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
const int M = 10010;
struct Edge{
int a, b, c;
}edge[M];
int dist[N], backup[N];
bool st[N];
int n, m, k;
void bellman_ford()
{
for (int i = 0; i < k; ++i)
{
memcpy(backup, dist, sizeof backup);
for (int j = 0; j < m; ++j)
{
int a = edge[j].a, b = edge[j].b, c = edge[j].c;
dist[b] = min(dist[b], backup[a]+c);
}
}
// 处理负权边
if (dist[n] > 0x3f3f3f3f/2) cout << "impossible" << endl;
else cout << dist[n] << endl;
}
int main()
{
memset(dist, 0x3f, sizeof dist);
cin >> n >> m >> k;
for (int i = 0; i < m; ++i)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
dist[1] = 0;
bellman_ford();
}
05 SPFA算法
优化后的Bellman-ford算法,优化思路:只有更新后的点,更新边才会更小,所以没更新的点直接跳过;
核心代码:
int spfa()
{// bool st[N]: 存第 i 个点是不是在队列中,防止存重复的点
memset(dist,0x3f,sizeof(dist));
dist[1] = 0;
queue<int> q; //存储所有待更新的点
q.push(1); // 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]) // 更新 t 的所有临边结点的最短路
{
int j = e[i];
if(dist[j] > dist[t]+w[i])
{
dist[j] = dist[t] + w[i];
if(!st[j]) //如果 j 不在队列,让 j 入队
{
q.push(j);
st[j] = true; // 标记 j 在队中
}
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1; // 不存在最短路
return dist[n];
}
例题洛谷P3371
参考代码:
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1e4 + 10;
const int M = 5e5 + 10;
int h[N], e[M], ne[M], w[M], idx;
bool st[N];
int dist[N];
int n, m, s;
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void spfa(int s)
{
memset(dist, 0x3f, sizeof dist);
queue<int> q;
q.push(s);
st[s] = true;
dist[s] = 0;
while (q.size())
{
int t = q.front(); // 与dijsktra的区别之一,普通queue没有top()
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;
}
}
}
}
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m >> s;
for (int i = 0; i < m; ++i)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
spfa(s);
for (int i = 1; i <= n; ++i)
{
if (dist[i] == 0x3f3f3f3f) cout << 2147483647 << " ";
else cout << dist[i] << " ";
}
return 0;
}
带有负环的图会导致最短路径失败,因为无限次经过负环可以使路径长度无限减小。但可以使用 SPFA 算法检测负环,方法是记录一个点被加入队列的次数,如果有一个点被加入队列的次数超过了节点数量,则说明存在负环。
因此,使用 SPFA 算法处理带负环的最短路径问题时,通常需要在算法外围添加一个负环检测的过程,如果检测到了负环,则认为不存在最短路径。如果没有检测到负环,则可以利用 SPFA 算法计算最短路径。
以下是带负环检测的,核心是一个 cnt 数组,如果 cnt>=n 了,因为1—》x的最多经历n个点,就认为存在负环。
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]) // 更新 t 的所有临边结点的最短路
{
int j = e[i];
if(dist[j] > dist[t]+w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1; // t到起点的边数+1
if(cnt[j] >= n) return true;// 存在负环
if(!st[j]) //如果 j 不在队列,让 j 入队
{
q.push(j);
st[j] = true; // 标记 j 在队中
}
}
}
}
return false;// 不存在负环
}
06 Floyd算法
基于动态规划,
- 直接 i 到 j;
- i 经过若干个结点到 k 再到 j
- 对于每一个k,我们都判断 d[i] [j] 是否大于 d[i] [k] + d[k] [j],如果大于,就可以更新d[i] [j]了;
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]);
}
附录 数组表示邻接表
01 需要的变量
方便使用,直接设为全局变量;
const int N = 1e3+10; // 点
const int M = 2e6+10; // 边
int h[N]; // h[i]表示以点i为头节点的边的编号
int e[2*M]; // e[i]表示第i条边对应的终点
int ne[2*M]; // ne[i]表示第i条边的下一条边的序号
int idx; // 边的序号,从0开始
02 初始化数组
方便遍历,需要中止条件,所以全部初始化为1,当遍历到-1时退出;
memset(h, -1, sizeof h);
03 添加边
void add(int a, int b) // 添加一条起点为a,终点为b的边
{
e[idx] = b; // idx边的终点设为b
ne[idx] = h[a]; // 下一条边的序号为h[a],如果为-1,表示没有下一条边
h[a] = idx++; // 这条边的序号设为idx
}
纠错:下图中 e[1] 其实是 e[0],写的时候没注意!
如图所示,如果没有其他边,就会指向-1,代表没有下一条边;
如果有,就会将当前边设为第一条,指向之前已经存在的边(c-a)。
04 邻接表的遍历
把每个节点当作头节点,然后遍历其中的存储的邻接点,直到遍历到-1为止;
for (int k = 0; k < N; ++k)
{
if (h[k] != -1)
{
cout << "表头节点:" << h[k] << " 邻接点:";
for (int i = h[k]; i != -1; i = ne[i]) // i代表边
{
cout << e[i] << " "; // 代表节点(i边的终点)
}
cout << endl;
}
}
05 图的遍历
void dfs(int u)
{
st[u] = true;
cout << u << " ";
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (j != -1)
{
dfs(j);
}
}
}
06 完整demo
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1e3 + 10;
const int M = 2e6 + 10;
int h[N], e[2 * M], ne[2 * M], idx;
bool st[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void travel()
{
for (int k = 0; k < N; ++k)
{
if (h[k] != -1)
{
cout << "h: " << h[k] << " other: "; // 边的节点
for (int i = h[k]; i != -1; i = ne[i])
{
int j = e[i];
cout << j << " ";
}
cout << endl;
}
}
}
void dfs(int u)
{
st[u] = true;
cout << u << " ";
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) // 防止出现环
{
dfs(j);
}
}
}
int main()
{
memset(h, -1, sizeof h);
add(1, 3);
add(2, 4);
add(1, 2);
add(3, 5);
travel();
cout << "dfs graph" << endl;
dfs(1);
return 0;
}
运行结果如图: