为什么我们想绕路呢?——k短路
k短路,顾名思义,由s到t第k短的路径,那么我们该如何解决这个问题?
引子——次短路
次短路:求出从s到t的第二短路径,考虑做法:
首先我们可以直接做出最短路,然后考虑:我们有很多种路径可以走到t,点与点之间由很多边我们没有走,那么我们很容易想到:我们每次松弛操作时都是将点与点最近的走法找到了,那如果我们选择一个第二优秀的走法,不就可以了?
那么考虑:我们需要修改最短路径上的几条边?
直觉是:一条,原因:假设需要修改边\(e_1\)和\(e_2\)可以使答案最优,那很显然:只修改\(e_1\)或者\(e_2\)一定比当前答案更小并且比最短路大。
直接给出代码:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <queue>
#include <cstring>
#include <cmath>
using namespace std;
typedef pair<int,int> P;
int head[2000005],top;
struct node{
int to,next,dis;
} a[2000005];
int dis1[10005];
int dis2[10005];
void add(int u,int v,int w){
a[++top].next=head[u];
a[top].to=v;
a[top].dis=w;
head[u]=top;
}
void dij(int x){
memset(dis1,0x3f,sizeof(dis1));
memset(dis2,0x3f,sizeof(dis2));
priority_queue<P,vector<P>,greater<P> >q;
dis1[x]=0;
q.push(make_pair(0,x));
while(!q.empty( )){
int w=q.top( ).first;
int u=q.top( ).second;
q.pop( );
if(dis2[u]<w)
continue;
for(int i=head[u];i!=-1;i=a[i].next){
int v=a[i].to;
int cost=w+a[i].dis;
if(dis1[v]>cost)
swap(dis1[v],cost),
q.push( P(dis1[v],v));
if(dis2[v]>cost&&cost>dis1[v])
swap(dis2[v],cost),
q.push(P(dis2[v],v));
}
}
}
int main(){
int n,r;
scanf("%d%d",&n,&r);
memset(head,-1,sizeof(head));
int u,v,w;
for(int i=1;i<=r;i++){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
dij(1);
printf("%d\n",dis2[n]);
return 0;
}
那么如何引申到k短路呢?
思考——k短路
显然,只修改一条边,做k次肯定不对,原因如下: ,
1 显然当k>e时就做不了了(当然会有这样的K)
2 存在x条次优边的和大于某条此优边
那么我们就需要一些非常bt的算法了
前车之鉴——Solution1 A*
首先我们得有一个类似于博弈的思想:次短路和最短路的不同在于失策一步(即以此非最有决策)。也就是说:你按着最短路向终点走,有一步失误了(毕竟人在河边走,哪有不湿鞋),然后紧接着你按着现在的最短方向走到了终点,那么这条路也许就是次短路。是不是次短路取决于你是哪一步走错了。而这一步,可能会让后续的答案非常非常大!!!!!或者很小。
那么此时,我们需要简单估计出这次失误的影响,来粗略判断它究竟可能时第几小。由此,有了 A*算法的估价函数。
Point 1 用 F(x) 表示以当前的走法走到 x 的距离,H(X) 表示 x 到终点的最短路长度。
--Note 1 那么对于 x 节点,我们下一步可以选择 “走对” 或者 “走错”,于是这一个 x 又可以染色到相邻的节点。
Point 2 每一次我们选择 F(x)+H(X) 最小的 x 节点染色,那么最先到达终点时终点的 F(x) 就是最短路 (很像 dijstra的贪心思想),那么显然:第二次到达终点就是次短路。
--Note 2 原因: 简而言之,我们的染色方式确保了被染色节点的 F(x)+H(x) 一定是递增的,而到达了终点,H(x)=0,那么 F(x) 就是递增的。(者可以用bfs思想来理解)
这种算法在随机数据下,若边权较小,同时k 较大,速度不如上一迭代加深搜索。但是它的优点是稳定。
PS:由于迭代加深这个算法实在实在太太太bt了,我们......不予赘述(百度,让世界更美好)
时间复杂度 \(O(NKlog_2K)\)
那么作为一个老图伦选手,我们绝壁在想:这中算法就和SPFA一样,肯定可以卡,于是:线索形成思想,然后......以后你做K短路又少了一种满分写法......考虑一种卡 A* 的图:构造一个大环,包含起始节点和终止节点即可。(相信你完全理解)
#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#define MX 10001
#define ME 100001
using namespace std;
int n,m,k;
int S,T;
struct Node{
int p,f,h;
bool operator <(const Node a)const{
if(a.f+a.h<f+h)return 1;
else return 0;
}
Node(int a,int b,int c){
this->p=a,this->f=b,this->h=c;
}
};
Node make(int a,int b,int c){
Node thi(a,b,c);
return thi;
}
int dis[MX];
//封装
class graph{
public:
int fst[MX],nxt[ME],v[ME],w[ME],lnum;
int q[MX],inq[MX];
priority_queue<Node> mp;
void init(){
memset(fst,0xff,sizeof(fst));
lnum=-1;
}
void addeg(int nu,int nv,int nw){
nxt[++lnum]=fst[nu];
fst[nu]=lnum;
v[lnum]=nv;
w[lnum]=nw;
}
//走最短路进行估价
void SPFA(int frm){
int h=0,t=1,x,y;
memset(dis,0x3f,sizeof(dis));
q[++h]=frm;
dis[frm]=0;
inq[frm]=1;
while(h>=t){
x=q[(t++)%ME];
for(int i=fst[x];i!=-1;i=nxt[i]){
y=v[i];
if(dis[y]>dis[x]+w[i]){
dis[y]=dis[x]+w[i];
if(!inq[y]){
q[(++h)%ME]=y;
inq[y]=1;
}
}
}
inq[x]=0;
}
}
int Astar(int frm,int to){
Node x(0,0,0);
int cnt=0;
if(frm==to)k++;
if(dis[frm]>100000)return -1;
mp.push(make(frm,0,dis[frm]));
while(!mp.empty()){
x=mp.top(),mp.pop();
if(x.p==to){
cnt++;
if(cnt==k)return x.f+x.h;
}
for(int i=fst[x.p];i!=-1;i=nxt[i])mp.push(make(v[i],x.f+w[i],dis[v[i]]));
}
return -1;
}
}g1,g2;
void input(){
int a,b,c;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&a,&b,&c);
g1.addeg(a,b,c);
g2.addeg(b,a,c);
}
scanf("%d%d%d",&S,&T,&k);
}
int main(){
g1.init(),g2.init();
input();
g2.SPFA(T);
printf("%d\n",g1.Astar(S,T));
return 0;
}
后车方向盘——Solution2 可持久化可并堆
PS:我们一般用可持久化左偏树,原因:代码比较好些,性质比较优秀,当然,斐波那契堆也是很棒的,但是说实话,考场打这种算法......真的有点顶。言归正传:
其实,可并堆的做法是对 A* 做法的一种改进。
由于 A* 做法可能在多次扩展后才能找到一条路径,浪费时间(等于浪费生命),也使得时间复杂度变得非常大。
如果可以在每次扩展时都能得到一条路径,就可以将时间优化到 \(O(K\log N)\) 这个级别。
怎么做到这样呢?我们先将反图的最短路树建立出来。任何一条 S-T 路径都可以表示为若干树边和非树边交替出现的序列。我们可以直接使用其非树边序列表示这个序列。
现在定义几个东西:
- dis(x):x点沿着最短路树走到 T的路径长度。
- \(e.s,e.t,e.w\):非树边 e的起始节点,终止节点和长度。
- \(\Delta e\):从\(e.s\)出发走非树边e,再走最短路,比直接走\(e.s\)的最短路多走的距离
一条路径的长度,可以表示为每条非树边多走的距离之和。即 \(\sum \Delta e\)
如果找到一条合法的路径 \(e_1,e_2,\cdots ,e_m\) ,考虑如何像 A* 算法,找一个比这条路径略长的新的路径。保证每次找到的路径不重复。
很容易:有两种方式可以找到一条新的合法路径,并且这条路径尽可能的短,而且可以保证不重复。
- Way 1 将 \(e_m\)替换成一条\(\Delta\)更大的非树边
- Way 2 将序列后面添加上一条 \(\Delta\)最小的可行非树边即\(e_1,e_2⋯,e_m,e_{m+1}\)
考虑正确性:将非树边序列映射到一张新的图上。将 \(e_i\)映射为新图的点,如果\(ej⋯ \cdots e_i,e_j\cdots\) 是一个合法的路径 (即 \(e_i.t\) 可以通过树边走到 \(e_j.s\) ),那么 \(e_i\)和\(e_j\)之间存在一条边。现在即:求这张新的图的不规定终点的 k 短路。于是,算法的正确性就非常显然了。
对于所有的非树边序列,只需保存它的\(\sum \Delta e\)以及\(e_m\)即可。每次,从所有的已知的非树边序列中选择\(\sum \Delta e\)最小的那一个作为新的路径,并且从它开始进行扩展,将扩展出的新序列加入已知序列的集合中。已知序列的集合需要用堆 (优先队列) 维护。
接下来就是如何扩展的问题了。先考虑如何找到一条边的所有可行后继非树边。这些边就是所有挂在最短路树上\(e_m.t\cdots T\)上的非树边。也就是说一条边的可行后继非树边集合为 \(e.t\)在最短路树上的父亲的集合并上挂在\(e.t\)上的非树边。由于我们在扩展时寻找的是最小可行非树边,因此将这个集合用可并堆维护,每个点的集合由其父亲的集合继承而来。这样,一条非树边的最小可行后继非树边就是 \(e.t\) 的可并堆的跟。
如何替换一条边呢?一条边所在的可并堆的两个儿子可以替换这条边。
综上,就是 \(O(K\log_2 N)\) 的 K 短路做法。
需要注意几个地方:
-
- 当所有的路径找完了,需要及时退出,以免 pop一个空的优先队列。
-
- 当图中根本就没有第二条路径,S的可并堆将是空的,这时候需要直接退出。
时间复杂度:\(O(T(shortest path)+nlog_2n+klog_2n)\)
下面是魔法猪学院的代码。(洛谷\(P2483\))
#include <iostream>
#include <algorithm>
#include <cmath>
#include <queue>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 5050;
const int M = 200050;
const int MAXN = 10*M;
const double eps = 1e-8;
int n,m,fa[N],tot,rt[N],pre[N];
double lim,g[N];
bool vis[N];
struct node{
int ls,rs,dis,tl;
double v;
}p[MAXN];
int merge(int x,int y){
if(!x||!y)return x+y;
if(p[x].v-p[y].v>=eps)swap(x,y);
int u = ++tot;
p[u] = p[x];
p[u].rs = merge(p[u].rs,y);
if(p[p[u].ls].dis<p[p[u].rs].dis)swap(p[u].ls,p[u].rs);
p[u].dis = p[p[u].rs].dis+1;
return u;
}
typedef pair<double,int> P;
int hed[N],cnt=1;
bool cov[2*M];
struct edge{
int to,nxt;
double w;
}e[2*M];
void ae(int f,int t,double w){
e[++cnt].to = t;
e[cnt].nxt = hed[f];
e[cnt].w = w;
hed[f] = cnt;
}
void dij(){
priority_queue<P,vector<P>,greater<P> > q;
for(int i=1;i<n;i++)g[i]=1e18;
q.push(P(0.0,n));
while(!q.empty()){
P tp = q.top();
q.pop();
int u = tp.second;
if(vis[u])continue;
vis[u] = 1;
for(int j=hed[u];j;j=e[j].nxt){
if(j&1){
int to = e[j].to;
if(g[to]+eps>g[u]+e[j].w){
g[to] = g[u]+e[j].w;
q.push(P(g[to],to));
}
}
}
}
}
void dfs(int u){
vis[u] = 1;
for(int j=hed[u];j;j=e[j].nxt)if(j&1){
int to = e[j].to;
if(fabs(g[to]-(e[j].w+g[u]))<=eps&&!vis[to]){
fa[to] = u;
cov[j^1] = 1;
dfs(to);
}
}
}
void topo(){
priority_queue<P>q;
for(int i=1;i<=n;i++)q.push(P(g[i],i));
for(int u,i=1;i<=n;i++){
u = q.top().second;q.pop();
if(fa[u])rt[u] = merge(rt[u],rt[fa[u]]);
}
}
int main(){
scanf ("%d%d",&n,&m);
scanf("%lf",&lim);
int f,t;
double w;
for(int i=1;i<=m;i++){
scanf ("%d%d",&f,&t);
scanf("%lf",&w);
ae(f,t,w);
ae(t,f,w);
}
dij();
memset(vis,0,sizeof(vis));
dfs(n);
for(int j=2;j<=cnt;j+=2){
if(!cov[j]){
int f = e[j^1].to,t = e[j].to;
double w = g[t]+e[j].w-g[f];
p[++tot].v = w,p[tot].dis = 1,p[tot].tl = t;
rt[f] = merge(rt[f],tot);
}
}
topo();
priority_queue<P>q;
int ans = 0;
if(lim+eps>g[1])
lim-=g[1],ans++;
if(rt[1])q.push(P(p[rt[1]].v,rt[1]));
while(!q.empty()){
P tp = q.top();
q.pop();
if(lim+eps>tp.first+g[1])
lim-=(tp.first+g[1]),ans++;
else
break;
int u = tp.second,ls = p[u].ls,rs = p[u].rs;
if(ls)
q.push(P(tp.first-p[u].v+p[ls].v,ls));
if(rs)
q.push(P(tp.first-p[u].v+p[rs].v,rs));
int t=p[u].tl;
if(rt[t])
q.push(P(p[rt[t]].v+tp.first,rt[t]));
}
printf("%d\n",ans);
return 0;
}
//本代码来自博客https://www.mina.moe/archives/2777,疯狂安利这个博主,写得很很很很好,大赞!
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#include <cmath>
#define MX 200008
#define oo 123123123123123
using namespace std;
template <typename T> void read(T& x){
x = 0; char c = getchar(); bool f = 0;
while(!isdigit(c) && c!='-') c = getchar();
if(c == '-') f = 1, c = getchar();
while(isdigit(c)) x = x*10+c-'0', c = getchar();
if(f) x = -x;
}
int N, M;
double E;
struct PNODE{
int p;
double d;
PNODE (const int& p0 = 0, const double& d0 = 0) : p(p0), d(d0) {}
bool operator < (const PNODE& t) const {return d > t.d;}
};
struct SNODE{
int ls, rs, ed, len;
double w;
SNODE (const int& e0 = 0, const double& w0 = 0) : ls(0), rs(0), ed(e0), len(0), w(w0) {}
};
struct HEAP{
int cnt;
SNODE tre[MX*6];
int merge(int x, int y){
int a;
if(!x || !y) return (x|y);
if(tre[x].w < tre[y].w) swap(x, y);
tre[a = ++cnt] = tre[y];
tre[a].ls = merge(x, tre[y].ls);
if(tre[tre[a].ls].len > tre[tre[a].rs].len) swap(tre[a].ls, tre[a].rs);
tre[a].len = tre[tre[a].ls].len + 1;
return a;
}
};
struct GRAPH{
int fst[MX], nxt[MX], v[MX], lnum;
int que[MX], inq[MX], pre[MX];
double w[MX], dis[MX];
void addeg(int nu, int nv, double nw){
nxt[++lnum] = fst[nu];
fst[nu] = lnum;
v[lnum] = nv;
w[lnum] = nw;
}
void spfa(int frm){
int h = 1, t = 1, x, y;
for(int i=1; i<=N; i++) dis[i] = +oo;
que[h] = frm;
dis[frm] = 0;
inq[frm] = 1;
while(h >= t){
x = que[(t++)%MX];
inq[x] = 0;
for(int i=fst[x]; i; i=nxt[i]){
y = v[i];
if(dis[y] > dis[x] + w[i]){
dis[y] = dis[x] + w[i];
pre[y] = i;
if(!inq[y]){
que[(++h)%MX] = y;
inq[y] = 1;
}
}
}
}
}
};
HEAP H;
GRAPH G, R;
priority_queue<PNODE> Q;
void input(){
int a, b; double c;
read(N); read(M); scanf("%lf", &E);
for(int i=1; i<=M; i++){
read(a); read(b); scanf("%lf", &c);
G.addeg(a, b, c);
R.addeg(b, a, c);
}
}
PNODE seq[MX];
int rot[MX];
void work(){
R.spfa(N);
for(int i=1; i<=N; i++) seq[i] = PNODE(i, R.dis[i]);
sort(seq+1, seq+N+1);
for(int a=N; a>=1; a--){
int x = seq[a].p;
for(int i=G.fst[x]; i; i=G.nxt[i]){
if(i != R.pre[x]){
H.cnt++;
H.tre[H.cnt] = SNODE(G.v[i], R.dis[G.v[i]]-R.dis[R.v[i]]+G.w[i]);
rot[x] = H.merge(rot[x], H.cnt);
}
}
rot[x] = H.merge(rot[x], rot[G.v[R.pre[x]]]);
}
if(rot[1]) Q.push(PNODE(rot[1], H.tre[rot[1]].w));
int cnt = 0;
if(E-R.dis[1] >= 0) E -= R.dis[1], cnt++;
while(E > 0){
if(Q.empty()) break;
PNODE e = Q.top();
Q.pop();
if(E-(e.d+R.dis[1]) < 0) break;
else E -= (e.d+R.dis[1]), cnt++;
if(H.tre[e.p].ls) Q.push(PNODE(H.tre[e.p].ls, e.d - H.tre[e.p].w + H.tre[H.tre[e.p].ls].w));
if(H.tre[e.p].rs) Q.push(PNODE(H.tre[e.p].rs, e.d - H.tre[e.p].w + H.tre[H.tre[e.p].rs].w));
if(rot[H.tre[e.p].ed]) Q.push(PNODE(rot[H.tre[e.p].ed], e.d + H.tre[rot[H.tre[e.p].ed]].w));
}
printf("%d\n", cnt);
}
int main(){
input();
work();
return 0;
}
小结
虽然到最后我们都没有明白:为什么要绕路,但是,我们已经解决了这个k短路的问题,