最短路 算法解析+例题
underthetime · · 算法·理论
最短路
基本
最短路可以认为是图论最基础的内容之一,涉及到的算法也很多。为了方便,假设图有
性质
有几条关于边权为正的图的最短路的性质:
- 任意两点之间的最短路,不会经过重复的点和边。
- 任意两点之间的最短路,结点数不会超过
n ,边数不会超过n-1 。
对于前者,由于边权为正,如果一条最短路需要经过重复的点或边,更优的做法显然是在第一次经过这个点或者边时就按照第二次乃至更多次从这个点或边离开时的路径直接离开,继续往后走。
对于后者,显然如果你经过了
Floyd
一个基于动态规划的 Floyd 都能跑。
令
转移过程中,先从小到大枚举
最后
const int maxn = 305,inf = 1e9; // “无穷大”的值依据题目边权大小而定。
int f[maxn][maxn],w[maxn][maxn],n;
void Floyd() {
for (int i = 1;i <= n;i ++)
for (int j = 1;j <= n;j ++)
if (i == j) f[i][j] = 0;
else f[i][j] = w[i][j]; // i,j不连通则w[i][j]=inf;
for (int k = 1;k <= n;k ++)
for (int i = 1;i <= n;i ++)
for (int j = 1;j <= n;j ++)
f[i][j] = min(f[i][j],f[i][k] + f[k][j]);
}
int Query(int u,int v) { return f[u][v]; }
不过麻雀虽小,五脏俱全。Floyd 能干的事情远不止最短路,最小环、传递闭包、连通性等等都能做,不在此论述。
Dijkstra
好好看看你们有木有拼错这个单词:(
一个基于松弛操作的单源最短路径算法,即以某一个点为起点,可以计算该点到其他所有点的最短路径。但是只适用于非负边权的图。
什么是松弛操作?我们钦定起点为
在 Dijkstra 中,初始化
每次我们从未确定最短路长度的点集中取出一个
为什么每次选取一个
对于
按照上文的选取方法,为什么每个点被用于松弛其他点一次之后就不再用于松弛了?(怎么保证某个点的最短路是否确定?)
这就提到了 Dijkstra 的限制条件,也是 Dijkstra 的复杂度保证——只适用于非负边权的图。
“不再被用于松弛”是因为自身的
如果 Dijkstra 算法
针对“寻找
- 手写堆:每次成功松弛一条边
(u,v) ,就将v 插入二叉堆中(如果v 已经在二叉堆中,直接修改相应元素的权值即可),每次取dis 最小的点直接取出堆顶即可。至多进行m 次插入(或修改)操作,至多n 次删除操作,二叉堆中最多待n 个点,操作复杂度即为O(\log n) ,总时间复杂度O((n+m)\log n)=O(m\log n) 。 STL优先队列:最常用的写法之一。和手写二叉堆类似,但如果同一个点的最短路径被多次松弛,已经在优先队列中但dis 值还未被更新的该点没办法修改其dis 值,只能继续呆在优先队列中,所以队列里最多会有m 个点,每成功一次松弛操作就要新加一个点。时间复杂度O(m\log m) 。
这里采用的是第二个实现方法。
#define mk make_pair
const int maxn = 1e5 + 5, maxm = 2e6 + 5;
// 采用链式前向星存图,邻接表同理。
struct Edge { int v,w,nxt; } e[maxm];
int head[maxn],cnt;
void addEdge(int u,int v,int w) {
e[++ cnt] = Edge{v,w,head[u]};
head[u] = cnt;
}
int dis[maxn]; bool vis[maxn]; // 最短路长度,及标记是否已经被确定最短路。
void Dijkstra(int S) {
memset(dis,0x3f,sizeof(dis)); // 初始化无穷大
dis[S] = 0;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
// x.second表示点的编号,x.first表示x.second在放进优先队列时dis[x.second]的大小。
q.push(mk(0,S));
while (!q.empty()) {
pair<int,int> top = q.top(); q.pop();
int u = top.second;
if (vis[u]) continue; // 已经被确定了,由于优先队列中会有点编号相同的元素特判一下。
vis[u] = true;
for (int i = head[u];i;i = e[i].nxt) {
int v = e[i].v, w = e[i].w;
if (dis[v] > dis[u] + w) { // 松弛操作
dis[v] = dis[u] + w;
if (!vis[v]) q.push(mk(dis[v],v));
}
}
}
}
一定注意只适用于非负边权的图!
BellanFord / SPFA
也许你更多听到的是 SPFA 这个名字。SPFA 算法可以说是对于 BellanFord 算法的一个优化。对于为什么说 SPFA 已经死了,现在你胆敢在没有负权边的图上跑 SPFA,就可以 AFO(
2018 年 7 月 19 日,某位同学在 NOI Day 1 T1 归程 一题里非常熟练地使用了一个广为人知的算法求最短路。
然后呢?
$\text{Ag} \rightarrow \text{Cu}$; 最终,他因此没能与理想的大学达成契约。 小 F 衷心祝愿大家不再重蹈覆辙。
可见出题人们的责任心之强。
回到正题。BellanFord 也是基于松弛操作实现的,但它能跑负边权。相较于 Dijkstra 每个点的出边只会松弛一次,BellanFord 每次都把
这就解决了负权的问题:假设最短路存在(没有负环),那么如果有一个点通过负权边使最短路变短了,那么其出边就可能成功松弛。那么这样
由于当图中每条边都无法被松弛时,
根据开头提到的最短路边数不会超过
因为 BellanFord 能跑负边权,且由于它独特的松弛方法,它可以用来判断是否有负环(最短路是否存在)。上文提到如果最短路存在松弛操作至多进行
所以当松弛操作进行到第
struct Edge { int u, v, w; }; // 存每条边
vector<Edge> mp;
int dis[maxn], u, v, w;
const int inf = 0x3f3f3f3f;
bool BellanFord(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 < mp.size(); j++) {
u = mp[j].u, v = mp[j].v, w = mp[j].w;
if (dis[u] == inf) continue;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
flag = true;
}
}
// 没有可以松弛的边时就停止算法。
if (!flag) break;
}
// 第n轮循环仍然可以松弛时说明s点可以抵达一个负环。
return flag;
}
然后介绍 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()) { // 可以发现和Dijkstra的堆优化版本挺像的,不过Dijkstra中每个点不会被重复松弛。
int u = q.front();
q.pop(); vis[u] = 0;
for (auto V : e[u]) {
int v = V.v, w = V.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) return false;
if (!vis[v]) { q.push(v); vis[v] = 1 };
}
}
}
return true;
}
例题
模板题
- P4779 【模板】单源最短路径(标准版)
再次鞭策SPFA按照上文写就行。
中等题
- P4880 抓住czx 绿题
如果 lty 从起点出发,发现在到达
所以我们可以算出以
最后特判一下 czx 停止瞬移后 lty 才能赶到的情况即可。
#include<bits/stdc++.h>
#define mk make_pair
using namespace std;
const int maxn = 1e5 + 5;
int n,m,b,e,dis[maxn],T,ans = 2e9; bool vis[maxn];
vector<pair<int,int> > mp[maxn];
pair<int,int> czx[maxn];
void addEdge(int u,int v,int w) {
mp[u].push_back(mk(v,w));
mp[v].push_back(mk(u,w));
}
void Dijkstra() {
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
memset(dis,0x3f,sizeof(dis));
dis[b] = 0; q.push(mk(0,b));
while (!q.empty()) {
auto now = q.top(); q.pop();
int u = now.second;
if (vis[u]) continue;
vis[u] = true;
for (auto V : mp[u]) {
int v = V.first, w = V.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (!vis[v]) q.push(mk(dis[v],v));
}
}
}
}
int main() {
scanf("%d%d%d%d",&n,&m,&b,&e);
for (int i = 1,u,v,w;i <= m;i ++) {
scanf("%d%d%d",&u,&v,&w);
addEdge(u,v,w);
}
Dijkstra(); scanf("%d",&T);
for (int i = 1;i <= T;i ++) scanf("%d%d",&czx[i].first,&czx[i].second);
sort(czx + 1,czx + T + 1);
int lstT = 0,lstP = e;
for (int i = 1,t,x;i <= T;i ++) {
t = czx[i].first, x = czx[i].second;
if (dis[lstP] < t) ans = min(ans,max(lstT,dis[lstP]));
lstT = t, lstP = x;
}
ans = min(ans,max(lstT,dis[lstP]));
printf("%d",ans);
return 0;
}
- P4644 [USACO05DEC] Cleaning Shifts S 蓝题
这题有 dp、线段树等其他做法,但这里介绍最短路做法。我都为这种方法震惊
题目要求用若干个区间覆盖完整个大区间,可以抽象理解成把区间当作若干条路径,代价即为边权,最终使得大区间的开头能够走到区间结尾即可。
对于两个区间
如何处理相交区间呢?显然可以用两个相交区间的并覆盖大区间的一个部分,但这么直接建图显然做不到。抽象理解一下,两个相交区间
但这样的复杂度显然不允许(最坏情况下
于是正解思路就出来了:一开始我们连
然后按照上文方法建边,以
#include<bits/stdc++.h>
using namespace std;
const int maxn = 10005,maxm = 1e5 + 5;
vector<pair<int,int> > mp[maxm];
void addEdge(int u,int v,int w) { mp[u].push_back({v,w}); }
int n,s,t,dis[maxm]; bool vis[maxm];
int main() {
scanf("%d%d%d",&n,&s,&t);
for (int i = t + 1;i > s;i --)
addEdge(i,i - 1,0);
for (int i = 1,u,v,w;i <= n;i ++) {
scanf("%d%d%d",&u,&v,&w);
addEdge(u,v + 1,w);
}
// 此题无负权边所以使用Dijkstra。
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
memset(dis,0x3f,sizeof(dis));
dis[s] = 0; q.push({0,s});
while (!q.empty()) {
int u = q.top().second; q.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto V : mp[u]) {
int v = V.first, w = V.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (!vis[v]) q.push({dis[v],v});
}
}
}
if (dis[t + 1] == 0x3f3f3f3f) dis[t + 1] = -1;
printf("%d",dis[t + 1]);
return 0;
}
- P6233 [eJOI2019] Awesome Arrowland Adventure
水蓝题
把每个格子都视作图上的一个点,坐标相邻的点之间的点连边。设从
#include<bits/stdc++.h>
#define mk make_pair
#define type pair<int,pair<int,int> >
using namespace std;
const int maxn = 505;
int n,m,yt[maxn][maxn],dis[maxn][maxn];
char mg[maxn][maxn]; bool vis[maxn][maxn];
vector<pair<pair<int,int>,int> > mp[maxn][maxn];
void addEdge(int ux,int uy,int vx,int vy,int w) {
mp[ux][uy].push_back(mk(mk(vx,vy),w));
}
int getnum(int x,int y) {
char now = mg[x][y];
if (now == 'N') return 0;
if (now == 'E') return 1;
if (now == 'S') return 2;
if (now == 'W') return 3;
return -1;
}
int turn(int now,int tg) {
if (now > tg) return 4 + tg - now;
return tg - now;
}
void Dijkstra() {
priority_queue<type,vector<type>,greater<type> > q;
memset(dis,0x3f,sizeof(dis));
q.push(mk(0,mk(1,1))); dis[1][1] = 0;
while (!q.empty()) {
auto top = q.top(); q.pop();
int ux = top.second.first,uy = top.second.second;
if (vis[ux][uy]) continue;
vis[ux][uy] = true;
for (auto V : mp[ux][uy]) {
int vx = V.first.first,vy = V.first.second,w = V.second;
if (dis[vx][vy] > dis[ux][uy] + w) {
dis[vx][vy] = dis[ux][uy] + w;
if (!vis[vx][vy]) q.push(mk(dis[vx][vy],mk(vx,vy)));
}
}
}
}
int main() {
scanf("%d%d",&n,&m);
for (int i = 1;i <= n;i ++) {
getchar();
for (int j = 1;j <= m;j ++)
mg[i][j] = getchar(),
yt[i][j] = getnum(i,j);
}
for (int i = 1;i <= n;i ++)
for (int j = 1;j <= m;j ++) {
if (yt[i][j] == -1) continue;
if (i - 1 > 0)
addEdge(i,j,i - 1,j,turn(yt[i][j],0));
if (i + 1 <= n)
addEdge(i,j,i + 1,j,turn(yt[i][j],2));
if (j - 1 > 0)
addEdge(i,j,i,j - 1,turn(yt[i][j],3));
if (j + 1 <= m)
addEdge(i,j,i,j + 1,turn(yt[i][j],1));
}
Dijkstra();
if (dis[n][m] == 0x3f3f3f3f) dis[n][m] = -1;
printf("%d",dis[n][m]);
return 0;
}
难题
- P2505 [HAOI2012] 道路 紫题
对于一条边
先判断被最短路经过的边有哪些。Dijkstra 或 SPFA 的松弛操作长这样:
显然在这两种算法结束后,对于一条边
如果我们钦定一个源点 DAG(显然最短路不会走回头路)。
还有一个性质:对于一条最短路
然后就可以在新建的由最短路径组成的图上拓扑排序+动态规划了。设
最终对于一条边
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1505,maxm = 5005,P = 1e9 + 7;
int n,m,cnt,head[maxn],dis[maxn],d[maxn];
bool vis[maxn],is[maxm]; int ans[maxm];
int ans1[maxn],ans2[maxn],topq[maxn],tot;
struct edge {
int nxt,w,u,v;
} e[maxm];
void addEdge(int u,int v,int w) {
e[++ cnt] = edge{head[u],w,u,v};
head[u] = cnt;
}
void spfa(int st) {
memset(dis,0x3f,sizeof(dis));
memset(is,0,sizeof(is));
queue<int> q; q.push(st); dis[st] = 0;
while (!q.empty()) {
int u = q.front(); q.pop(); vis[u] = false;
for (int i = head[u];i;i = e[i].nxt) {
int v = e[i].v, w = e[i].w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (!vis[v]) {
vis[v] = true;
q.push(v);
}
}
}
}
for (int i = 1;i <= m;i ++) {
int u = e[i].u, v = e[i].v, w = e[i].w;
if (dis[v] == dis[u] + w) is[i] = true;
// cout << is[i] << ' ';
}
// putchar('\n');
}
void topo(int st) {
memset(d,0,sizeof(d));
memset(ans1,0,sizeof(ans1));
memset(ans2,0,sizeof(ans2));
queue<int> q; ans1[st] = 1; tot = 0;
for (int i = 1;i <= m;i ++)
if (is[i]) d[e[i].v] ++;
q.push(st); // st为起点,入度一定为0,其他点入读一定不为0(不是起点)
while (!q.empty()) {
int u = q.front(); q.pop(); topq[++ tot] = u;
for (int i = head[u];i;i = e[i].nxt) {
int v = e[i].v;
if (!is[i]) continue;
ans1[v] = (ans1[v] + ans1[u]) % P;
if (--d[v] == 0) q.push(v);
}
}
for (int j = tot;j > 0;j --) {
int u = topq[j]; ans2[u] ++;
for (int i = head[u];i;i = e[i].nxt) {
int v = e[i].v;
if (!is[i]) continue;
ans2[u] = (ans2[u] + ans2[v]) % P;
}
}
}
int main() {
scanf("%d%d",&n,&m);
for (int i = 1,u,v,w;i <= m;i ++) {
scanf("%d%d%d",&u,&v,&w);
addEdge(u,v,w);
}
for (int st = 1;st <= n;st ++) {
spfa(st); topo(st);
for (int i = 1;i <= m;i ++) {
if (is[i])
ans[i] = (ans[i] + (1ll * ans1[e[i].u] * ans2[e[i].v]) % P) % P;
}
}
for (int i = 1;i <= m;i ++)
printf("%d\n",ans[i]);
return 0;
}
- P4473 [国家集训队] 飞飞侠 紫题
暴力建边边数复杂度是
什么是优化建边?就是把原本连的边,在不影响最终答案(比如说拓扑排序时不影响连通性,求最短路时不会出现原图中不存在的最短路长度)的情况下,尽可能减少连边数量或降低连边复杂度。
线段树优化建边,通常用于处理点向区间、区间向点的连边(区间和区间连边情况更加复杂)。具体地,把线段树上的父亲与儿子之间连边权为
对于这一题,我们将每一行都建上一棵线段树(最终为一个森林);建完边后以这三个人分别为起点跑最短路,最后比对一下谁最优即可。具体实现看代码。
#include<bits/stdc++.h>
#define int long long
#define mk make_pair
using namespace std;
const int maxn = 305,maxm = 1e6 + 5,maxM = 2e7 + 5;
int c[maxn][maxn],r[maxn][maxn],n,m;
pair<int,int> ans[maxn]; int p[maxn];
int lson[maxm],rson[maxm],scnt,pos[maxn][maxn],nowl,nowr,st[maxn];
int dis[maxm],Ans[4][4];
struct Edge{ int v,w,nxt; } e[maxM];
int head[maxm],cnt;
void addEdge(int u,int v,int w) {
e[++ cnt] = Edge{v,w,head[u]};
head[u] = cnt;
}
int build(int x,int l,int r,int rt) {
if (l == r) {
pos[x][l] = rt;
return rt;
}
int mid = (l + r) >> 1ll;
lson[rt] = build(x,l,mid,++ scnt);
rson[rt] = build(x,mid + 1,r,++ scnt);
addEdge(rt,lson[rt],0ll);
addEdge(rt,rson[rt],0ll);
return rt;
}
void modify(int u,int l,int r,int rt,int k) {
if (nowl <= l && r <= nowr) {
addEdge(u,rt,k);
return ;
}
int mid = (l + r) >> 1ll;
if (nowl <= mid) modify(u,l,mid,lson[rt],k);
if (mid < nowr) modify(u,mid + 1,r,rson[rt],k);
}
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
void Dijkstra(int s,int id) {
for (int i = 1;i <= scnt;i ++) dis[i] = 1e18;
dis[s] = 0; q.push({0,s});
while (!q.empty()) {
int u = q.top().second, length = q.top().first; q.pop();
if (dis[u] != length) continue;
for (int i = head[u];i;i = e[i].nxt) {
int v = e[i].v, w = e[i].w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.push({dis[v],v});
}
}
}
for (int i = 1;i < 4;i ++) Ans[id][i] = dis[p[i]];
}
signed main() {
scanf("%lld%lld",&n,&m);
for (int i = 1;i <= n;i ++)
st[i] = build(i,1,m,++ scnt);
for (int i = 1;i <= n;i ++)
for (int j = 1;j <= m;j ++)
scanf("%lld",&r[i][j]);
for (int i = 1;i <= n;i ++)
for (int j = 1;j <= m;j ++) {
scanf("%lld",&c[i][j]);
for (int k = max(i - r[i][j],1ll);k <= min(i + r[i][j],n);k ++) {
nowl = max(1ll,j - (r[i][j] - abs(k - i)));
nowr = min(m,j + (r[i][j] - abs(k - i)));
modify(pos[i][j],1,m,st[k],c[i][j]);
}
}
int x,y;
for (int i = 1;i < 4;i ++) {
scanf("%lld%lld",&x,&y);
p[i] = pos[x][y];
}
for (int i = 1;i < 4;i ++) {
Dijkstra(p[i],i);
ans[i].second = i - 1;
}
for (int i = 1;i < 4;i ++)
for (int j = 1;j < 4;j ++)
ans[i].first += Ans[j][i];
sort(ans + 1,ans + 4);
if (ans[1].first >= 1e18) puts("NO");
else printf("%c\n%lld",'X' + ans[1].second,ans[1].first);
return 0;
}