最短路算法及其常见扩展应用
第一节——最短路问题
基本概念:由于无向边可以看作两条相反的有向边,于是我们默然按照有向边的形式讨论
存图方式:
- 邻接矩阵:空间复杂度
,优点: 查找 的边是否存在,方便
scanf("%d%d%d",&u,&v,&w);
a[u][v]=w;//邻接矩阵
- 邻接表:空间复杂度
void add(int u,int v,int w){//u->v的边权为w的边
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
//遍历时:
for(int i=head[u];i;i=nxt[i])……
- vector,这个和邻接表相比无任何突出特点,故略去
邻接表是最常用的
单源最短路问题(SSSP)
为了方便叙述,下面没特殊说明默认
Dijkstra算法
应用于非负权图上,基于贪心思想
它的过程是这样的
- 寻找到所有未被标记的的节点中
值最小的,将其取出并标记,记为 - 遍历从
出发的所有边,将这些边所连后继节点 的 值全部用 更新, - 重复上述步骤直到全部节点被标记
性质:由于是非负权图,我们取出的 节点的 一定是无法被更新的节点,此时的 值就是起点到 的最短距离
上述算法的复杂度为
我们发现,算法的瓶颈在于第一步,对于这种情况,我们可以采取二叉堆优化
详细地说,我们采用一个小根堆,以 值为第一关键字进行优化,那么我们的步骤就可以变为
1.取出堆顶节点 ,若被标记,则继续取出堆顶直到堆顶无标记为止(懒惰删除法)
2.更新 的所有后继节点 ,如果被成功更新,则将 入队
3.直到堆为空时,算法结束
实现细节
1.如果懒得重载运算符,建议把
2.时刻记得memset(dist,0x3f,sizeof dist);
代码如下:
// 堆优化Dijkstra算法,O(mlogn)
const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], Next[M], d[N];
bool v[N];
int n, m, tot;
// 大根堆(优先队列),pair的第二维为节点编号
// pair的第一维为dist的相反数(利用相反数变成小根堆,参见0x71节)
priority_queue< pair<int, int> > q;
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
void dijkstra() {
memset(d, 0x3f, sizeof(d)); // dist数组
memset(v, 0, sizeof(v)); // 节点标记
d[1] = 0;
q.push(make_pair(0, 1));
while (q.size()) {
// 取出堆顶
int x = q.top().second; q.pop();
if (v[x]) continue;
v[x] = 1;
// 扫描所有出边
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i], z = edge[i];
if (d[y] > d[x] + z) {
// 更新,把新的二元组插入堆
d[y] = d[x] + z;
q.push(make_pair(-d[y], y));
}
}
}
}
int main() {
cin >> n >> m;
// 构建邻接表
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
// 求单源最短路径
dijkstra();
for (int i = 1; i <= n; i++)
printf("%d\n", d[i]);
}
它的复杂度是
Bellman-ford与SPFA
在国际上,
首先,在一张有向图上,若是对于任意边
- 建立一个队列,最初队里只有起点
, 其余均为正无穷 - 取出队头
,将所有队头出发的后继节点 进行更新,若更新成功,则将 入队 - 重复第二步直到队列为空
代码模板
// SPFA算法
const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], Next[M], d[N];
int n, m, tot;
queue<int> q;
bool v[N];
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
void spfa() {
memset(d, 0x3f, sizeof(d)); // dist数组
memset(v, 0, sizeof(v)); // 是否在队列中
d[1] = 0; v[1] = 1;
q.push(1);
while (q.size()) {
// 取出队头
int x = q.front(); q.pop();
v[x] = 0;
// 扫描所有出边
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i], z = edge[i];
if (d[y] > d[x] + z) {
// 更新,把新的二元组插入堆
d[y] = d[x] + z;
if (!v[y]) q.push(y), v[y] = 1;
}
}
}
}
int main() {
cin >> n >> m;
// 构建邻接表
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
// 求单源最短路径
spfa();
for (int i = 1; i <= n; i++)
printf("%d\n", d[i]);
}
在随机图的情况下,SPFA算法的时间复杂度是
同样,正是因为SPFA使得三角形不等式绝对收敛,以至于其可以在负权图上工作,当位于正权图的时候,一样可以采取堆优化,最后得到的算法与dijkstra算法一模一样,一个是基于贪心的非负权算法,一个是基于三角形不等式收敛的通用算法,在非负权图上却是殊途同归
特殊情形下的线性算法
- 在
中,我们可以按照拓扑序或者记忆化搜索 递推出最短路 - 在边权只为0/1的情况下,我们可以不使用优先队列,直接将dijkstra算法里的优先队列替换为双端队列,对于一个新加入的节点,与其父节点连边的边权是1就插入队尾,是0就插入队头
- 形式上来说,单源最短路径问题很类似与优先队列BFS,本质上来说是一致的,所以类似于
算法的估价函数一样可以使用
全源最短路径算法:Floyd
简单来说就是在一张图上要求出任意两点间的最短路径,我们使用Floyd算法,时间复杂度为
使用动态规划的思想,设
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!