题目大意
给定一棵有边权的树和若干条路径,现在可以把这棵树的某一条边的边权变成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).
其次,要善于转换问题,想一想这个问题有没有其他的表述方式或者等价条件(公共边=覆盖次数为总路径数),这种转化常常是解题关键.
关于树上差分的更多知识(包括对点操作的树上差分),可以参见这一篇博客:关于差分,树上差分的浅谈