题目:
简化题目:
在一个图中删一条边,然后再跑此刻图中最短路,有时间的限制,存在每条边都可以在任意时刻删。求删完后的最短路径长。
题解:
(这个题我赶了一下午,跟G-hsm大佬一起讨论,多亏大佬的细心讲解,我才大概能懂,再次感谢!)
(这个题,输出 − 1 -1 −1 即可有15分,输出样例就可以20分了,这样的题我觉得20分就够,,,因为这个题的暴力是在是不好调还不好写。尽管让我在台上讲了这个题,但是还是有很多同学没懂,挂机了,,,在博客这里补一下)
正解:
(不考虑无解的情况)由于我们要使小A和小B的策略都很优,所以就要在每条边都进行判断选择可删或者不可删,最后就是要求这所有情况的 m a x max max,这一样的话就很容易想到DP,DP什么呢,设 f [ i ] f[i] f[i]为答案,表示从i号点走到n号点,没删边时能走出的最短距离。因为要删边就要考虑到删边的时间,因为在不同的时候删不同的边就会的到不同的答案,所以说需要把所有情况都处理出来,就会得到一个很显然的DP式: f [ i ] = m a x f [ j ] + l ( i , j ) , d ( i , 1 ) f[i]=max{f[j]+l(i,j),d(i,1)} f[i]=maxf[j]+l(i,j),d(i,1),这里的 l ( i , j ) l(i,j) l(i,j) 表示边 ( i , j ) (i,j) (i,j)的长度, d ( i , j ) d(i,j) d(i,j) 就表示把 ( i , j ) (i,j) (i,j) 这条边删掉后最短路的长度,这事实上是一个带环的DP转移,可以被看作最短路模型,用 d i j k s t r a dijkstra dijkstra 算法进行解决。这样就大概得到了一个解题的方向,然后就开始处理 d ( i , j ) d(i,j) d(i,j) 。
处理 d ( i , j ) d(i,j) d(i,j)的时候要运用到最短路径树和并查集,(最短路径树没有见过或是不懂的可以参考这篇博客:最短路径树 ,最好自己推一下上面的图。推荐去做一下LOJ10064的黑暗城堡,最短路径树的计数问题),那么我们处理的方法就是:跑一个以 n n n为源点的单源最短路,就是 d i j k s t r a dijkstra dijkstra,然后就在每个点上进行记录,记录从 n n n到这个点的最短路的倒数第二个点是谁。由于要保证父子关系,所以就会把反边也删掉,每个点都只留下一条有向链。然后就开始建树,构造新图来存这个树,用刚才存下来的边与边之间的关系,在查找一个,边的长度为在原图中这两个点之间的边的长度,但是由于下面的式子,可以直接存一下式子的前半部分。这样你就构出了一棵树,以 n n n为根的有根树,这就是最短路径树,已经构造好了。你也可以从另一方面想一下,就是,在求 d ( i , j ) d(i,j) d(i,j)的时候,如果删掉的边不是最短路径树上的边时,hi没有意义的,因为这样 d ( i , j ) d(i,j) d(i,j)就会是原图中的i到n的最短路径长度。然后先在就是要求:树上的每条边,求出它被删掉后,从点n到这条边连接着的儿子的最短路长度。可以用下面的图和式子解释:
选择一个边
(
u
,
v
)
(u,v)
(u,v) 进行删边,则式子就是:
p
(
a
,
b
)
=
d
i
s
[
b
]
+
L
(
a
,
b
)
+
(
d
i
s
t
[
a
]
−
d
i
s
t
[
v
]
)
=
(
d
i
s
t
[
a
]
+
d
i
s
t
[
b
]
+
L
(
a
,
b
)
)
−
d
i
s
t
[
v
]
p(a,b)=dis[b]+L(a,b)+(dist[a]-dist[v])=(dist[a]+dist[b]+L(a,b))-dist[v]
p(a,b)=dis[b]+L(a,b)+(dist[a]−dist[v])=(dist[a]+dist[b]+L(a,b))−dist[v]
这个是删掉边
(
u
,
v
)
(u,v)
(u,v)之后所得的最短路长度,具体的理解可以看下图:(蓝色减去绿色就是红色)
这就是求出来从点 n n n到这条边连接着的儿子的最短路长度,就是 n n n不走这条边的时候到这个点的最短路。
看到这个式子之后就可以想到是只有v是需要枚举处理的,至于 l ( a , b ) l(a,b) l(a,b),因为是有解的所以一定是存在一条 l ( a , b ) l(a,b) l(a,b),这条边不是最短路径树上的边,因为要让 p ( a , b ) p(a,b) p(a,b)越小,所以就让 ( d i s t [ a ] + d i s t [ b ] + L ( a , b ) ) (dist[a]+dist[b]+L(a,b)) (dist[a]+dist[b]+L(a,b))越小,排个序就好,这样就有保证了。然后再过来枚举 ( u , v ) (u,v) (u,v)这条边,显然,暴力的做法就是一条一条枚举,这样因为图的大小和时间的限制,就会发现,这个是一定会T掉的,(所以我就说嘛,别写暴力,可能暴力你都想不到,好好想前面的题才是正经事),想一下刚开始是怎么枚举的?那就是在从小到大的树边上在枚举v,在每一个v上再枚举l(a,b),这时候可以发现一个性质,被赋值过一次的点就不需要再被赋值。以此条件进行优化:设 F [ u ] F[u] F[u]表示 u u u及 u u u的祖先中,深度最大的还没被赋值过的点。在枚举边的是时候可以装换为跳到每一个要更新的点上,每次找以x为祖先的深度最大的还没有被赋值的点,然后更新他的pr[]就行。上述过程即为一个并查集形式的操作,时间复杂度也和并查集的复杂度相同。
求出来 d ( i , j ) d(i,j) d(i,j),最后再套上刚开始的DP即可。
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
const int M=450000,N=120000;
typedef long long ll;
const ll inf=1000000000000000LL;
struct node
{
int a,b,jl;ll l;
bool operator <(const node &no) const
{return l<no.l;}
};
node e[M],a[M];
int fi[N],ne[M],dep[N],fa[N],father[N];
ll pr[N],d[N],dp[N];
int p=0,pE=0,cnt=0,i,n,m,aa,bb,t;
priority_queue<pair<ll,int> >q;
int getf(int x){if (fa[x]==x) return x;fa[x]=getf(fa[x]);return fa[x];}
void addE(int aa,int bb,ll l){pE++;e[pE].a=aa;e[pE].b=bb; e[pE].l=l;e[pE].jl=0;ne[pE]=fi[aa];fi[aa]=pE;}
void addA(int aa,int bb,ll l){p++;a[p].a=aa;a[p].b=bb;a[p].l=l;}
//两次建边
void dfs(int x)
{
for(int j=fi[x];j;j=ne[j])
if(e[j].jl==1)
{
if(dep[e[j].b]==-1)
{
dep[e[j].b]=dep[x]+1;
d[e[j].b]=d[x]+e[j].l;
father[e[j].b]=x;//从n到这个点的最短路径经过的倒数第二个点是谁。
dfs(e[j].b);
}
else e[j].jl=0;//删去的y到x的边,但是不会删去x到y的边
}
}
//把边权搞到根节点n上 ,这里是处理出来d[i]和最短路上两点的关系
void dfs2(int x)
{
for(int j=fi[x];j;j=ne[j])
{
int y=e[j].b,z=e[i].l;
if (e[j].jl==0&&y>x&&y!=father[x])//判断是否是在最短路上的倒数第二个点
addA(x,y,d[x]+d[y]+z);//每个点与自己记录的倒数第二个点连边,边的长度为在原图中这两个点之间的边的长度
}
for(int j=fi[x];j;j=ne[j]) if (e[j].jl==1) dfs2(e[j].b);//沿着整个边进行建树
}
//构建最短路径树
int main()
{
// freopen("city.in","r",stdin);
// freopen("city.out","w",stdout);
scanf("%d%d",&n,&m);
for (i=1;i<=m;i++)
{
int u,v,l;
scanf("%d%d%d",&u,&v,&l);
addE(u,v,l),addE(v,u,l);
}
//以n为源点最短路
for(i=1;i<=n;i++) d[i]=inf; d[n]=0;
q.push(make_pair(0,n));
while (!q.empty())
{
if (-d[q.top().second]!=q.top().first) {q.pop();continue;}
int u=q.top().second;q.pop();
for (int j=fi[u];j>0;j=ne[j])
if (d[u]+e[j].l<d[e[j].b])
{
d[e[j].b]=d[u]+e[j].l;
q.push(make_pair(-d[e[j].b],e[j].b));
}
}
//构建最短路树
for (int j=1;j<=pE;j++) if(d[e[j].b]==d[e[j].a]+e[j].l) e[j].jl=1;
//标记最短路的边
memset(dep,-1,sizeof(dep)); dep[n]=0;dfs(n); dfs2(n);
//处理n与各个节点的关系以及d[i]
//图,,,,
//路径长度是dis[b]+L(a,b)+(dist[a]-dist[v])=(dist[a]+dist[b]+L(a,b))-dist[v]
//把前面单独取出来
//枚举删边
sort(a+1,a+1+p);
//算贪心 ,要按顺序把a-b链上的点的pr[]都搞出来,就是利用这个,所以要在这条链上跳。
//在搞pr的时候就相当于并查集了。。也就很像所谓的kruskal了。。
for(i=1;i<=n;i++) fa[i]=i; for(i=1;i<=n;i++) pr[i]=inf;//存的是整个式子
for(i=1;i<=p;i++)
{
int u=getf(a[i].a),v=getf(a[i].b);
while (u!=v)
{
if (dep[u]<dep[v]) swap(u,v);
pr[u]=a[i].l-d[u]; cnt++;//这点枚举删边
fa[u]=getf(father[u]); u=fa[u];
}
if (cnt>=n-1) break;
}
//所以我们再跑一遍最短路的目的就是综合上面的情况。。
for (i=1;i<=n;i++) dp[i]=inf;
dp[n]=0;q.push(make_pair(0,n));
while(!q.empty())
{
if (-dp[q.top().second]!=q.top().first) {q.pop();continue;}
int u=q.top().second;q.pop();
for (int j=fi[u];j;j=ne[j])
{
int v=e[j].b;
ll ret=dp[u]+e[j].l;
if (e[j].jl) ret=max(ret,pr[v]);//另一种就是割掉一条边所产生的pr[]。
else ret=max(ret,d[v]);//一种就是直接在树上就有的最短路上的点,那么它对于答案的贡献就是d【】
if(ret<dp[v]) dp[v]=ret,q.push(make_pair(-dp[v],v));
}
}
if(dp[1]==inf) cout<<-1<<endl;
else cout<<dp[1]<<endl;//答案就是dp[1]了
return 0;
}