最短路合集
一.一些定义
本篇纯属不想打代码之余搞出来的东西,可能不会有什么参考意义吧。这些定义也是我随便取的名字,也不知道有没有专业的叫法。并且所有的最短路求都是用的 (Dijkstra) 打的,不会用过世的算法。
最短路树
从一个点 s 出发到其他所有点的最短路所构成的树,此时每一个点只贡献一条链。大概的构思来自于 D算法中每个点只出队入队一次,相当于拿它去更新剩余点的时候自身的最短路就已经确定了,这条最短路有且只有一个前驱点,此时记录前驱(或者建树)就行。然后可以运用优先队列中 pair 的排序法则结合转移的边的编号做一些骚事情,后面有例题。
最短路图
从 s 到 t 的最短路图由所有 s 到 t 的最短路所组成。性质是一个 DAG 。当然也可以做成一个无向图。建图的方法是分别从 s,t 都跑一边最短路,枚举每条边 ((u,v)),看是否满足 (dis_{s,u}+w_{u,v}+dis_{v,t}=dis_{s,t}),满足就连边。
二.一些变式
1.元
求 s 到其它点的最短路。
D算法即可。
2.边数
求解 s 到其他点的边数 最大/最小 的最短路,即在单纯的距离最短路上,对于距离相同的做一个边数的约束。也就是魔改一下转移方程的问题,记录一个经过边数就好。略微拓展可以做出最多经过 k 条边的最短路。
3.计数
s 到其他点的最短路有多少条,也是魔改方程,很简单就不多讲,挂两个例题。最短路计数,路径统计
4.严格次短路
s 到 t 的严格次短路。这个还是相当于魔改方程,只是对于每一个维护一个最短与次短,放上核心代码以及例题
int u=q.top().second.second;
q.pop();
if(vis[u]==2)continue;
vis[u]++;
for(register int i=head[u];i;i=ne[i]){
int k=fi[u]+dis[i],v=to[i],fl=0;
if(k<fi[v])se[v]=fi[v],fi[v]=k,fl=1;
else if(k>fi[v]&&k<se[v])se[v]=k,fl=1;
if(se[v]>dis[i]+se[u])se[v]=dis[i]+se[u],fl=1;
if(fl)q.push(make_pair(-fi[v],make_pair(-se[v],v)));
}
由于每个点只出入堆一次只能算出最短路,但是最短路会影响其他的次短路,所以只出入堆一次不能将两个都算出来,于是改为出入堆两次就行。
5.最短路边
一个边与 s 到 t 的最短路上的边一共有三种关系,一定是,可能是,不是,有两种做法。
第一个做法
由于关联到所有在最短路上的边,于是建一个最短路图,并把它建成无向的,去上边找桥边。不在图上就不是,在图上但不是桥边就是可能,剩下的就是一定是。这个应该很裸。
第二个做法
还是建最短路图,但是在上面跑一个 DP,记录一个点到 s 和 t 的方案数,不妨记为 (f_{s,i},f_{i,t}) ,那么对于一个在最短路图上面的边 ((u,v)) 若满足 (f_{s,u}*f_{v,t}=f_{s,t}) 便是一定,剩下的类推。主要是用的乘法原理,但是很多时候容易乘崩,于是多选几个模数吧。
例题与核心代码
链接,这道题只用知道关系后与最短路做差就行。
const int MAXN=200005;
int n,m,s,e;
int head[3][MAXN],ne[3][MAXN],to[3][MAXN],w[3][MAXN],tot[3],fr[3][MAXN];
inline void add(int alf,int x,int y,int z){
w[alf][++tot[alf]]=z;
fr[alf][tot[alf]]=x;
to[alf][tot[alf]]=y;
ne[alf][tot[alf]]=head[alf][x];
head[alf][x]=tot[alf];
}
long long dis[2][MAXN];
bool vis[2][MAXN];
priority_queue<pair<long long,int> > q[2];
inline void di(int alf,int x){
dis[alf][x]=0;
q[alf].push(make_pair(0,x));
while(!q[alf].empty()){
int u=q[alf].top().second;
q[alf].pop();
if(vis[alf][u])continue;
vis[alf][u]=1;
for(register int i=head[alf][u];i;i=ne[alf][i]){
int v=to[alf][i];
if(dis[alf][v]>dis[alf][u]+w[alf][i]){
dis[alf][v]=dis[alf][u]+w[alf][i];
q[alf].push(make_pair(-dis[alf][v],v));
}
}
}
}
int dfn[MAXN],low[MAXN],date;
bool is[MAXN];
void tarjan(int x,int k){
dfn[x]=low[x]=++date;
for(register int i=head[2][x];i;i=ne[2][i]){
int v=to[2][i];
if(!dfn[v]){
tarjan(v,i);
low[x]=min(low[x],low[v]);
if(low[v]>dfn[x])is[w[2][i]]=1;
}
else if(i!=(k^1))low[x]=min(low[x],dfn[v]);
}
}
int main(){
n=read();m=read();s=read();e=read();
for(register int i=1,x,y,z;i<=m;++i){
x=read();y=read();z=read();
add(0,x,y,z),add(1,y,x,z);
}
memset(dis,0x3f,sizeof(dis));
di(0,s);
di(1,e);
++tot[2];
for(register int i=1;i<=n;++i)
for(register int j=head[0][i];j;j=ne[0][j])
if(dis[0][e]==dis[0][i]+w[0][j]+dis[1][to[0][j]])
add(2,i,to[0][j],j),add(2,to[0][j],i,j);
tarjan(s,0);
for(register int i=1;i<=tot[0];++i){
long long u=dis[0][e]-1-dis[0][fr[0][i]]-dis[1][to[0][i]];
if(is[i])printf("YES
");
else if(u>0)printf("CAN %lld
",w[0][i]-u);
else printf("NO
");
}
return 0;
}
6.最小环
就是要求最小环,分为有向图和无向图
有向图
采用 (Floyd) 一开始将自己到自己的值设为正无穷,然后直接跑,最后自己到自己冲就行
无向图
同样是采用 (Floyd) 但是并不是单纯的直接跑,根据 DP 的顺序来看,(Floyd) 的真意可以是以前 K 个点中转的最短路,换而言之,第一维是在不断地加中转点,所以每次加入中转点时,一边计算最短路,一边用 k 连接这个最短路的左右两个端点,转移为 (a_{ki}+a_{kj}+dis_{ij}) 这里的 a 是指的原本就有的边,而非求出来的最短路,用 (dis_{ki}+dis_{kj}+dis_{ij}) 的写法是错误的,因为 (dis_{ki}) 所代表的路径是可能包含 (dis_{ij}) 的,如此便构不成环了。
至于无向图为什么不能简单的自己到自己,那是因为通常的无向图建边都是双向的,也就是一条边等效于一条环。
例题与代码
第一个例题是板子。
for(int k=1;k<=n;++k){
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
if(i<j&&j<k&&a[i][k]<a[0][0]&&a[k][j]<a[0][0]&&i!=j&&j!=k)
ans=min(ans,dp[i][j]+a[i][k]+a[j][k]);
if(dp[i][k]<dp[0][0]&&dp[k][j]<dp[0][0])
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
}
}
}
第二个例题则是对于 (Floyd) 的本质理解。一起放在这里了,本质就是不断地加中转点,未被拿去中转的就不会经过他。以下给出代码。
for(i=0;i<n;++i)dp[i][i]=0,t[i]=read();
for(i=1;i<=m;++i){
x=read();y=read();d=read();
dp[x][y]=dp[y][x]=min(dp[x][y],d);
}
Q=read();
for(int I=1,T;I<=Q;++I){
x=read();y=read();T=read();
for(;t[la]<=T&&la<n;++la)
for(i=0;i<n;++i)
for(j=0;j<n;++j)dp[i][j]=min(dp[i][j],dp[i][la]+dp[la][j]);
if(la<=x||la<=y||dp[x][y]==dp[n+1][n+1])printf("-1
");
else printf("%d
",dp[x][y]);
}
7.(Floyd) 的扩展
说是 (Floyd) 其实也不是,只是写到了就顺便写一些。
问题是求解用 k 步从 s 到 t 的方案数。可以不怎么思考的写出方程 (f_{k,i,j}=sumlimits_{kin V}f_{k-1,i,k}*f_{1,k,j}),然后发现这个真的很像矩阵的乘法,即将上述抽象成 (f_k=f_{k-1}*f_1) 然后归纳一下得到 (f_k=f_1^k).且 (f_1) 恰为一个邻接矩阵(仅表示是否相连)。
一些例题
第一个例题,发现长度在 0~9 之间,于是暴力拆点,一个变 10 个,只有一个主点,具体细节就不讲述了,跟文章没什么大关系,爱看看。
const int mod=2009;
int n,t;
int jz[100][100],ans[100][100],res[100][100];
void mul(int a[100][100],int b[100][100],int c[100][100]){
memset(res,0,sizeof(res));
for(int i=1;i<=9*n;++i){
for(int j=1;j<=9*n;++j){
for(int k=1;k<=9*n;++k)res[i][j]=(res[i][j]+a[i][k]*b[k][j])%mod;
}
}
for(int i=1;i<=9*n;++i){
for(int j=1;j<=9*n;++j)c[i][j]=res[i][j];
}
}
void jzksm(int a[100][100],int k){
while(k){
if(k&1)mul(ans,a,ans);
k>>=1;
mul(a,a,a);
}
}
int main(){
n=read();t=read();
for(int i=1,st;i<=n;++i){
st=(i-1)*9;
for(int j=1;j<=8;++j)jz[st+j][st+j+1]=1;
for(int j=1,x;j<=n;++j){
scanf("%1d",&x);
if(x)jz[st+x][9*(j-1)+1]=1;
}
}
for(int i=1;i<=9*n;++i)ans[i][i]=1;
jzksm(jz,t);
printf("%d",ans[1][9*(n-1)+1]);
return 0;
}
第二个例题,倒是不用管边权了,但是不能马上回手掏,于是采用点边互换?大意是若有边 (a(x-y),b(y-z)) 将 a,b 连边,那么只要一个无向图拆出来的两个边不连就行。用一个虚点连起点的边会方便计算答案,当然也可以记录下来再挨个加起来。
const int mod=45989;
int n,m,t,s,e,tot,u[125],v[125],alf;
int jz[125][125],ans[125][125],res[125][125];
void mul(int a[125][125],int b[125][125],int c[125][125]){
memset(res,0,sizeof(res));
for(int i=1;i<=tot;++i){
for(int j=1;j<=tot;++j){
for(int k=1;k<=tot;++k)res[i][j]=(res[i][j]+a[i][k]*b[k][j])%mod;
}
}
for(int i=1;i<=tot;++i){
for(int j=1;j<=tot;++j)c[i][j]=res[i][j];
}
}
void jzksm(int a[125][125],int k){
while(k){
if(k&1)mul(ans,a,ans);
k>>=1;
mul(a,a,a);
}
}
int main(){
n=read();m=read();t=read();s=read();e=read();
s++,e++;
u[++tot]=0,v[tot]=s;
for(int i=1,x,y;i<=m;++i){
x=read();y=read();
x++;y++;
u[++tot]=x,v[tot]=y;
u[++tot]=y,v[tot]=x;
}
for(int i=1;i<=tot;++i){
ans[i][i]=1;
for(int j=1;j<=tot;++j){
if(i!=j&&i!=(j^1)&&v[i]==u[j])jz[i][j]=1;
}
}
jzksm(jz,t);
for(int i=1;i<=tot;++i)
if(v[i]==e)alf=(alf+ans[1][i])%mod;
printf("%d",alf);
return 0;
}
8.加边最短路
在没有边的两点间加边使得最短路不变的方案数。题目连接,反正蛮水的,两遍最短路,(n^2)枚举就能过,代码不给。
另一个是比较重要的,单位权无向图,问删除每条边后点 1 到其余所有点最短路是否最多加 1。需要用到 BFS树,很显然的是在 BFS树上的深度和最短路直接挂钩,然后考虑只跨一层和同层的非树边是否存在,没找到例题。
9.删边最短路
这个,有亿点点复杂。求解断掉询问的一条边后的最短路。首先若是完全不同的最短路径(指 s 到 t ) 不止一条,随便断,都不会变的。如此考虑最短路径只有一条的情况。在我的想象中,是两棵最短路树横向插在了一起,共有一条链(最短路径),当然代码不这么写XD,若是不是这条链上的边断了,没有关系的,不会变。
考虑若是链上的一条边 ((u,v)) 断掉了,使得最短路经过了一个原本不在链上的 ((i,j)) 那么此时的最短路长度必然为 (dis_{si}+w_{ij}+dis_{jt}), 这个距离所代表了一个链,而我们期望这个链与原最短链在开始和结尾各重叠了一部分,称之为 (L,R),当然是可以不重叠的,但是在这个 (L,R) 中的边,断掉了之后是不能让 ((i,j)) 做这个最短路的,于是能让 ((i,j)) 成为最短路的只可能在最短链上除去(L,R)的部分中,可喜的是这一部分是连续的。于是枚举每一个不在最短路径链上的边,让它对相应的部分更新,使用线段树维护一个 min 值。
当然具体的细节也有很多,先跑一遍,对最短链上的边编号过后,再跑两边,建出那两棵树的同时对于树上的每一个点进行一个对应区间的左右端点传递。不详细讲,放代码。例题一,例题二,都是大同小异的,这里给出例题一的代码。
const int MAXN=100001;
int n,m;
int head[MAXN],fr[MAXN<<2],ne[MAXN<<2],to[MAXN<<2],w[MAXN<<2],tot;
inline void add(int x,int y,int z){
w[++tot]=z,to[tot]=y,ne[tot]=head[x],head[x]=tot,fr[tot]=x;
}
int pre[MAXN],dis[3][MAXN],l[MAXN],r[MAXN];
priority_queue<pair<int,int> > q;
bool vis[MAXN],road[MAXN<<2];
inline void di(int alf,int s){
memset(dis[alf],0x3f,sizeof(dis[alf]));
memset(vis,0,sizeof(vis));
dis[alf][s]=0;
q.push(make_pair(0,-1));
while(!q.empty()){
int u,qwq=q.top().second;
q.pop();
if(qwq==-1)u=s;
else u=to[qwq];
if(vis[u])continue;
vis[u]=1;
if(qwq!=-1){
if(alf==0)pre[u]=qwq;
else if(alf==1&&!road[qwq])l[u]=l[fr[qwq]];
else if(alf==2&&!road[qwq])r[u]=r[fr[qwq]];
}
for(int i=head[u];i;i=ne[i]){
int v=to[i];
if(dis[alf][v]>dis[alf][u]+w[i]){
dis[alf][v]=dis[alf][u]+w[i];
q.push(make_pair(-dis[alf][v],i));
}
}
}
}
int minn[MAXN<<3];
void change(int k,int l,int r,int z,int y,int len){
if(z==-1||y==-1||l>y||r<z||z>y)return ;
if(l>=z&&r<=y){minn[k]=min(minn[k],len);return ;}
int mid=(l+r)>>1;
change(k<<1,l,mid,z,y,len);
change((k<<1)|1,mid+1,r,z,y,len);
}
int hp=-1,num;
void Down(int k,int l,int r){
if(l==r){
if(minn[k]>hp)hp=minn[k],num=1;
else if(minn[k]==hp)num++;
return ;
}
int mid=(l+r)>>1;
minn[k<<1]=min(minn[k<<1],minn[k]);
Down(k<<1,l,mid);
minn[(k<<1)+1]=min(minn[(k<<1)+1],minn[k]);
Down((k<<1)+1,mid+1,r);
}
int main(){
n=read();m=read();
for(int i=1,s,t,c;i<=m;++i){
s=read();t=read();c=read();
add(s,t,c);add(t,s,c);
}
di(0,1);
int now=n,k=-1;
while(now!=1&&pre[now]){
road[pre[now]]=road[dzx(pre[now])]=1;
r[to[pre[now]]]=++k;
l[to[pre[now]]]=k-1;
now=fr[pre[now]];
}
r[1]=-1,l[1]=k;
di(1,1);
di(2,n);
memset(minn,0x3f,sizeof(minn));
for(int i=1;i<=n;++i)
for(int j=head[i],v;j;j=ne[j])
if(dis[0][i]<=dis[0][(v=to[j])]&&!road[j])
change(1,0,k,r[v],l[i],w[j]+dis[0][i]+dis[2][v]);
Down(1,0,k);
if(hp==dis[0][n])num=m;
printf("%d %d",hp,num);
return 0;
}
三.杂题浅讲
题一
链接,求的是边权异或最小,且所有的环的异或都是0,可以显然得出任意两点间的答案不管怎么走都不会变。放代码。
int n,m,q,f[100005];
int get(int x){
if(f[x]==x)return x;
return (f[x]=get(f[x]));
}
int head[100005],ne[200005],to[200005],dis[200005],tot;
inline void add(int x,int y,int z){
dis[++tot]=z,ne[tot]=head[x],head[x]=tot,to[tot]=y;
}
int xo[100005];
void dfs(int x,int pre){
for(register int i=head[x];i;i=ne[i]){
int v=to[i];
if(v==pre)continue;
xo[v]=dis[i]^xo[x];
dfs(v,x);
}
}
int main(){
n=read();m=read();q=read();
for(register int i=1;i<=n;++i)f[i]=i;
for(register int i=1,x,y,z,xx,yy;i<=m;++i){
x=read();y=read();z=read();
xx=get(x);yy=get(y);
if(xx!=yy){
f[xx]=yy;
add(x,y,z);
add(y,x,z);
}
}
dfs(1,0);
for(register int i=1,x,y;i<=q;++i){
x=read();y=read();
printf("%d
",xo[x]^xo[y]);
}
return 0;
}
题二
链接,求两个最短路图的并集上最长链,DAG拓扑瞎写。
const int MAXN=400005;
int n,m,s1,e1,s2,e2;
long long dis[10][MAXN];
bool vis[MAXN];
priority_queue<pair<int,int> > q;
inline void di(int k,int alf,int x){
memset(vis,0,sizeof(vis));
dis[k][x]=0;
q.push(make_pair(0,x));
while(!q.empty()){
int u=q.top().second;
q.pop();
if(vis[u])continue;
vis[u]=1;
for(register int i=head[alf][u];i;i=ne[alf][i]){
int v=to[alf][i];
if(dis[k][v]>dis[k][u]+w[alf][i]){
dis[k][v]=dis[k][u]+w[alf][i];
q.push(make_pair(-dis[k][v],v));
}
}
}
}
bool is[MAXN];
queue<int> que;
int ans[MAXN],kyl,degree[MAXN];
inline void topo(){
for(int i=1;i<=n;++i)if(!degree[i])que.push(i);
while(!que.empty()){
int u=que.front();
que.pop();
kyl=max(kyl,ans[u]);
for(int i=head[1][u];i;i=ne[1][i]){
int v=to[1][i];
--degree[v];
ans[v]=max(ans[v],ans[u]+w[1][i]);
if(!degree[v])que.push(v);
}
}
}
int main(){
n=read();m=read();
s1=read();e1=read();s2=read();e2=read();
for(int i=1,u,v,d;i<=m;++i){
u=read();v=read();d=read();
add(0,u,v,d),add(0,v,u,d);
}
memset(dis,0x3f,sizeof(dis));
di(0,0,s1);di(1,0,e1);di(2,0,s2);di(3,0,e2);
for(int i=1;i<=n;++i){
for(int j=head[0][i];j;j=ne[0][j]){
if(w[0][j]+dis[0][i]+dis[1][to[0][j]]==dis[0][e1]){
if(w[0][j]+dis[2][i]+dis[3][to[0][j]]==dis[2][e2]){
add(1,i,to[0][j],w[0][j]);
++degree[to[0][j]];
}
}
}
}
topo();
memset(head[1],0,sizeof(head[1]));
tot[1]=0;
for(int i=1;i<=n;++i){
for(int j=head[0][i];j;j=ne[0][j]){
if(w[0][j]+dis[0][i]+dis[1][to[0][j]]==dis[0][e1]){
if(w[0][j]+dis[3][i]+dis[2][to[0][j]]==dis[2][e2]){
add(1,i,to[0][j],w[0][j]);
++degree[to[0][j]];
}
}
}
}
topo();
printf("%d",kyl);
return 0;
}
题三
链接,尽量不选特殊边,在最短路时堆中比较魔改就好,特殊边后读编号比较大。
priority_queue<pair<long long,pair<int,int> > > q;
bool vis[MAXN];
long long dis[2][MAXN];
inline void di(int alf,int s){
memset(dis[alf],0x3f,sizeof(dis[alf]));
memset(vis,0,sizeof(vis));
dis[alf][s]=0;
q.push(make_pair(0,make_pair(s,1)));
while(!q.empty()){
int u=q.top().second.first,qwq=-q.top().second.second;
q.pop();
if(vis[u])continue;
vis[u]=1;
if(qwq!=-1)road[qwq]=1;
for(int i=head[u];i;i=ne[i]){
int v=to[i];
if(dis[alf][v]>=dis[alf][u]+w[i]){
dis[alf][v]=dis[alf][u]+w[i];
q.push(make_pair(-dis[alf][v],make_pair(v,-i)));
}
}
}
}