【学习笔记】最短路
【学习笔记】最短路
前言:只是对一些最短路算法的实现整理。
以下内容有部分摘自OI_wiki。
Floyd 算法
求全源最短路。可以有负边权。
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]);
Floyd 算法也可以在前
CF295B Greg and Graph
正向思路是按照题意每次删除点
所以我们逆向思考,看成每次添加一个点
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 505;
int dis[N][N], del[N];
ll ans[N];
bool flag[N];
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n; cin>>n;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
cin>>dis[i][j];
for(int i=1; i<=n; i++)
cin>>del[i];
for(int p=n; p>=1; p--){
int k = del[p];
flag[k] = 1;
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]);
if(flag[i] && flag[j]) ans[p] += dis[i][j];
}
}
}
for(int i=1; i<=n; i++)
cout<<ans[i]<<" ";
return 0;
}
传递闭包
给出若干个元素以及它们的两两关系,如果这些元素具有传递性,我们就可以推出尽可能多的元素之间的关系。
此时
转移方程为:
具体解释为
实现过程:
-
令
,满足自己到自己可达, 之间有关系(关系是 到 )。 数组内其余均为 。 -
三重循环跑一遍 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] |= (dis[i][k] & dis[k][j]);
Dijkstra 算法
求解非负权图中单源最短路最稳定的算法。
实现过程:
将结点分成两个集合:已确定最短路长度的点集(记为
-
初始化
,其他点的 均为 。 -
然后重复操作:
从
集合中,选取一个最短路长度最小的结点,移到 集合中。对那些刚刚被加入 集合的结点的所有出边执行松弛操作。直到 集合为空,算法结束。
有多种方法来维护操作中最短路长度最小的结点,不同的实现导致了 Dijkstra 算法时间复杂度上的差异。
这里主要讲堆优化的 Dijkstra 的实现过程 (因为只会这个):
-
初始化
,其他点的 均为 。并且将起点 以及它的距离 入队。 -
重复以下操作直至队列(堆)为空:
-
取出堆顶元素,并出队。堆是以当前元素的
尽量小为关键字。 -
如果已经访问过这个点
(堆顶元素对应的),则无视它进入下一次循环(因为 Dijkstra 算法基于贪心的思想已经更新了经过该点的最短路)。否则标记该点 为已访问。 -
遍历以
为起点的边,记终点为 , 为 的边权,如果满足 ,则进行松弛操作,并将该元素和其对应的 入队。
-
时间复杂度为
以下为模板题的代码实现:
#include<bits/stdc++.h>
#define INF 0x7fffffff
using namespace std;
int read(){
int f=1, k=0; char c = getchar();
while(c<'0' || c>'9'){if(c=='-') f = -1; c = getchar();}
while(c>='0' && c<='9'){k = (k<<1) + (k<<3) + (c^48); c = getchar();}
return f*k;
}
const int MAXN = 100010;
struct node{
int to, va;
bool operator <( const node &x )const{
return va > x.va;
}
};
vector<node> p[MAXN];
int dis[MAXN];
bool vis[MAXN];
int n, m, s;
priority_queue<node> q;
void dijkstra(){
for(int i=1; i<=n; i++)
dis[i] = INF;
dis[s] = 0;
q.push((node){s, 0});
while(!q.empty()){
node x = q.top(); q.pop();
if(vis[x.to]) continue;
vis[x.to] = true;
for(int i=0; i<p[x.to].size(); i++){
node now = p[x.to][i];
if(dis[x.to]+now.va < dis[now.to]){
dis[now.to] = dis[x.to]+now.va;
q.push((node){now.to, dis[now.to]});
}
}
}
}
int main(){
n = read(), m = read(), s = read();
for(int i=1, x, y, z; i<=m; i++){
x = read(), y = read(), z = read();
p[x].push_back((node){y, z});
}
dijkstra();
for(int i=1; i<=n; i++){
printf("%d ", dis[i]);
}
return 0;
}
顺带一提最短路计数。多了一个统计。
Bellman–Ford 算法
Bellman–Ford 算法是一种基于松弛操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。
实现过程:
不断尝试对图上每一条边进行松弛。每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。
每次循环是
还有一种情况,如果从
需要注意的是,以
因此如果需要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 0 的边,然后以超级源点为起点执行 Bellman–Ford 算法。
代码实现(判是否存在负环):
struct Edge {
int u, v, w;
};
vector<Edge> edge;
int dis[MAXN], u, v, w;
const int INF = 0x3f3f3f3f;
bool bellmanford(int n, int s) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
bool flag = false; // 判断一轮循环过程中是否发生松弛操作
for (int i = 1; i <= n; i++) {
flag = false;
for (int j = 0; j < edge.size(); j++) {
u = edge[j].u, v = edge[j].v, w = edge[j].w;
if (dis[u] == INF) continue;
// 无穷大与常数加减仍然为无穷大
// 因此最短路长度为 INF 的点引出的边不可能发生松弛操作
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
flag = true;
}
}
// 没有可以松弛的边时就停止算法
if (!flag)
break;
}
// 第 n 轮循环仍然可以松弛时说明 s 点可以抵达一个负环
return flag;
}
SPFA 算法
关于 SPFA,它死了。
本质上是经过队列优化过的 Bellman–Ford 算法。所以能做的事情跟它差不多。并且最坏复杂度也是
优化思路:
很多时候我们并不需要那么多无用的松弛操作。
很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。
那么我们用队列来维护「哪些结点可能会引起松弛操作」,就能只访问必要的边了。
SPFA 也可以用于判断
实现过程:
-
初始化
,其他点的 均为 , 均为 。并且将起点 入队。 -
重复以下操作直至队列为空:
-
取出队头元素
,并出队。并且标记 。 -
遍历以
为起点的边,记终点为 , 为 的边权,如果满足 ,则进行松弛操作。满足可松弛操作时 不在队列内( )则将 入队,并标记 。
-
想要判负环的话加个统计,看最短路经过的边数是否
判断一整个图中是否有负环也需要建一个超级源点,最后看松弛次数是否
代码实现(判负环):
struct edge {
int v, w;
};
vector<edge> e[maxn];
int dis[maxn], cnt[maxn], vis[maxn];
queue<int> q;
bool spfa(int n, int s) {
memset(dis, 63, sizeof(dis));
dis[s] = 0, vis[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = 0;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1; // 记录最短路经过的边数
if (cnt[v] >= n) return false;
// 在不经过负环的情况下,最短路至多经过 n - 1 条边
// 因此如果经过了多于 n 条边,一定说明经过了负环
if (!vis[v]) q.push(v), vis[v] = 1;
}
}
}
return true;
}
以下为一些关于图的应用...
无向图最小环
暴力解法:
设
那么无向图中的最小环是
注意若是在有向图中求最小环,相对应的公式要修改,最小环是
总时间复杂度
Floyd:
记原图中
我们注意到 Floyd 算法有一个性质:在最外层循环到点
由最小环的定义可知其至少有三个顶点,设其中编号最大的顶点为
故在循环时对于每个
总时间复杂度为
int val[maxn + 1][maxn + 1]; // 原图的邻接矩阵
int dis[maxn + 1][maxn + 1]; // 最短路矩阵
int floyd(int n) {
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = val[i][j]; // 初始化最短路矩阵
int ans = INT32_MAX;
for (int k = 1; k <= n; ++k) {
for (int i = 1; i < k; ++i)
for (int j = 1; j < i; ++j)
ans = min(ans, dis[i][j] + val[i][k] + val[k][j]); // 更新答案
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]); // 正常的 floyd 更新最短路矩阵
}
return ans;
}
Dijkstra:
枚举所有边,每一次求删除一条边之后对这条边的起点跑一次 Dijkstra,道理同上。
时间复杂度
差分约束
差分约束系统是一种特殊的
差分约束系统中的每个约束条件
注意到,如果
过程:
设
一些转化技巧:
题意 | 转化 | 连边 |
---|---|---|
add(a, b, -c); |
||
add(b, a, c); |
||
add(b, a, 0), add(a, b, 0); |
P1993 小 K 的农场
比较全面的模板题。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5005;
struct node{
int v, w;
};
vector<node> g[N];
bool inque[N];
int cnt[N], dis[N];
int n, m;
bool spfa(int s){
memset(dis, 0x3f, sizeof(dis));
queue<int> Q;
Q.push(s); inque[s] = true;
dis[s] = 0;
while(!Q.empty()){
int u = Q.front();
Q.pop(); inque[u] = false;
for(auto [v, w] : g[u]){
if(dis[v] > dis[u]+w){
dis[v] = dis[u]+w;
cnt[v] = cnt[u]+1;
if(cnt[v]>=n+1) return false;
if(!inque[v])
Q.push(v), inque[v] = true;
}
}
}
return true;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>m;
while(m--){
int op; cin>>op;
if(op==1){
// b<=a-c dis[v]<dis[u]+{u->v}
int a, b, c; cin>>a>>b>>c;
g[a].push_back({b, -c});
} else if(op==2){
// a<=b+c
int a, b, c; cin>>a>>b>>c;
g[b].push_back({a, c});
} else{
int a, b; cin>>a>>b;
// a=b a>=b b>=a
g[b].push_back({a, 0});
g[a].push_back({b, 0});
}
}
for(int i=1; i<=n; i++){
g[0].push_back({i, 0});
}
cout<<(spfa(0) ? "Yes" : "No");
return 0;
}
最短路树
在单源最短路的过程中,每个点
如果我们得到一棵树,根节点为单源最短路的起点
简单来说,定义构建一棵树,使得树上任意不属于根的节点
实现过程:
要得到最短路树,首先需要知道每个点的前驱。
在 Dijkstra 的过程中,我们只需要记录每个点
然后把每个节点的父亲设置成它的前驱就行了。
注意到这样做必定是没有环的。因为将节点出队顺序看作一个拓扑序,节点之间的前驱关系是符合这个拓扑序的。也就是说每个点的前驱必然比它早出队。因此即使图上有零权环也没关系。
P3556 [POI2013] MOR-Tales of seafaring
题意:给
因为不一定是简单路径,所以可以在一条边上反复横跳,这样只要分别记录
如果一个点是孤立的,那么怎么走都无解!
思路一(重要):分层图。
考虑拆点。将
因为边权均为
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5005;
vector<int> g[N<<1];
struct node{
int id, v, w;
};
int dis[N<<1][N<<1];
void bfs(int s){
queue<int> Q;
Q.push(s);
while(!Q.empty()){
int u = Q.front(); Q.pop();
for(int v : g[u]){
if(!dis[s][v]){ // 防止一条边走过两次之后继续重复
dis[s][v] = dis[s][u]+1;
Q.push(v);
}
}
}
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m, qy; cin>>n>>m>>qy;
for(int i=1; i<=m; i++){
int u, v; cin>>u>>v;
g[u].push_back(v+n); g[v+n].push_back(u);
g[u+n].push_back(v); g[v].push_back(u+n);
}
for(int i=1; i<=n; i++)
bfs(i);
for(int i=1; i<=qy; i++){
int u, v, d; cin>>u>>v>>d;
if(d & 1){
if(dis[u][v+n]<=d && dis[u][v+n]) cout<<"TAK\n";
else cout<<"NIE\n";
} else{
if(dis[u][v]<=d && dis[u][v]) cout<<"TAK\n";
else cout<<"NIE\n";
}
}
return 0;
}
思路二:用不同的数组存距离。
因为
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5005;
vector<int> g[N];
struct node{
int id, v, w;
};
vector<node> query[N];
bool ans[1000005], vis[N];
int dis[N][2]; // 分奇偶
void spfa(int s){
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
queue<int> Q;
dis[s][0] = 0;
vis[s] = 1; Q.push(s);
while(!Q.empty()){
int u = Q.front(); Q.pop();
vis[u] = 0;
for(int v : g[u]){
if(dis[v][1] > dis[u][0]+1){
dis[v][1] = dis[u][0]+1;
if(!vis[v]) vis[v] = 1, Q.push(v);
}
if(dis[v][0] > dis[u][1]+1){
dis[v][0] = dis[u][1]+1;
if(!vis[v]) vis[v] = 1, Q.push(v);
}
}
}
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m, qy; cin>>n>>m>>qy;
for(int i=1; i<=m; i++){
int u, v; cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
for(int i=1; i<=qy; i++){
int u, v, d; cin>>u>>v>>d;
query[u].push_back({i, v, d});
}
for(int u=1; u<=n; u++){
if(!query[u].empty() && g[u].size()){
spfa(u);
for(auto p : query[u]){
if(p.w >= dis[p.v][p.w&1]) ans[p.id] = 1;
}
}
}
for(int i=1; i<=qy; i++)
cout<<(ans[i] ? "TAK" : "NIE")<<"\n";
return 0;
}
百度地图的实时路况
题目大意:给定一张
思路:分治 Floyd。
每次递归时计算
在
由主定理
trick:zzy 称其为时间线段树。撤销
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 305;
int n, dis[N][N];
ll solve(int l, int r){
ll ans = 0;
if(l == r){
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
if(i!=l && j!=r) ans += dis[i][j];
return ans;
}
int tmp[N][N];
int mid = l + (r-l)/2;
memcpy(tmp, dis, sizeof(dis));
// 左不变右变(右边有不取的元素)
for(int k=l; k<=mid; k++){
for(int i=1; i<=n; i++){
if(i==k) continue;
for(int j=1; j<=n; j++){
if(i==j || j==k) continue;
if(dis[i][k]!=-1 && dis[k][j]!=-1){
if(dis[i][j]!=-1) dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
else dis[i][j] = dis[i][k]+dis[k][j];
}
}
}
}
ans += solve(mid+1, r);
memcpy(dis, tmp, sizeof(tmp));
// 右不变同理
for(int k=mid+1; k<=r; k++){
for(int i=1; i<=n; i++){
if(i==k) continue;
for(int j=1; j<=n; j++){
if(i==j || j==k) continue;
if(dis[i][k]!=-1 && dis[k][j]!=-1){
if(dis[i][j]!=-1) dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
else dis[i][j] = dis[i][k]+dis[k][j];
}
}
}
}
ans += solve(l, mid);
memcpy(dis, tmp, sizeof(tmp));
return ans;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
cin>>dis[i][j];
cout<<solve(1, n);
return 0;
}
P1811 最短路
看到题目是求带限制条件的最短路,想到 BFS。
当走到一个点的时候,记录四个值
这样,枚举下一个点
-
是否是一个被禁止的三元组。 -
把双向边拆成两条单向边后,判断这条边有没有被走过。因为这是 BFS,早标记的时间肯定不大于晚标记的时间。
最终利用
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define tiii tuple<int, int, int>
#define pii pair<int, int>
int n, m, k, tail, head;
map<tiii, bool> M;
vector<int> g[3005];
map<pii, int> Enum;
int Ecnt;
bool vis[40005];
struct node{
int pos, lst, dis, pre;
}Q[40005];
void print(int x){
if(Q[x].pre) print(Q[x].pre);
cout<<Q[x].pos<<" ";
}
void bfs(){
head = tail = 1;
Q[1].pos = 1;
while(head <= tail){
node u = Q[head];
for(auto v : g[u.pos]){
if(vis[Enum[{u.pos, v}]] || M[{u.lst, u.pos, v}]) continue;
if(v == n){
cout<<u.dis+1<<"\n";
print(head);
cout<<n;
exit(0);
}
vis[Enum[{u.pos, v}]] = 1;
Q[++tail] = {v, u.pos, u.dis+1, head};
}
head++;
}
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>m>>k;
for(int i=1; i<=m; i++){
int u, v; cin>>u>>v;
g[u].push_back(v);
Enum[{u, v}] = ++Ecnt;
g[v].push_back(u);
Enum[{v, u}] = ++Ecnt;
}
for(int i=1; i<=k; i++){
int a, b, c; cin>>a>>b>>c;
M[{a, b, c}] = 1;
}
bfs();
cout<<"-1";
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探