最短路算法
邻接矩阵
设一个图中有个点,那么这个图的邻接矩阵就是一个的矩阵。
所以用一个二维数组来储存这个邻接矩阵。
举个例子:若已知无向图中,到的路径权值是,那可以给赋值为,由于这是一个无向图,所以还需要反向连一次边,就是把也赋值为。
邻接矩阵的空间复杂度是。
邻接表
邻接矩阵的空间复杂度对于稀疏图来说太浪费,所以邻接表就派上用场。
邻接表通常用四个数组,,,来储存。
数组记录了每个节点的第一条边在数组和数组中的储存位置;
数组和数组分别储存每条边的终点和边权;
数组则模拟了链表指针;
//加入一条从x到y,权值为z有向边
void add (int x, int y, int z) {
ver[++ tot] = y;
edge[tot] = z;
next[tot] = head[x];
head[x] = tot;
}
//访问从x出发的所有边
for (int i = head[x]; i; i = next[i]) {
int y = ver[i], z = edge[i];
}
邻接表的空间复杂度为。
Dijkstra
基础
Dijkstra算法是求单源最短路的,单源的意思是只能知道某一个点到其他所有点的最短距离。
算法流程:每次找出未被标记,且距离源点最近的点,然后从这个点出发,更新这个点的所有出边的点到源点的距离,重复这个操作,直到所有节点都被标记。
接下来我们看代码:
void dijkstra (int s) { //s是我们的源点
memset(dis, INF, sizeof(dis)); //我们把所有的点到源点的距离都初始化为无穷大
int cur = s; //cur为当前节点,初始化为源点
dis[cur] = 0;
vis[cur] = 1;
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= n; j ++)
if (!vis[j] && dis[cur] + map[cur][j] < dis[j]) //此时找到了从cur出发能到达下一个点且未被标记的更优路径,所以更新这条路径
dis[j] = dis[cur] + map[cur][j];
int mini = INF;
for (int j = 1; j <= n; j ++)
if (!vis[j] && dis[j] < mini) //此时找到了能到达的最短且未被标记的点
mini = dis[cur = j]; //更新下一次我们出发的点
vis[cur] = 1; //标记我们下一次出发的点
}
}
优化
上个程序的时间复杂度为,那么如何优化呢?
是否发现我们每次找下一次出发的点的时候,都是暴力来找的,那如果用优先队列来维护这个最近的点,则可以用的时间来找出这个点。
同时我们还可以把邻接矩阵换成邻接表,优化空间复杂度。
代码如下:
struct Point {
int id, val; //id是节点的编号,val是从源点到达id这个节点的路径
Point (int id, int val) : id(id), val(val) {}
bool operator < (const Point &a) const { //重载运算符
return val > a.val;
}
};
void dijkstra (int s) {
for (int i = 1; i <= n; i ++)
dis[i] = INF;
priority_queue <Point> q;
q.push(Point(s, 0));
dis[s] = 0;
while (!q.empty()) {
int x = q.top().id; //用优先队列来找到我们的下一个目标节点cur
q.pop();
if (vis[x]) continue;
vis[x] = 1;
for (int i = head[x]; i; i = next[i]) { //邻接表遍历
int y = ver[i];
if (!vis[y] && dis[y] > dis[x] + edge[i]) {
dis[y] = dis[x] + edge[i];
q.push(Point(y, dis[y]));
}
}
}
}
所以我们还是写个模板题练练手吧!
Dijkstra算法优化后虽然时间复杂度优越,但是对于有负权边的图则跑不了。
因为Dijkstra基于贪心思想,每次都只走当前最短的路,且节点被标记后就无法改变,但是如果有负权边,贪心思想则是错误的。
那么对于负权边的图,SPFA算法就可以解决这个问题。
SPFA
给定一张有向图,若图中的某一条边满足(,分别表示两端节点,表示权值),则称这条边满足三角形不等式。
若所有边都满足三角形不等式,那么数组就是最短路。
SPFA算法正是让所有边都满足三角形不等式。
算法流程:建立一个队列,把源点放进队列里。每次从队列取出队头,从这个点出发,如果他的出边使得,就把这个出边的点放入队列,然后把队头弹出。重复以上操作,直到队列清空。
代码如下:
void spfa (int s) {
for (int i = 1; i <= n; i ++)
dis[i] = INF;
queue <int> q;
q.push(s);
dis[s] = 0;
while (!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 0;
for (int i = head[x]; i; i = next[i]) {
int y = ver[i];
if (dis[y] > dis[x] + edge[i]) {
dis[y] = dis[x] + edge[i];
if (!vis[y]) {
q.push(y);
vis[y] = 1;
}
}
}
}
}
Floyd
floyd算法与上面两个算法不同的是它求的是多源最短路,它的实质其实是动态规划,定义为到的最短路,初始值,为邻接矩阵。
状态转移方程,为沿途经过的城市。
代码也十分简单:
for (int k = 1; k <= n; k ++)
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
其中值得注意的是,这重状态的必须放在最外面循环。
因为是阶段,如果将其放在最内层循环,将在这次循环后被确定,但是在这次循环中,和都不一定是最短路。
而如果放在最外层循环,是一直在被更新的。
然后便是模板题了。
试题
T1:维修电路
T2:跳跳虎回家
T3:最优贸易
题解
维修电路:
可以把每个方格的周围四个点都看成一个节点,节点数量。
如果在位置的方格:
左上角的点:;
右上角的点:;
左下角的点:;
右下角的点:;
这样就可以把所有的点都表示出来了。
把\的电路的左上角和右下角的点以边权相连,左下角和右上角的点以边权相连,代表需要花费。
同理,把/的电路也以相同的方法相连。
最后,用Dijkstra算法跑一遍最短路就可以找出最小花费了。
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 300010;
int n, t, r, c, tot, head[N], ver[N << 2], edge[N << 2], nex[N << 2], vis[N], dis[N];
struct Point {
int id, vel;
Point (int id, int vel) : id(id), vel(vel) {}
bool operator < (const Point &a) const {
return vel > a.vel;
}
};
inline void add (int x, int y, int z) {
ver[++ tot] = y;
edge[tot] = z;
nex[tot] = head[x];
head[x] = tot;
}
void dijkstra (int x) {
for (int i = 1; i <= n; i ++)
dis[i] = 0x3f3f3f3f;
priority_queue <Point> q;
q.push(Point(x, 0));
dis[x] = 0;
while (!q.empty()) {
int cur = q.top().id;
q.pop();
if (vis[cur]) continue;
vis[cur] = 1;
for (int i = head[cur]; i; i = nex[i]) {
int id = ver[i];
if (!vis[id] && dis[id] > dis[cur] + edge[i]) {
dis[id] = dis[cur] + edge[i];
q.push(Point(id, dis[id]));
}
}
}
}
int main () {
scanf("%d", &t);
while (t --) {
memset(head, 0, sizeof(head));
memset(ver, 0, sizeof(ver));
memset(edge, 0, sizeof(edge));
memset(nex, 0, sizeof(nex));
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
tot = 0;
scanf("%d%d", &r, &c);
n = (r + 1) * (c + 1);
for (int i = 1; i <= r; i ++) {
char s[N];
scanf("%s", s + 1);
for (int j = 1; j <= c; j ++) {
if (s[j] == '\\') {
add((i - 1) * (c + 1) + j, i * (c + 1) + j + 1, 0);
add(i * (c + 1) + j + 1, (i - 1) * (c + 1) + j, 0);
add((i - 1) * (c + 1) + j + 1, i * (c + 1) + j, 1);
add(i * (c + 1) + j, (i - 1) * (c + 1) + j + 1, 1);
} else {
add((i - 1) * (c + 1) + j + 1, i * (c + 1) + j, 0);
add(i * (c + 1) + j, (i - 1) * (c + 1) + j + 1, 0);
add((i - 1) * (c + 1) + j, i * (c + 1) + j + 1, 1);
add(i * (c + 1) + j + 1, (i - 1) * (c + 1) + j, 1);
}
}
}
if ((r + c) % 2) {
printf("NO SOLUTION\n");
continue;
}
dijkstra(1);
printf("%d\n", dis[(r + 1) * (c + 1)]);
}
return 0;
}
跳跳虎回家:
这道题相比最短路模板,多出了限制使用的路。
我们让表示从源点到节点,使用了次限制道路的最短路。
假设我们现在从节点走到节点,使用了次限制道路:
若当前走的是普通道路,;
若当前走的是限制道路,。
最后,在使用限制道路的数量的前提下,找出最小的就是答案了。
代码:
#include <cstdio>
#include <queue>
#include <iostream>
#include <vector>
using namespace std;
const int INF = 1e8;
const int MAXN = 510;
const int MAXM = 2010;
int n, m, q, k, ans, dis[MAXN][MAXN];
struct data {
int u, j;
};
struct edge {
int to, w, d;
};
vector <edge> e[MAXN];
inline int read () {
int res = 0;
char ch = getchar();
while (ch < '0' || ch > '9')
ch = getchar();
while (ch >= '0' && ch <= '9') {
res = res * 10 + (ch - '0');
ch = getchar();
}
return res;
}
void spfa () {
for (int i = 1; i <= n; i ++)
for (int j = 0; j <= k; j ++)
dis[i][j] = INF;
dis[1][0] = 0;
queue <data> q;
q.push((data){1, 0});
while (!q.empty()) {
int u = q.front().u, j = q.front().j;
q.pop();
for (int i = 0; i < e[u].size(); i ++) {
int v = e[u][i].to, w = e[u][i].w, d = e[u][i].d;
if (d) {
if (dis[v][j + 1] > dis[u][j] + w) {
dis[v][j + 1] = dis[u][j] + w;
q.push((data){v, j + 1});
}
}
else {
if (dis[v][j] > dis[u][j] + w) {
dis[v][j] = dis[u][j] + w;
q.push((data){v, j});
}
}
}
}
}
int main () {
n = read();
m = read();
q = read();
k = read();
k = min(k, min(n, q));
for (int i = 1; i <= m; i ++) {
int u, v, w;
u = read();
v = read();
w = read();
e[u].push_back((edge){v, w, 0});
}
for (int i = 1; i <= q; i ++) {
int u, v, w;
u = read();
v = read();
w = read();
e[u].push_back((edge){v, w, 1});
}
spfa();
ans = INF;
for (int i = 0; i <= k; i ++)
ans = min(ans, dis[n][i]);
if (ans == INF)
ans = -1;
printf("%d\n", ans);
return 0;
}
最优贸易:
题目需要我们从号节点到号节点的途中,以最优的差价买入和卖出水晶球。
我们不妨定义一个数组,代表从号节点到号节点的途中,节点可以在之前由最低的价格买入。
数组的初始值分别是所在节点的水晶球的价格,如果我们从节点到节点,那么。
因此我们在用SPFA走这张图的时候,可以更新数组。
再定义一个数组,代表在走到节点之前获得的最高差价。
如果我们从节点到节点,很显然可以这么转移:(表示节点的水晶球价格)。
由于必须在节点结束,所以就是答案。
代码:
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
const int N = 100010, M = 500010;
int n, m, a[N], head[N], ver[M << 1], nex[M << 1], tot, vis[N], minn[N], f[N];
inline void add (int x, int y) {
ver[++ tot] = y;
nex[tot] = head[x];
head[x] = tot;
}
inline void spfa () {
queue<int> q;
vis[1] = 1;
q.push(1);
while (!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 0;
for (int i = head[x]; i; i = nex[i]) {
int y = ver[i];
minn[y] = min(minn[y], minn[x]);
if (a[y] - minn[y] > f[y] || f[x] > f[y]) {
f[y] = max(a[y] - minn[y], f[x]);
if (!vis[y]) {
vis[y] = 1;
q.push(y);
}
}
}
}
}
int main () {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) {
f[i] = -1;
scanf("%d", &a[i]);
minn[i] = a[i];
}
f[1] = 0;
for (int i = 1; i <= m; i ++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
if (z == 1)
add(x, y);
else {
add(x, y);
add(y, x);
}
}
spfa();
printf("%d", f[n]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探