前言 这个本来应该在从外地学习回来之后就因该写的,但是只写了一半,不过看看自己以前写的,果然还是理解太浅了;
正文 也许有很多人觉得最短路就是那几个算法,不过也确实,但是我们的难点主要在建图上。
但是既然是基础,那么就应该好好掌握做法,多敲几遍就好了。
最短路主要的几个算法有 $\text{Dijkstra,floyd,Bellman-ford}$ 和$\text{SPFA}$,而SPFA是Bellman-ford的队列优化,所以一般用SPFA算法。
Floyed 我们先来看floyd,这是一个 $O(n^3)$ 算法,在大部分的题中是会超时的,但是我们在一些题中会发现一些巧妙地运用,之后我们会见到
模板题: 牛大赛Cow Contest
FJ的N(1 <= N <= 100)头奶牛们最近参加了场程序设计竞赛:)。在赛场上,奶牛们按1..N依次编号。每头奶牛的编程能力不尽相同,并且没有哪两头奶牛的水平不相上下,也就是说,奶牛们的编程能力有明确的排名。 整个比赛被分成了若干轮,每一轮是两头指定编号的奶牛的对决。如果编号为A的奶牛的编程能力强于编号为B的奶牛(1 <= A <= N; 1 <= B <= N; A != B) ,那么她们的对决中,编号为A的奶牛总是能胜出。 FJ想知道奶牛们编程能力的具体排名,于是他找来了奶牛们所有 M(1 <= M <= 4,500)轮比赛的结果,希望你能根据这些信息,推断出尽可能多的奶牛的编程能力排名。比赛结果保证不会自相矛盾。
这道题就是一个简单地转换,我们只要看可以判断出的前面的牛+后面的牛=n-1即可,而可以前面的牛和后面的牛可以通过建图,跑最短路,看是否能够到达即可,所以说我们直接来看板子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <cstdio> #include <iostream> #include <cmath> using namespace std ;int n,m,e[107 ][107 ],ans;int main () { scanf ("%d%d" ,&n,&m); for (int i=1 ,a,b;i<=m;i++){ scanf ("%d%d" ,&a,&b); e[a][b]=1 ; } for (int k=1 ;k<=n;k++){ for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++){ if (i!=j&&e[i][k]&&e[k][j]) e[i][j]=1 ; } } } for (int i=1 ;i<=n;i++){ int r=0 ; for (int j=1 ;j<=n;j++){ if (i==j) continue ; if (e[i][j]||e[j][i]) r++; else break ; } if (r==n-1 ) ans++; } printf ("%d" ,ans); }
分治Floyed(update) $\text{Floyed}$ 的本质是动规,而且能够看出,最外层的枚举,即经过点的枚举,可以分开枚举,我们用这种技巧可以得到不经过某个点两点之前最短路,我之前的一次模拟赛就考到了这道题;
与其类似的是 2016计蒜之道复赛A[百度地图的实时路况] ;
于是我将这道题加入,深刻理解一下Floyed的插点枚举;
给你一个 $n$ 个点的图,定义 $d(x,y,z)$ 是不经过点 $y$ , $x$ 到 $z$ 最短路径的长度,求
考虑Floyed的插点枚举,那么我们分治使枚举分开,并且有效地利用已经枚举的信息.
流程即:枚举经过的点,而分治可以让我们分开枚举,当分治到一个点时,这个点时没有枚举的,统计答案;
模拟: 在分治区间 $[L,R]$ 时,此时在此区间中的点我们还没有枚举过,首先记录初始 $dis$ ,以便我们之后使用,之后将 $[L,mid]$ 的点枚举,去分治 $[mid+1,R]$ 中的点,之后递归回来时,消除枚举区间 $[L,mid]$ 对 $dis$ 的影响,即恢复至之前记录的值,再枚举区间 $[mid+1,R]$ ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include <bits/stdc++.h> #define maxn 307 #define ll long long using namespace std ;int n,m,dis[maxn][maxn];template <typename type_of_scan>inline void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } template <typename type_of_print>inline void print (type_of_print x) { if (x>9 ) print(x/10 ); putchar (x%10 +'0' ); } ll solve (int l,int r) { ll ans=0 ; if (l==r){ for (int i=1 ;i<=n;i++){ if (i==l) continue ; for (int j=1 ;j<=n;j++){ if (j==l) continue ; ans+=1l l*dis[i][j]; } } return ans; } int mid=l+r>>1 ,other[maxn][maxn]; memcpy (other,dis,sizeof other); for (int k=l;k<=mid;k++){ for (int i=1 ;i<=n;i++){ if (dis[i][k]==-1 ) continue ; for (int j=1 ;j<=n;j++){ if (i==j) continue ; if (dis[k][j]==-1 ) continue ; if (~dis[i][j]) 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,other,sizeof dis); for (int k=mid+1 ;k<=r;k++){ for (int i=1 ;i<=n;i++){ if (dis[i][k]==-1 ) continue ; for (int j=1 ;j<=n;j++){ if (i==j) continue ; if (dis[k][j]==-1 ) continue ; if (~dis[i][j]) 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); return ans; } int main () { freopen("sum.in" ,"r" ,stdin ); freopen("sum.out" ,"w" ,stdout ); scan(n); for (int i=1 ;i<=n;i++) for (int j=1 ;j<=n;j++) scan(dis[i][j]); printf ("%lld\n" ,solve(1 ,n)); return 0 ; }
(注:这是我考试时的代码,题面略有不同,如不连通时最短路为-1,请注意);
bitset优化Floyed(update) bitset是一个存0/1串的类似数组的东西,但是其类似于二进制运算,速度快;
例题
度量一个有向图联通情况的一个指标是连通数,指图中可达顶点对个的个数。
给一个 $n(n<=2000)$ 个节点的连通图,求连通数;
直接跑Floyed是绝对会超时的,但是bitset可以在其中优化转移,这一步:
1 dis[i][j]|=dis[i][k]|dis[k][j]
可以转化成这一步
1 dis[i]|=dis[k](dis[i][k]==true )
这应该就很显然了;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <bits/stdc++.h> #define maxn 2007 using namespace std ;int n,ans;bitset <maxn>a[maxn];template <typename type_of_scan>inline void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } template <typename top,typename ... tops>inline void scan (top &x,tops&... X) { scan(x),scan(X...); } int main () { scan(n); for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++){ char s=getchar(); while (s!='1' &&s!='0' ) s=getchar(); a[i][j]=s-'0' ; } a[i][i]=1 ; } for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=n;j++){ if (a[j][i]) a[j]|=a[i]; } } for (int i=1 ;i<=n;i++) ans+=a[i].count(); printf ("%d\n" ,ans); }
Dijkstra 由于原来的Dijkstra太慢,就算加上链式前向星,也不能优化多少;
所以我们直接来看堆优化的Dijstra;
Code(Dijkstra) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <bits/stdc++.h> #define allom 100007 #define INF 2147483647; using namespace std ;int s,n,m,cent=1 ;priority_queue<pair<int ,int >,vector <pair<int ,int > >,greater<pair<int ,int > > >q; struct node {int next,to,w;}edge[500001 ];int vis[allom],head[500001 ],dis[allom];template <typename type_of_scan>void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } void add (int a,int b,int w) { edge[cent].next=head[a]; edge[cent].to=b; edge[cent].w=w; head[a]=cent; cent++; } void Dijkstra (int s) { dis[s]=0 ; q.push(make_pair(dis[s],s)); while (!q.empty()){ int x=q.top().second;q.pop(); if (vis[x]) continue ;vis[x]=1 ; for (int i=head[x];i;i=edge[i].next){ int k=edge[i].to; if (dis[k]>edge[i].w+dis[x]){ dis[k]=edge[i].w+dis[x]; q.push(make_pair(dis[k],k)); } } } for (int f=1 ;f<=n;f++) printf ("%d " ,dis[f]); } int main () { scan(n),scan(m),scan(s); for (int i=1 ;i<=n;i++) dis[i]=INF; for (int i=1 ,a,b,c;i<=m;i++){ scan(a),scan(b),scan(c); add(a,b,c); } Dijkstra(s); return 0 ; }
Code(SPFA) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <bits/stdc++.h> #define allom 100007 #define INF 0x3f3f3f using namespace std ;struct node { int next,to,w; }edge[500005 ]; int n,m,s,cent,head[500005 ],vis[allom];int dis[allom];queue <int >q;void add (int u,int v,int w) { edge[++cent]=(node){head[u],v,w};head[u]=cent; } void SPFA (int s) { for (int i=1 ;i<=n;i++) dis[i]=INF; dis[s]=0 ; q.push(s),vis[s]=1 ; while (!q.empty()){ int u=q.front(); q.pop(),vis[u]=0 ; for (int i=head[u],k;i;i=edge[i].next){ k=edge[i].to; if (dis[k]>dis[u]+edge[i].w){ dis[k]=dis[u]+edge[i].w; if (!vis[k]) q.push(k),vis[k]=1 ; } } } } int main () { scanf ("%d%d%d" ,&n,&m,&s); for (int i=1 ,a,b,c;i<=m;i++){ scanf ("%d%d%d" ,&a,&b,&c); add(a,b,c); } SPFA(s); for (int i=1 ;i<=n;i++){ dis[i] == INF ? printf ("2147483647" ) : printf ("%d" ,dis[i]); if (i!=n) printf (" " ); } }
当然,这个只是一个模板,其中SPFA在稠密图中会退变为O(mn)算法,此时Dijkstra的优势就出来了。
注意:Dijkstra一般不去跑负权值图,如果想要去跑了话,要add许多东西,这不在我们的讨论范围之内;
有人说SPFA它死了,但是它还没有凉透,在判断负环中仍有着不可替代的作用,而且在大部分图是稀疏图时,SPFA是跑得飞快,判负环时dfs-Floyed更快。
所以根据数据范围自行选择,一般在遇见边大于200000而没有缩点时,可以果断扔SPFA了;
然后我们主要介绍几个最短路的类型:
1.分层图 分层图主要用于一些较为特殊的题目,比如说
冻结
给一张 $n$ 个点 $m$ 条边的无向图,每个边有一个权值 $w$ ,有 $k$ 次机会将一个边的边权变成 $w/2$ ,求从 $1$ 到 $n$ 的最短路;
分层图模版题,我们将使用次数看成一个状态,分别建 $k$ 个图,相邻两个图之间连接用权值的一半即可;
update:好像可以设每个点的状态,最短路时转移即可;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <iostream> #include <cmath> #include <cstdio> #include <algorithm> #include <cstring> #include <queue> #include <utility> #include <map> #include <cstdlib> #define maxn 10000007 using namespace std ;int n,m,k,head[maxn],dis[maxn],vis[maxn],cent,ans=2147483647 ;struct node {int next,to,w;}edge[2 *maxn];priority_queue<pair<int ,int >,vector <pair<int ,int > >,greater<pair<int ,int > > >q; int point (int a,int b) {return a+b*n;} void add (int u,int v,int w) { edge[++cent]=(node){head[u],v,w};head[u]=cent; } template <typename type_of_scan>inline void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } void Dijkstra (int s) { for (int i=1 ;i<=n*k+n+1 ;i++) dis[i]=124563335 ; dis[s]=0 ; q.push(make_pair(dis[s],s)); while (!q.empty()){ int x=q.top().second;q.pop(); if (vis[x]) continue ; vis[x]=1 ; for (int i=head[x];i;i=edge[i].next){ int y=edge[i].to; if (dis[y]>dis[x]+edge[i].w){ dis[y]=dis[x]+edge[i].w; q.push(make_pair(dis[y],y)); } } } } int main () { scan(n),scan(m),scan(k); for (int i=1 ,a,b,w;i<=m;i++){ scan(a),scan(b),scan(w); for (int j=0 ;j<=k;j++) add(point(a,j),point(b,j),w),add(point(b,j),point(a,j),w); for (int j=0 ;j<=k;j++) add(point(a,j),point(b,j+1 ),w/2 ),add(point(b,j),point(a,j+1 ),w/2 ); } Dijkstra(1 ); for (int i=0 ;i<=k;i++) ans=min(ans,dis[point(n,i)]); printf ("%d" ,ans); }
2.同余建图 这道题算是思路清奇,于是就将其展示出来(当时就是这道题导致整个博客咕咕了)
有一个 $h$ 层的楼房,一个跳楼机可以选择跳 $x,y,z$ 层或者跳回一楼,求能跳到的楼层数;
这道题思路并不好想,说是同余,主要是因为统计答案时与跑最短路优化数组大小的方式罢了;
先考虑如何统计答案,如果我们知道一个数 $num$ 是由 $y,z$ 构成的,即 $num=ay+b z$ ,那么如果加上 $x$ ,它能到达的楼层数为 $(h-num)/x+1$ 其中 $+1$ 是因为要统计不加 $x$ 时的答案;
那么如果我们能找到这些数,再去统计不就好了吗,但是会有重复,而且数字太过庞大,无法完全跑出,那么考虑什么时候回重复计数,即当 $num1=num2+ax$ ,此时 $num1$ 的贡献已经被 $num2$ 统计过,再观察两个数字,发现 $num1 \% x=num2 \% x$ ,那么我们只要找到余数相同中最小值即可,余数共有 $x$ 种;
设计转移方程: 设 $f[i]$ 指余数为 $i$ 时最小楼层数,那么转移有两种方式;
$f[(i+y)\%x]=min(f[i]+y,f[(i+y)\%x])$
$f[(i+z)\%x]=min(f[i]+z,f[(i+y)\%x])$
我们可以用最短路来优化类似于点 $i$ 与点 $(i+y)\%x$ 连边,代价为 $y$ ;
讲到这应该就够了,这里附上的代码是我以前写的,有点丑,但是能看懂;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <bits/stdc++.h> #define INF 1000000000000000LL #define ll long long #define maxn 100007 using namespace std ;ll h,dis[maxn]; int a[4 ],x,y,z,mn,vis[maxn];int min (int x,int y) {return x<y?x:y;}queue <int >q;const int n=3 ;void SPFA () { memset (dis,0x3f3f3f3f ,sizeof (dis)); vis[1 ]=1 ;q.push(1 );dis[1 ]=1 ; while (!q.empty()){ int x=q.front();q.pop(); for (int i=1 ;i<=n;i++) if (a[i]!=mn){ int y=(a[i]+x)%mn; if (dis[y]>dis[x]+a[i]){ dis[y]=dis[x]+a[i]; if (!vis[y]) vis[y]=1 ,q.push(y); } } vis[x]=0 ; } } ll query (ll x) { ll ans=0 ; for (int i=0 ;i<mn;i++){ if (x>=dis[i]) ans+=(x-dis[i])/mn+1 ; } return ans; } int main () { scanf ("%lld%d%d%d" ,&h,&x,&y,&z); mn=min(x,min(y,z)); if (x==1 ||y==1 ||z==1 ){ printf ("%lld" ,h); return 0 ; } a[1 ]=x,a[2 ]=y,a[3 ]=z; SPFA(); printf ("%lld" ,query(h)); return 0 ; }
3.另类的建图方式 线段树,树剖优化建图,倍增优化建图,然后再去跑最短路,但这个我并不是非常想说,先咕咕了;
TO BE CONTINUE