K短路的几种求法

版权声明:转载注明出处,谢谢,有问题可以向博主联系 https://blog.csdn.net/VictoryCzt/article/details/83104568

K短路

引入

对于最短路,我们可以用 D i j s t r a , S p f a , F l o y d \rm Dijstra,Spfa,Floyd 等算法求出。

那么对于第 K K 短的路,我们该如何求取呢?

Ps. 这里都是在有向图上求取 K K 短路,无向图上可以将其建为双向边然后跑有向图上的 K K 短路即可。


算法一:搜索

我们可以在最短路算法上,轻微改动一下。我们将每一条从起点 S S 到终点 T T 的路径搜出来,排序后选取第 K K 个即可。复杂度为 O ( w ( n + m ) + w l o g w ) O(w(n+m)+wlogw) n , m n,m 为点数和边数, w w 为路径总条数。

显然,这样暴力的算法只可能拿到非常少的分。

算法二:搜索剪枝

我们发现,当你搜索的路径数已经达到 K K 条后,凡是已经大于你所搜过的最大的路径长度的情况可以统统剪掉,将较小的加入后,我们弹出最大的一条,并将剪枝用的最大值修改成新的最大值,这个最大值肯定是单调递减的,所以可以大大减少搜索量。

但是,对于特殊构造的图,仍然如同暴力。

算法三:启发式搜索

启发式搜索,就是常用的 A A^* 算法。

同样的还是暴力搜索,我们可以对当前的局势进行预估判断,从而达到剪枝的最大化。

所以这里我们设置两个函数 g g f f f f 表示你当前走到这里,然后走到终点至少还总共会走多少距离; g g 则表示你当前走了多少距离。

所以这里我们要先将图反建(边的方向变反),然后从 T T 跑出单源最短路, T T 到点 i i 的最短路记为 d t [ i ] dt[i] ,预处理了这个我们就能对当前局势进行预判了。

当前的函数 g g 的值就等于走来的点的 g g' 值加上这条边的权值:
g [ v ] = g [ u ] + d i s [ u ] [ v ] g[v]=g[u]+dis[u][v]

而当前的预判函数 f f 的值,当然就是当前已经确定的距离加上我们预期能走的最短距离,预期最短距离已经预处理了,所以可以得知:
f [ v ] = g [ v ] + d t [ v ] f[v]=g[v]+dt[v]

然后我们利用前面普通的剪枝方式,按照 f f 为第一关键字, g g 为第二关键字,将状态排序,每次找最小的出来更新,那么刚好更新到第 K K 个的时候,它就是第 K K 短的啦。

这里我们就用 b f s bfs (宽度优先搜索)加上优先队列就可以做到了。

复杂度最好为 O ( K + m l o g m ) O(K+mlogm) ,最坏的话和暴力差不多,所以只适合没有特别精心构造的图。

下面上博主的简陋代码QWQ:

poj2449 AC
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010,M=1e5+10;
const int inf=0x3f3f3f3f;
int n,m,s,t,k;
struct ss{
	int to,last,val;
	#define F to
	#define G last
	#define id val
	ss(){}
	ss(int a,int b,int c):to(a),last(b),val(c){}
};
struct Graphy{
	ss g[M];
	int head[M],cnt;
	void add(int a,int b,int c){
		g[++cnt]=ss(b,head[a],c);head[a]=cnt;
	}
	void clear(){
		memset(head,0,sizeof(head));cnt=0;
	}
};
namespace Gra_Inv{
	Graphy G;
	int que[M],p,q;
	bool vis[M];
	bool spfa(int st,int en,int *dis){
		memset(dis,0x3f,sizeof(int)*(n+1));
		dis[en]=0;que[p=q=0]=en;vis[en]=1;
		int *head=G.head;ss *g=G.g;
		for(;p<=q;p++){
			int a=que[p%M],v;
			for(int i=head[a];i;i=g[i].last){
				v=g[i].to;
				if(dis[v]>dis[a]+g[i].val){
					dis[v]=dis[a]+g[i].val;
					if(!vis[v]){
						vis[v]=1;
						que[++q%M]=v;
						if(dis[que[(p+1)%M]]>dis[que[q%M]]){
							swap(que[(p+1)%M],que[q%M]);
						}
					}
				}
			}
			vis[a]=0;
		}
		return dis[st]!=inf;
	}
}
namespace Gra_Astar{
	Graphy G;
	struct node{
		ss s;
		node(){}
		node(ss a):s(a){}
		bool operator <(const node &a)const
		{return s.F>a.s.F||(s.F==a.s.F&&s.G>a.s.G);}
	};
	priority_queue <node> Q;
	int dis[M];
	int A_star(int st,int en,int k){
		int *head=G.head;ss *g=G.g;
		node tmp=node(ss(dis[st],0,st)),now;
		Q.push(tmp);
		if(st==en)++k;
		while(!Q.empty()){
			tmp=Q.top();Q.pop();
			int a=tmp.s.id,v;
			if(a==en) {--k;if(!k)return tmp.s.F;}
			for(int i=head[a];i;i=g[i].last){
				v=g[i].to;
				now.s.G=tmp.s.G+g[i].val;
				now.s.F=now.s.G+dis[v];
				now.s.id=v;
				Q.push(now);
			}
		}
		return -1;
	}
}
int a,b,c;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&a,&b,&c);
		Gra_Inv::G.add(b,a,c);
		Gra_Astar::G.add(a,b,c);
	}
	scanf("%d%d%d",&s,&t,&k);
	if(Gra_Inv::spfa(s,t,Gra_Astar::dis)){
		printf("%d\n",Gra_Astar::A_star(s,t,k));
	}else puts("-1");
	return 0;
}

最短路树加可持久化堆

这个是在 A A^* 算法上和最短路算法上,总结升华提取出来的一个较为稳定的 K K 短路算法。

我们同样的按照 A A^* K K 短路的思路,我们反向建图,然后跑 T T 的单源最短路,我们可以发现,这个跑出来的 n 1 n-1 条最短路上的边,和 n n 个点一定构成一棵树。

因为树上的两两点对之间的路径是唯一的(路径为最短路),且这个树的根为 T T

那么我们考虑没有在这颗树上的边(称为非树边),如果我们选择了一条 u v u\rightarrow v 的非树边,那么它一定会使得最短路变化(大多数情况下是变大),而变化的值为 d i s [ v ] + n o t _ t r e e _ e d g e [ u ] [ v ] d i s [ u ] dis[v]+not\_tree\_edge[u][v]-dis[u] (其中 d i s [ v ] dis[v] 表示 v v T T 的最短路长度)。

所以我们可以将所有的非树边的边权重新设置为 d i s [ v ] + n o t _ t r e e _ e d g e [ u ] [ v ] d i s [ u ] dis[v]+not\_tree\_edge[u][v]-dis[u] ,那么第 K K 短路肯定为 S T S\rightarrow T 的最短路加上一个 S T S\rightarrow T 的非树边序列的权值。

非树边序列:即为你这条路上所走过的非树边,按照从 S T S\rightarrow T 的方向。

因为最短路的长度确定了,那么我们只需这个非树边序列的权值,是所有 S T S\rightarrow T 中的合法非树边序列权值中的第 K K 小即可。

所以我们来考虑,对于每一个点, S T S\rightarrow T 的非树边序列如果经过它的话,一部分的序列肯定是这个点到 T T 的非树边序列中的一种,所以对于每一个点,我们用一个小根堆来维护它到 T T 的非树边序列值。

那么对于一个点 v v ,我们如何快速得到它的小根堆(排好序的非树边序列)呢?

我们可以由当前的点 v v 到父亲 f [ v ] f[v] 的非树边加上 f [ v ] f[v] 的小根堆的状态即可,所以为了方便更新和减少空间开销,所以我们要用可持久化堆(这里使用左偏树)。

然后先预处理所有点的序列,然后我们每次取出最小的一个,然后加入新的状态,直到更新到第 K K 个,答案就为最短路长度加上第 K K 个非树边序列的值即可。

复杂度为 O ( n l o g n + m l o g m + k l o g k ) O(nlogn+mlogm+klogk)

这个复杂度是比较稳定的(缺点是不好记录路径)。

下面上用可持久化左偏树+ S p f a \rm Spfa 实现的代码:

注意:有两种情况

  • 允许点重复走的:当 S = T S=T 时,要减去一条 S S 到自己的路径。
  • 不允许重复走的要记录已经走过的点(一般点数不多,直接 b o o l bool 数组或者 b i t s e t bitset 记录)
同样是poj的那道题
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int M=7e5+1,N=1e3+10;
const int inf=0x7fffffff;
int n,m;

struct ss{
	int to,last,len;
	ss(){}
	ss(int a,int b,int c):to(a),last(b),len(c){}
};

struct Graphy{
	ss g[M];
	int head[M],cnt;
	void add(int a,int b,int c){
		g[++cnt]=ss(b,head[a],c);head[a]=cnt;
	}
}G,GI;

struct node{
	int val,v;
	node(){}
	node(int a,int b):val(a),v(b){}
	bool operator <(const node &a)const{return val>a.val;}
}pp,qq;
priority_queue <node> Q;

struct Left_Tree{
	int h[M],v[M],w[M],son[2][M],tot,root[M];
	void clear(){tot=0;}
	#define l(o) son[0][o]
	#define r(o) son[1][o]
	int newnode(int vv,int idd,int hh){
		++tot;
		w[tot]=vv;v[tot]=idd;h[tot]=hh;
		l(tot)=r(tot)=0;
		return tot;
	}
	int merge(int a,int b){
		if(!a||!b) return a|b;
		if(w[a]>w[b])swap(a,b);
		int newp=newnode(w[a],v[a],h[a]);
		l(newp)=l(a);r(newp)=r(a);
		r(newp)=merge(r(newp),b);
		if(h[l(newp)]<h[r(newp)])swap(l(newp),r(newp));
		h[newp]=h[r(newp)]+1;
		return newp;
	}
	void insert(int &o,int vv,int idd){
		int p=newnode(vv,idd,0);
		o=merge(p,o);
	}
}LT;

int que[M],p,q;bool in[M];
void spfa(int st,int en,int *dis,Graphy &G){
	int *head=G.head,v;ss *g=G.g;
	for(int i=1;i<=n;i++)dis[i]=inf;
	que[p=q=0]=en;in[en]=1;dis[en]=0;
	for(;p<=q;p++){
		int a=que[p%M];
		for(int i=head[a];i;i=g[i].last){
			v=g[i].to;
			if(dis[v]>dis[a]+g[i].len){
				dis[v]=dis[a]+g[i].len;
				if(!in[v]){
					in[v]=1;
					que[++q%M]=v;
					if(dis[que[(p+1)%M]]>dis[que[q%M]]){
						swap(que[(p+1)%M],que[q%M]);
					}
				}
			}
		}
		in[a]=0;
	}
}
int fd[M],stk[M],dis[M],top,fs[M];
void dfs(int a){
	in[a]=1;stk[++top]=a;int v,w;
	for(int i=GI.head[a];i;i=GI.g[i].last){
		v=GI.g[i].to;w=GI.g[i].len;
		if(!in[v]&&dis[v]==dis[a]+w){
			fd[v]=a;fs[i]=1;
			dfs(v);
		}
	}
}
void Rebuild(){
	int a,v;
	for(int w=1;w<=top;w++){
		a=stk[w];
		LT.root[a]=LT.root[fd[a]];
		for(int i=G.head[a];i;i=G.g[i].last){
			v=G.g[i].to;
			if(!fs[i]&&dis[v]!=inf){
				LT.insert(LT.root[a],dis[v]-dis[a]+G.g[i].len,v);
			}
		}
	}
}
int Solve_K(int st,int en,int kth){
	if(!LT.root[st]) return -1;
	if(kth==1) return dis[st];
	pp.val=dis[st]+LT.w[LT.root[st]];
	pp.v=LT.root[st];
	Q.push(pp);
	while(!Q.empty()){
		--kth;
		qq=Q.top();Q.pop();
		if(kth==1) return qq.val;
		int ls=LT.l(qq.v),rs=LT.r(qq.v),o=LT.v[qq.v];
		int nowv=qq.val;
		if(LT.root[o]) Q.push(node(nowv+LT.w[LT.root[o]],LT.root[o]));
		if(ls) Q.push(node(nowv+LT.w[ls]-LT.w[qq.v],ls));
		if(rs) Q.push(node(nowv+LT.w[rs]-LT.w[qq.v],rs));
	}
	return -1;
}

void add_side(int a,int b,int c){
	G.add(a,b,c);GI.add(b,a,c);
}

void calc_K(int ss,int tt,int kk){
	if(ss==tt)++kk;
	spfa(ss,tt,dis,GI);
	if(dis[ss]==inf) {puts("-1");return;}
	dfs(tt);
	Rebuild();
	int ans=Solve_K(ss,tt,kk);
	printf("%d\n",ans);
}
int s,t,k;
void work(){
	int a,b,c;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&a,&b,&c);
		add_side(a,b,c);
	}
	scanf("%d%d%d",&s,&t,&k);
	calc_K(s,t,k);
}
int main(){work();return 0;}

若有不对或者疑问的地方欢迎提出!!

博主才学,可能有不清楚或者不对的地方。QAQ

猜你喜欢

转载自blog.csdn.net/VictoryCzt/article/details/83104568