最短路算法
最短路算法
存在最短路的前提
不存在负环。
链接
还是 OIWiki 好啊~~
Floyd 算法
两两间最短路,可判负环。时间复杂度 。
像 DP 的思路一样。
设 表示允许经过 的点, 的最短距离。
的 DP 即可。
按照 的顺序循环,每次与 取 即可。
即
f[k][x][y] = min(f[k - 1][x][y], f[k - 1][x][k] + f[k - 1][k][y]);
因为第一维对结果无影响,我们可以发现数组的第一维是可以省略的,于是可以直接改成 f[x][y] = min(f[x][y], f[x][k]+f[k][y])
。
搬 OIWiki 的代码:
for (k = 1; k <= n; k++) { for (x = 1; x <= n; x++) { for (y = 1; y <= n; y++) { f[k][x][y] = min(f[k - 1][x][y], f[k - 1][x][k] + f[k - 1][k][y]); } } }
综上时间复杂度就是 ,空间复杂度就是 。
Floyd 求图的传递闭包
用 bitset 优化可以实现 的复杂度。
Floyd 判负环
Bellman-Ford 算法
单源最短路,可判负环。时间复杂度 。
共循环 次,每次枚举每一条边进行松弛操作(即对边 对 进行取 操作)。若在某一次没有对任意一条边进行松弛操作,可以直接结束循环。
证明只用循环 次即可求出最短路。
因为第 次操作可以确定最短路经过 条边的结点的 ,因此最坏情况下只需要重复 次即可。
证毕。
然鹅我们一般循环 次,为的是顺便判一下有没有负环(最短路是否存在)。
判负环
若第 次仍然可以进行松弛操作,则存在负环。
Code (显然是从 OIWiki 搬的)
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 也可以用于判断 点是否能抵达一个负环,只需记录最短路经过了多少条边,当经过了至少() 条边时,说明 点可以抵达一个负环。
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; }
Dijkstra 算法
单源最短路。时间复杂度 或 。必须为非负权图。
搬 OIWiki 甚至懒得打字
简单来说,就是每次选一条集合内目前最短的路径,然后扩展一圈,放回集合。
时间复杂度 的。
Code(搬的)
struct edge { int v, w; }; vector<edge> e[maxn]; int dis[maxn], vis[maxn]; void dijkstra(int n, int s) { memset(dis, 63, sizeof(dis)); dis[s] = 0; for (int i = 1; i <= n; i++) { int u = 0, mind = 0x3f3f3f3f; for (int j = 1; j <= n; j++) if (!vis[j] && dis[j] < mind) u = j, mind = dis[j]; vis[u] = true; for (auto ed : e[u]) { int v = ed.v, w = ed.w; if (dis[v] > dis[u] + w) dis[v] = dis[u] + w; } } }
优先队列优化
时间复杂度 。
复杂度易证,略。
终于有我自己写的代码了
#include<bits/stdc++.h> #define ll long long #define pf printf #define sf scanf using namespace std; const int N=1e5+7; int cnt,head[N]; struct edge{ int to,ne,val; } e[N<<1]; void add(int u,int v,int w) { e[++cnt]={v,head[u],w},head[u]=cnt; } int n,m,s,u,v; int w; struct node{ int u,dis; bool operator>(const node &x)const{ return dis>x.dis; } }; priority_queue<node,vector<node>,greater<node> > q; int dis[N]; bool vi[N]; void getans(){ memset(dis,0x3f,sizeof(dis)); dis[s]=0; q.push({s,0}); while(!q.empty()){ int u=q.top().u,d=q.top().dis; q.pop(); if(vi[u]) continue; vi[u]=1; for(int i=head[u];i;i=e[i].ne){ int v=e[i].to,w=e[i].val; if(d+w<dis[v]){ dis[v]=d+w; q.push({v,dis[v]}); } } } } int main(){ sf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++){ sf("%d%d%d",&u,&v,&w); add(u,v,w); } getans(); for(int i=1;i<=n;i++){ pf("%d ",dis[i]); } }
Johnson 全源最短路径算法
……到时候补充。
差分约束系统
其实这才是我今天的正题额。
若对这样一组不等式,我们要求 ,那么:
对于 我们建一条有向边 ,在图上跑单源最短路 SPFA 算法(当然这个例子没有负权,也可以跑 Dijkstra 算法),。
为什么呢?
可以发现,在求最短路的过程中,对于边 总有 ,我们把它叫做三角不等式。即:
这个式子和
是不是很像呢?
因此差分约束系统可以转换成有向图求解。
因为我们的有向图不一定连通,为了方便,我们一般设一个超级源点 ,令 ,连边 ,边权为 ,即增加关系 。这样,我们就可以求出一组解 (毕竟 与所有点都有权为 的边,所以 最大也是 )。
当然,如果差分约束系统里的所有 换成 也可以做,只是把求最短路变成求最长路罢了。
SPFA 最短路 Code
#include<bits/stdc++.h> #define ll long long #define pf printf #define sf scanf using namespace std; const int N=5e3+7; int n,m,u,v,w; int cnt,head[N]; struct edge{ int to,ne,val; }e[N<<1]; int s; void add(int u,int v,int w){ e[++cnt]={v,head[u],w}; head[u]=cnt; } int dis[N]; bool vi[N]; int tot[N]; bool SPFA(){ queue<int> q; q.push(s);vi[s]=1; memset(dis,0x3f,sizeof(dis)); dis[s]=0; while(!q.empty()){ int u=q.front(); q.pop();vi[u]=0; for(int i=head[u];i;i=e[i].ne){ int v=e[i].to,w=e[i].val; if(dis[u]+w<dis[v]){ dis[v]=dis[u]+w; tot[v]=tot[u]+1; if(tot[v]>n){//注意因为加入了超级源点,因此一共有 n+1 个点,最短路包含恰好 n 条边是可以的。 return 0; } if(!vi[v]) q.push(v); vi[v]=1; } } } return 1; } int main(){ sf("%d%d",&n,&m); s=n+1; for(int i=1;i<=n;i++) add(s,i,0); for(int i=1;i<=m;i++){ sf("%d%d%d",&v,&u,&w); add(u,v,w); } if(SPFA()) for(int i=1;i<=n;i++){ pf("%d ",dis[i]); } else pf("NO\n"); }
同余最短路
简介
来自 OI Wiki,讲得好,就搬上来了。
给定 个整数,每个数字可以使用多次,判定能拼出哪些数字。
其中给定数字的值域 使得时间复杂度主要是 的。
如果我们只有一个数字 ,那么我们可以拼出 。
如果再来一个 ,我们就可以拼出 。
我们将 视为同余系的模数,认为只要有 我们就可以拼出 了。因此我们需要求出只使用 ,在模 意义下可以到达那些 。
对于每个 ,我们连一条 的边。边权为 。对于除了 的 个数皆是如此。
这样我们从 开始跑最短路。 就表示我可以走到的最小的 的数字是什么,此后的 都是可以走到的。
经验
模板题:P3403 跳楼机
把 减一,当做从第零层开始会更加方便一些。
以 为模数,求该同余系意义下的同余最短路。时间复杂度 。
同样很板子。有 个数字,时间复杂度 。
本文来自博客园,作者:wing_heart,转载请注明原文链接:https://www.cnblogs.com/wingheart/p/18357725
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】