题目描述
给定一棵树和一些路径,每个点有一个点权,求每个点点权恰好等于路径上的访问排位的数量.
举个例子: s − > t s->t s−>t经过了点 k k k,点 k k k的权值为 w w w且点 k k k恰好是这条路径上第 w + 1 w+1 w+1个点(路径上第一个点 s s s的访问排位为0),则 a n s [ k ] + + ans[k]++ ans[k]++.
题目分析
25pts:
随便乱搞都可以. 我采用的方法是在线处理每条路径,用树剖的方法把路径剖下来,然后再与标准比较,如果 w w w和次序相同那么这个点的 a n s + + ans++ ans++,时间复杂度 O ( n m l o g 2 n ) O(nmlog^2n) O(nmlog2n). 能过 25 % 25\% 25%的数据.
100pts:
我们需要转换思路. 每次的一条链不能对每一个元素都进行操作,否则时间快不上来.
那我们就想到了一种更加简单的对树上区间进行操作的方法
树上差分:
差分通常是和前缀和结合起来的,比如说以上博文的例子,如果覆盖了,那么在区间前端加一,在区间末端减一.
这样,我们就相当于给区间打了标记,不用每次给出一段区间时都处理,而是最后再使用一个差分数组,统一处理标记,复杂度自然就下来了.
树上差分也是一样. 通过打标记的方式,来把对区间处理的复杂度由 O ( n ) O(n) O(n)降为 O ( 1 ) O(1) O(1).
比如说,我们有这样一条路径
对于这条路径,我们这样打上标记
我们在计算一个点被多少条路径经过时,只要统计在它的子树中的标记的和就可以了.
值得注意的是,两个-1的标记,一个打在 l c a lca lca上,另一个打在 f a [ l c a ] fa[lca] fa[lca]上,这样可以保证 l c a lca lca不会因为是两段的交集而重复计算路径数.
这样子,我们把原来每条路径上的点处理一次,总的处理复杂度为 O ( n 2 ) O(n^2) O(n2),变成了每条路径进行 O ( 1 ) O(1) O(1)处理,最后再用一次 d f s dfs dfs O ( n ) O(n) O(n)算出每个点的覆盖次数,从而大大降低了时间复杂度.
不难发现,我们同样可以把以上差分的处理方法运用到这道题上.
桶思想:
简而言之,桶就是不论里面的东西是什么,而只看里面的东西有多少.
关于桶这一思想,可以参见另外一篇博客:(留坑待填)
我们首先要对这一题的题目条件进行转换.
不难发现,对于任意一个在某一条路径上的点 x x x,如果这条路径对这个点的答案有贡献,那么必定满足以下条件之一:
1.如果这个点在 s − > l c a s->lca s−>lca上,那么 d e p [ x ] − d e p [ u ] = w [ x ] dep[x]-dep[u]=w[x] dep[x]−dep[u]=w[x];
2.如果这个点在 l c a − > t lca->t lca−>t上,那么 d e p [ u ] − d e p [ l c a ] + d e p [ x ] − d e p [ l c a ] = w [ x ] dep[u]-dep[lca]+dep[x]-dep[lca]=w[x] dep[u]−dep[lca]+dep[x]−dep[lca]=w[x].
由以上两个条件,可以发现:
对于式1, d e p [ u ] = w [ x ] + d e p [ x ] dep[u]=w[x]+dep[x] dep[u]=w[x]+dep[x],等号右边是定值,由上面的差分思路可知,我们只要找到所有在 x x x的子树中且深度为 d e p [ u ] dep[u] dep[u]的点的数量即可.
对于式2, d e p [ u ] − 2 ∗ d e p [ l c a ] = w [ x ] − d e p [ x ] dep[u]-2*dep[lca]=w[x]-dep[x] dep[u]−2∗dep[lca]=w[x]−dep[x],等号右边是定值,这里不方便找点,我们改为找一个桶的大小,只需要找到在子树中值为 d e p [ u ] − 2 ∗ d e p [ l c a ] dep[u]-2*dep[lca] dep[u]−2∗dep[lca]桶的大小即可.
那么思路就很明显了,我们开两组桶,每个桶里面储存的是恰好满足条件的路径的数量,比如说 b u c k e t 1 [ d e p ] bucket1[dep] bucket1[dep]就表示了起始点深度为 d e p dep dep的路径的多少, b u c k e t 2 [ d e p ] bucket2[dep] bucket2[dep]表示了满足 d e p [ u ] − 2 ∗ d e p [ l c a ] dep[u]-2*dep[lca] dep[u]−2∗dep[lca]的路径的数量多少.
为什么要开两个桶数组呢?因为对于每个点 x x x,我们要查询的桶都有两个, w [ x ] + d e p [ x ] w[x]+dep[x] w[x]+dep[x]和 w [ x ] − d e p [ x ] w[x]-dep[x] w[x]−dep[x].
此外,第二个数组可能发生 d e p [ u ] − 2 ∗ d e p [ l c a ] < 0 dep[u]-2*dep[lca]<0 dep[u]−2∗dep[lca]<0的情况,为了防止这种情况出现,我们还需要给第二个桶的容量统一加上 m a x n maxn maxn.
到这里这道题就基本做完了. 倍增求lca是树上差分的套路就不讲了 但是还需要考虑一些其他的东西:
1.当一个点同时满足以上两个式子的时候, a n s ans ans会重复计算,多算一次路径的贡献. (这里和普通树上差分不太一样)
上面的两个式子
d e p [ u ] − 2 d e p [ l c a ] = w [ x ] − d e p [ x ] dep[u]-2dep[lca]=w[x]-dep[x] dep[u]−2dep[lca]=w[x]−dep[x]
d e p [ u ] = w [ x ] + d e p [ x ] dep[u]=w[x]+dep[x] dep[u]=w[x]+dep[x]
因为 d e p [ u ] dep[u] dep[u]相同,所以联立得
w [ x ] − d e p [ x ] + 2 d e p [ l c a ] = w [ x ] + d e p [ x ] w[x]-dep[x]+2dep[lca]=w[x]+dep[x] w[x]−dep[x]+2dep[lca]=w[x]+dep[x],
d e p [ x ] = d e p [ l c a ] dep[x]=dep[lca] dep[x]=dep[lca],也就是说,如果 l c a lca lca满足其中一个式子,它必定满足另一个,所以如果 l c a lca lca满足一个式子, a n s [ l c a ] ans[lca] ans[lca]
要-1,以抵消多算一次的影响.
2.一个点的 a n s ans ans应该取未访问它的子树之前和访问它的子树之后的差值,以防止非同一子树的桶的元素造成的影响.
3.桶的大小要开够.
程序实现
#include<bits/stdc++.h>
#define maxn 300010
using namespace std;
struct edge{
int v,next;
}e[maxn<<1];
int head[maxn],tot;
void add(int u,int v){
e[++tot].v =v;
e[tot].next =head[u];
head[u]=tot;
}
int dep[maxn],fa[maxn][30];
void dfs1(int u,int pre){
dep[u]=dep[pre]+1;
fa[u][0]=pre;
for(int i=head[u];i;i=e[i].next ){
int v=e[i].v ;
if(v==pre)continue;
dfs1(v,u);
}
}
int LCA(int u,int v){
if(dep[u]<dep[v])swap(u,v);
for(int i=20;i>=0;i--){
if(dep[fa[u][i]]>=dep[v])u=fa[u][i];
}
if(u==v)return u;
for(int i=20;i>=0;i--){
if(fa[u][i]==fa[v][i])continue;
u=fa[u][i],v=fa[v][i];
}
return fa[u][0];
}
int ans[maxn],wt[maxn];
map<int ,int >add_start[maxn],add_to[maxn],lca_start[maxn],lca_to[maxn];
int bucket1[maxn<<1],bucket2[maxn<<1];
int sums[maxn],sumt[maxn],minus1[maxn],minus2[maxn];
void dfs2(int u,int pre){
int t1=bucket1[wt[u]+dep[u]],t2=bucket2[wt[u]-dep[u]+maxn];
for(int i=head[u];i;i=e[i].next ){
int v=e[i].v ;
if(v==pre)continue;
dfs2(v,u);
}
for(int i=1;i<=sums[u];i++)bucket1[add_start[u][i]]++;//差分为了保证准确性,通常都是先加后减
for(int i=1;i<=sumt[u];i++)bucket2[add_to[u][i]]++;//相应的桶++
ans[u]+=bucket1[wt[u]+dep[u]]-t1+bucket2[wt[u]-dep[u]+maxn]-t2;//计算增量
for(int i=1;i<=minus1[u];i++)bucket1[lca_start[u][i]]--;
for(int i=1;i<=minus2[u];i++)bucket2[lca_to[u][i]]--;//相应的桶--
}
int n,m;
int main(){
scanf("%d%d",&n,&m);
for(int i=1,u,v;i<n;i++){
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
for(int i=1;i<=n;i++)scanf("%d",&wt[i]);
dfs1(1,1);
for(int j=1;j<=20;j++)
for(int i=1;i<=n;i++)
fa[i][j]=fa[fa[i][j-1]][j-1];
for(int i=1,s,t,lca;i<=m;i++){
scanf("%d%d",&s,&t);
lca=LCA(s,t);
add_start[s][++sums[s]]=dep[s];//用map存:在这个点的位置桶dep[s]++
add_to[t][++sumt[t]]=dep[s]-2*dep[lca]+maxn;//同上
lca_start[lca][++minus1[lca]]=dep[s];//这个桶在lca的位置--
lca_to[lca][++minus2[lca]]=dep[s]-2*dep[lca]+maxn;
if(dep[lca]+wt[lca]==dep[s])ans[lca]--;//如果一个点是lca又满足条件,则要防止重复计算路径
}
dfs2(1,1);
for(int i=1;i<=n;i++)printf("%d ",ans[i]);
return 0;
}
题后总结
1.桶是非常重要的一种思想,桶可以用来存满足某个条件的数的数量,比较方便.
但是使用桶,就要求数据范围不是很大,否则占用空间就太多了.
对于全局桶的应用:记录它对某次计算的贡献可以统计他的增量.
2.对于树上的区间操作:
区间是否便于用线段树维护?是则使用树链剖分,否则使用差分或者差分思想.
使用差分思想:题意给出的(可能隐藏的)式子能否转换成便于差分维护/便于用差分思想维护(这种情况下通常是桶)的形式?