浅谈树dfs序与欧拉序与LCA结合的相关问题

浅谈树 d f s dfs dfs序与欧拉序与 L C A LCA LCA结合的相关问题

声明:

在本文章中, d f s 序 dfs序 dfs指dfs中点的入栈顺序,而出栈顺序表示该点子树的结尾区间

void dfs(int x,int fa){
	in[x]=++ti;
    ....
    out[x]=ti;
}

欧拉序长为 2 n 2n 2n,且一般记录对应位置的符号

void dfs(int x,int fa){
    in[x]=++ti;f[ti]=1;
    ...
    out[x]=++ti;f[ti]=-1
}

两者都可将子树线性化,只是面对不同问题,代码复杂度与维护难度稍有不同,基本上dfs序能做的,欧拉序都能做,有些问题我会列举出来,有些不会

诚然,里面很多问题树剖都可以替代dfs序和欧拉序,但是还是希望大家在遇到子树线性化与某点到根的链计算维护的时候可以先思考这两者,再考虑树剖,复杂的树链问题当然就使用树剖了

下面先给出关于这两者的7个经典问题及其解法

1.树上单点加,子树点权和

d f s dfs dfs序后每个位置维护每个点的权值,所以树状数组单点加,区间查询

2. x − > y x->y x>y树上链加,单点查询

离线树上差分, x + + , y + + , L C A [ ( x , y ) ] − − , f a t [ L C A ( x , y ) ] − − x++,y++,LCA[(x,y)]--,fat[LCA(x,y)]-- x++,y++,LCA[(x,y)],fat[LCA(x,y)],子树和查询,只是这里在线,所以 d f s dfs dfs序转化后,相等于1问题,链加变单点加,单点查询变区间查询,树状数组维护即可

3.x->y树上链加,子树和查询

d f s 序 dfs序 dfs后,修改同2,考虑修改A,查询B,当A是B的子树时,才有 ( d e p [ A ] − d e p [ B ] + 1 ) ∗ w (dep[A]-dep[B]+1)*w (dep[A]dep[B]+1)w,所以两个树状数组分别维护

( d e p [ x ] + 1 ) ∗ w (dep[x]+1)*w (dep[x]+1)w, w w w,答案为 S u m ( o u t [ B ] ) − S u m ( i n [ B ] − 1 ) − ( S u m 1 ( o u t [ B ] ) − S u m 1 ( i n [ B ] − 1 ) ) ∗ d e p [ B ] Sum(out[B])-Sum(in[B]-1)-(Sum1(out[B])-Sum1(in[B]-1))*dep[B] Sum(out[B])Sum(in[B]1)(Sum1(out[B])Sum1(in[B]1))dep[B]

4.单点加,x->y路径点权查询

两种方法:路径点权转化为 + x − > r t , + y − > r t , − ( L C A ( x , y ) − > r t ) , − ( f a t [ L C A ( x , y ) ] − > r t ) +x->rt,+y->rt,-(LCA(x,y)->rt),-(fat[LCA(x,y)]->rt) +x>rt,+y>rt,(LCA(x,y)>rt),(fat[LCA(x,y)]>rt)

(1)欧拉序后,点到根路径和等价于 S u m ( i n [ x ] ) Sum(in[x]) Sum(in[x]),修改就在 i n [ x ] in[x] in[x]位置 + = v a l +=val +=val, o u t [ x ] out[x] out[x]位置 − = w -=w =w就好了,树状数组维护, O ( l n ( 2 n ) ) O(ln(2n)) O(ln(2n))(更快更好写)

(2) d f s dfs dfs序后,每个位置直接保留 x − > r t x->rt x>rt的答案,则操作变成 [ i n [ x ] , o u t [ x ] ] [in[x],out[x]] [in[x],out[x]]区间加,单点查询,线段树维护, O ( l o g n ) O(logn) O(logn),

5.子树加,单点查询

d f s 序 dfs序 dfs后线段树 [ i n [ x ] , o u t [ x ] ] [in[x],out[x]] [in[x],out[x]]区间修改,单点查询

6.子树加,子树查询

同6差不多, d f s 序 dfs序 dfs后区间加,区间和,线段树即可

7.子树加,链权值查询

链权值查询转为4个点到根查询, d f s dfs dfs序后每个位置直接保留 x − > r t x->rt x>rt的权值和,子树加对于x的子树中y点来说, + = ( 1 − d e p [ x ] ) ∗ v a l + v a l ∗ d e p [ y ] +=(1-dep[x])*val+val*dep[y] +=1dep[x])val+valdep[y],两个树状数组分别维护 ( 1 − d e p [ x ] ) ∗ v a l (1-dep[x])*val (1dep[x])val v a l val val,区间修改单点查询差分转单点修改区间前缀和查询即可

答案就是 S u m 1 ( i n [ x ] ) + S u m 2 ( i n [ x ] ) ∗ d e p [ x ] Sum1(in[x])+Sum2(in[x])*dep[x] Sum1(in[x])+Sum2(in[x])dep[x]

8.链路径加,单点查询,子树查询

比3多了个查询,直接在维护的其中一个树状数组上查就好了

9.单点加,子树加,链权值查询

7 7 7,多个单点加,只是在 S u m 1 Sum1 Sum1这个树状数组内一样做个差分修改即可

我们可以发现对于树上的基本操作,单点加,单点查询,子树加,子树查询,链加,链查询,基本上只要查询或者修改只有一种操作,就可以用 d f s 序 dfs序 dfs来维护,不用写树剖,而一般更为常见于子树问题,当然我觉得很多问题线段树合并都可以暴力做x

下面是几道相关的题目

1.bzoj4034

题意:树上单点加,子树加,路径和

思路:就是9,随便写,当然只是个树剖模板题,带多个 l o g log log而已

#include<bits/stdc++.h>
#define lson p<<1,l,mid
#define rson p<<1|1,mid+1,r 
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=1e5+5;
typedef long long ll;
int a[maxn],head[maxn],ver[maxn<<1],next1[maxn<<1],tot,ti,in[maxn],out[maxn],val[maxn<<1],f[maxn<<1];
struct SegmentTree{
    ll sum[maxn<<3],add[maxn<<3];
    void pushDown(int p,int l,int r){
        int mid=l+r>>1;
        sum[ls]+=1ll*(f[mid]-f[l-1])*add[p];
        sum[rs]+=1ll*(f[r]-f[mid])*add[p];
        add[ls]+=add[p];add[rs]+=add[p];
        add[p]=0;   
    }
    void update(int p,int l,int r,int L,int R,int val){
        if(L<=l&&r<=R){
            sum[p]+=1ll*(f[r]-f[l-1])*val;
            add[p]+=val;
            return;
        }
        if(add[p])pushDown(p,l,r);
        int mid=l+r>>1;
        if(L<=mid)update(lson,L,R,val);
        if(R>mid)update(rson,L,R,val);
        pushUp(p);
    }
    void pushUp(int p){
        sum[p]=sum[ls]+sum[rs];
    }
    void build(int p,int l,int r){
        add[p]=0;
        if(l==r){
            sum[p]=val[l];return;
        }
        int mid=l+r>>1;
        build(lson);
        build(rson);
        pushUp(p);
    }
    ll query(int p,int l,int r,int L,int R){
        if(L<=l&&r<=R)return sum[p];
        if(add[p])pushDown(p,l,r);
        int mid=l+r>>1;
        ll ans=0;
        if(L<=mid)ans+=query(lson,L,R);
        if(R>mid)ans+=query(rson,L,R);
        return ans;
    }
}tr;
void dfs(int x,int fa){
    in[x]=++ti;
    val[ti]=a[x];f[ti]=1;
    for(int i=head[x];i;i=next1[i]){
        int y=ver[i];
        if(y==fa)continue;
        dfs(y,x);
    }
    out[x]=++ti;
    val[ti]=-a[x];f[ti]=-1;
}
void add(int x,int y){
    ver[++tot]=y,next1[tot]=head[x],head[x]=tot;    
}
int main(){
    int n,m,q,op;
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    int x,y;
    for(int i=1;i<n;++i){
        scanf("%d%d",&x,&y);
        add(x,y);
        add(y,x);
    }
    dfs(1,0);
    for(int i=1;i<=ti;++i)f[i]+=f[i-1];
    tr.build(1,1,ti);
    for(int i=1;i<=q;++i){
        scanf("%d",&op);
        if(op==1){
            scanf("%d%d",&x,&y);
            tr.update(1,1,ti,in[x],in[x],y);
            tr.update(1,1,ti,out[x],out[x],y);
        }else if(op==2){
            scanf("%d%d",&x,&y);
            tr.update(1,1,ti,in[x],out[x],y);      
        }else if(op==3){
            scanf("%d",&x);
            cout<<tr.query(1,1,ti,1,in[x])<<"\n";
        }
    }
    return 0;
}

2.hdu5692

题意:求一条从根出发必经x点的最大权值路径

思路:子树问题,想到与 d f s dfs dfs序有关,显然一个点权值只对他的子树有贡献,直接dfs序建线段树,区间更新区间最大值即可

#pragma comment(linker, "/STACK:1024000000,1024000000")

#include<iostream>
#include<cstdio>
#include<algorithm>
#define lson p<<1,l,mid 
#define rson p<<1|1,mid+1,r 
#define ls p<<1  
#define rs p<<1|1
using namespace std;
const int maxn=1e5+5;
typedef long long ll;
int t,n,m,x,y,head[maxn],ver[maxn<<1],next1[maxn<<1],ti,in[maxn],out[maxn],op,a[maxn],tot;
void add(int x,int y){
    ver[++tot]=y,next1[tot]=head[x],head[x]=tot;
}
struct SegmentTree{
    ll mx[maxn<<2],add[maxn<<2];
    void pushDown(int p,int l,int r){
        int mid=l+r>>1;
        mx[ls]+=add[p];
        mx[rs]+=add[p];
        add[ls]+=add[p];add[rs]+=add[p];
        add[p]=0;   
    }
    void pushUp(int p){
        mx[p]=max(mx[ls],mx[rs]);
    }
    void update(int p,int l,int r,int L,int R,int val){
        if(L<=l&&r<=R){
            mx[p]+=val;
            add[p]+=val;return;
        }
        if(add[p])
            pushDown(p,l,r);
        int mid=l+r>>1;
        if(L<=mid)update(lson,L,R,val);
        if(R>mid)update(rson,L,R,val);
        pushUp(p);
    }
    void build(int p,int l,int r){
        add[p]=0;
        if(l==r){
            mx[p]=0;return;
        }
        int mid=l+r>>1;
        build(lson);
        build(rson);
        pushUp(p);
    }
    ll query(int p,int l,int r,int L,int R){
        if(L<=l&&r<=R)return mx[p];
        int mid=l+r>>1;
        if(add[p])
            pushDown(p,l,r);
        ll ans=-1e18;
        if(L<=mid)ans=max(ans,query(lson,L,R));
        if(R>mid)ans=max(ans,query(rson,L,R));
        return ans;
    }
}tr;
void dfs(int x,int fa){
    in[x]=++ti;
    for(int i=head[x];i;i=next1[i]){
        int y=ver[i];
        if(y==fa)continue;
        dfs(y,x);
    }
    out[x]=ti;
}
int main(){
    scanf("%d",&t);
    int T=0;
    while(t--){
ti=0;
        scanf("%d%d",&n,&m);
        tot=0;//清空
        for(int i=0;i<=n;++i)head[i]=0;
        for(int i=1;i<n;++i){
            scanf("%d%d",&x,&y);
            add(x,y);
            add(y,x);
        }
        cout<<"Case #"<<++T<<":\n";
        dfs(0,-1);
        tr.build(1,1,n);
        for(int i=0;i<n;++i){
            scanf("%d",&a[i]);
            tr.update(1,1,n,in[i],out[i],a[i]);
        }
        for(int i=1;i<=m;++i){
            scanf("%d",&op);
            if(!op){
                scanf("%d%d",&x,&y);
                tr.update(1,1,n,in[x],out[x],y-a[x]);
                a[x]=y;
            }else{
                scanf("%d",&x);
                cout<<tr.query(1,1,n,in[x],out[x])<<"\n";
            }
        }
    }
    return 0;
}

3.hdu 5877

题意:给一颗有根树,点有非负权值,问有多少对 u , v u,v u,v满足 u u u v v v的祖先且 a u ∗ a v ≤ k a_u*a_v\leq k auavk

思路:

第一个条件显然是个一维偏序关系,可以考虑dfs直接化去,这题可以考虑一个点只对子树有贡献,或者一个点只被祖先贡献,这里我采取后者, d f s dfs dfs的时候查询 < = k / a v <=k/a_v <=k/av的个数,然后单点增加,回溯的时候再单点减少即可。所以只需要离散化后用树状数组维护即可,当然也可以在回溯的时候考虑第一种方法,考虑每个点对子树的贡献,直接在 d f s dfs dfs序上单点修改,区间查询即可

这题需要注意,当 a [ i ] a[i] a[i]为0的时候, k / a [ i ] k/a[i] k/a[i]应该设为无穷

当然这题是子树问题,也可以用线段树合并暴力自底向下统计

#include<bits/stdc++.h>
#define lowbit(x) (x&(-x))
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const ll INF=2e18;
int t,ver[maxn<<1],head[maxn],next1[maxn<<1],tot,n,m,deg[maxn];
ll a[maxn<<1],b[maxn<<1],ans=0,k;
void add(int x,int y){
    ver[++tot]=y,next1[tot]=head[x],head[x]=tot;
}
int ask(ll x){
    return lower_bound(b+1,b+1+m,x)-b;
}
struct BIT{
    int c[maxn<<1],N;
    void init(int x){
        this->N=x;
        for(int i=0;i<=x;++i)c[i]=0;
    }
    int query(int x){
        int ans=0;
        while(x){
            ans+=c[x];
            x-=lowbit(x);
        }
        return ans;
    }
    void add(int x,int val){
        while(x<=N){
            c[x]+=val;
            x+=lowbit(x);
        }
    }
}bit;
void dfs(int x,int fa){
    int pos=(a[x]?ask(k/a[x]):m),pos2=ask(a[x]);
    ans+=bit.query(pos);
    bit.add(pos2,1);
    for(int i=head[x];i;i=next1[i]){
        int y=ver[i];
        if(y==fa)continue;
        dfs(y,x);
    }
    bit.add(pos2,-1);
}
int main(){
    scanf("%d",&t);
    while(t--){
        ans=0;
        tot=0;
        scanf("%d%lld",&n,&k);
        for(int i=1;i<=n;++i)head[i]=0,deg[i]=0;
        for(int i=1;i<=n;++i){
            scanf("%lld",&a[i]);
            if(!a[i])a[i+n]=INF;
            else a[i+n]=k/a[i];
            b[i]=a[i];
            b[i+n]=a[i+n];
        }
        for(int i=1;i<n;++i){
            int x,y;
            scanf("%d%d",&x,&y);
            add(x,y);
            add(y,x);
            deg[y]++;
        }
        int rt=0;
        for(int i=1;i<=n;++i){
            if(!deg[i]){rt=i;break;}
        }
        sort(b+1,b+1+2*n);
        m=unique(b+1,b+1+2*n)-(b+1);
        bit.init(m);
        dfs(rt,0);
        cout<<ans<<'\n';
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值