10.02 T3题解

题目:

简化题目:

在一个图中删一条边,然后再跑此刻图中最短路,有时间的限制,存在每条边都可以在任意时刻删。求删完后的最短路径长。

题解:

(这个题我赶了一下午,跟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;
} 

一定要勇敢的飞呀——蓝书

Continue……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值