P2680 运输计划(树上差分,树链剖分求lca,二分答案)

P2680 运输计划

题目大意

给定一棵有边权的树和若干条路径,现在可以把这棵树的某一条边的边权变成0,试求改变了这条边后最长路径的最小值.

题目解析

二分答案:

最长路径最小值,第一反应就是二分最长路径的长度.

二分法的实现不需要讲了,主要思考这里的 c h e c k check check怎么写.

对于每一次二分答案,我们都会判断所有的路径是否大于当前的最长路径长度( m i d mid mid),那部分路径长度大于最大路径长度的路径,我们自然需要想方法尽量使这些路径的长度小于当前 m i d mid mid. 既然这些路径每一个都需要减小才有可能小于 m i d mid mid,而每次我们只能修改一条边,所以我们显然要把这些路径的公共边中最长一条变成0.

如果不能修改(即这些路径没有公共边)或者修改之后最长的路径仍然大于 m i d mid mid,说明 m i d mid mid要变大,否则 m i d mid mid可能可以变小.

那么我们现在就把问题转换成了:判断若干条路径的公共边,并找到公共边中最长的.


树上差分:

我们思考发现,若干条路径的公共边等同于找一条边,边的被覆盖次数恰好为路径总数次.

这样我们进一步把求公共边转化了求边的覆盖次数的问题(只要覆盖了那么多次就是公共边了)

每一条路径是树上的区间操作,而且求的又是覆盖次数(差分就是用来求覆盖次数的),自然想到树上差分.

这里是对边的操作,为了方便我们得把边上操作转换成点的操作,也就是一个点往他的父亲连的边存在这个点上.

有一条 u − > f a [ u ] u->fa[u] u>fa[u]的边,那么我们就用 u u u统计这条边的被覆盖次数. 原理为:在树上每个点都只有一个父亲.

然后就是对边操作的树上差分的常见套路:对于每一条路径 s − > t s->t s>t,差分数组 s u m [ s ] + 1 , s u m [ t ] + 1 , s u m [ l c a ] − 2 sum[s]+1,sum[t]+1,sum[lca]-2 sum[s]+1,sum[t]+1,sum[lca]2.

为什么 s u m [ l c a ] − 2 sum[lca]-2 sum[lca]2呢?由上面的描述, l c a lca lca这个点存的是 l c a − > f a [ l c a ] lca->fa[lca] lca>fa[lca]的覆盖次数,而这一条边实际上没有被覆盖,所以应该-2. 这也是对边操作和对点操作的一个很大的不同.

求一个点的被覆盖次数时,求子树的差分数组的和即可.

实际上这道题只要写出普通的区间差分就有链的40pts了

这样我们每条 l e n > m i d len>mid len>mid的路径都 O ( 1 ) O(1) O(1)在路径上打标记,最后再一遍 O ( n ) O(n) O(n) d f s dfs dfs算出最长的公共边,每次 c h e c k check check的时间为 O ( n + m ) O(n+m) O(n+m),总的时间复杂度为 O ( ( n + m ) l o g   n ) O((n+m)log\:n) O((n+m)logn),可以切掉此题.


预处理:

根据以上的分析,我们发现我们需要预先对几个东西进行预处理:

1.每条路径的长度. 不然你怎么知道哪些路径 l e n > m i d len>mid len>mid

2.每条路径的最近公共祖先. (在差分数组修改时要用)


树链剖分求lca:

你以为这道题真的完了吗?没有!出题人故意出的只有一个的30w的点可不是乱搞而已.

如果用通常的倍增求lca,最后一个点会T得很惨,吸氧都过不去.死活卡在95pts

所以我们不得不使用一种更为快速的求最近公共祖先的方法:树链剖分.

树链剖分求lca,和树链剖分的对区间查询/修改差不多,都是不断的跳直到两个点在同一条重链上为止.

在同一条重链上时,显然深度浅的那个点是lca,输出就好了.

核心代码如下

//其他的操作和树剖修改/查询区间一样,不过第二个dfs只需要求top就行了
int LCA(int u,int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]])swap(u,v);
		u=fa[top[u]];//点跳到上一条链上
	}
	//在同一条链上,输出深度较小的
	if(dep[u]>dep[v])return v;
	else return u;
}

虽然理论上复杂度都是 O ( l o g   n ) O(log\:n) O(logn),实测树链剖分却大约比倍增快 40 % 40\% 40%左右,究其原因,大约是树链剖分是两边一起跳,最劣的情况下复杂度是 O ( l o g   n ) O(log\:n) O(logn),而倍增每次一开始只操作一个点,时间复杂度基本稳定较慢.

另外,这道题中我们求每一条路径的长度时,也是用类似于以上求lca的方法求的,稍微处理一下每个点到 t o p top top(链顶)的总边长就好了.

程序实现

#include<bits/stdc++.h>
#define maxn 300010
#define mid ((l+r)>>1)
using namespace std;
struct edge{
	int v,w,next;
}e[maxn<<1];
int head[maxn],tot;
void add(int u,int v,int w){
	e[++tot].v =v;e[tot].w =w;
	e[tot].next =head[u];head[u]=tot;
}
int son[maxn],size[maxn],dep[maxn],fa[maxn],dist_son[maxn];
void dfs1(int u,int pre){
	dep[u]=dep[pre]+1;
	size[u]=1;fa[u]=pre;
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs1(v,u);
		size[u]+=size[v];
		if(size[v]>size[son[u]]){
			dist_son[u]=e[i].w ;
			son[u]=v;
		}
	}
}
int top[maxn],dist[maxn];
void dfs2(int u,int topu,int dis){
	//dis表示从链首到当前位置的总长(类似前缀和)
	dist[u]=dis;
	//dist表示每个点到top的边长的总和
	top[u]=topu;
	if(!son[u])return ;
	dfs2(son[u],topu,dist_son[u]+dist[u]);
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==fa[u]||v==son[u])continue;
		dfs2(v,v,e[i].w );
	}
}
int LCA(int u,int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]])swap(u,v);
		u=fa[top[u]];
	}
	if(dep[u]>dep[v])return v;
	else return u;
}//树链剖分求lca
int get_len(int u,int v){
	int ret=0;
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]])swap(u,v);
		ret+=dist[u];
		u=fa[top[u]];
		//路径长加上这条链的长度
	}
	if(dep[u]>dep[v])return ret+dist[u]-dist[v];
	else return ret+dist[v]-dist[u];
	//已在同一条链上,路径长+=同一条链上距离的差值
}//树链剖分求路径总长
int n,m,ans;
struct node{
	int s,t,lca,len;
}path[maxn];//path存路径的信息
int sum[maxn],path_max,max_len;
void dfs(int u,int pre,int cnt,int dis){
	//点u存的是u->fa[u]的边长,也就是dis
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs(v,u,cnt,e[i].w );//边长下传
		sum[u]+=sum[v];
	}
	if(sum[u]>=cnt&&dis>max_len)max_len=dis;
	//sum[u]>=cnt,说明是公共边;max_len存最大的公共边
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,u,v,w;i<n;i++){
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);add(v,u,w);
	}
	dfs1(1,1);
	dfs2(1,1,0);
	int l=0,r=0;
	for(int i=1,s,t;i<=m;i++){
		scanf("%d%d",&s,&t);
		path[i].s =s,path[i].t =t;
		path[i].lca =LCA(s,t);
		path[i].len =get_len(s,t);
		r=max(r,path[i].len );
		//输入时预处理每条路径
	}
	while(l<=r){
		//我们现在试着使最长路径的长度为mid
		memset(sum,0,sizeof sum);
		int cnt=0,len=0;path_max=0,max_len=-1;
		//每次查询前都要初始化,sum为差分数组
		for(int i=1;i<=m;i++){
			if(path[i].len <=mid)continue;
			sum[path[i].s ]++;
			sum[path[i].t ]++;
			sum[path[i].lca ]-=2;//常规的树上差分
			path_max=max(path_max,path[i].len );
			//path_max存所有路径中最长的
			cnt++;
			//cnt记录满足条件的路径数量(也就是公共边的覆盖次数)
		}
		dfs(1,1,cnt,0);//O(n)求最大的公共边
		if(max_len==-1){l=mid+1;continue;}//如果没有公共边
		len=path_max-max_len;
		//修改之后的最长路径如果仍然大于mid,则mid不可能是最长路径的长度
		if(len<=mid)ans=mid,r=mid-1;
		else l=mid+1;
	}
	printf("%d\n",ans);
	return 0;
}

题后反思

做题一定要产生直觉,比如说见到“最小值最大”就想到二分,“求覆盖次数”就想到(树上)差分(毕竟比线段树少一个log).

其次,要善于转换问题,想一想这个问题有没有其他的表述方式或者等价条件(公共边=覆盖次数为总路径数),这种转化常常是解题关键.

关于树上差分的更多知识(包括对点操作的树上差分),可以参见这一篇博客:关于差分,树上差分的浅谈

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值