【线段树+DFS序】CQYZ_Vijos_P3751 树上路径动态查询

【问题描述】

  现在给你一棵树,共有n个节点,由1至n编号,每条边有一个权值。现在你要完成一些指令,这些指令包括:
  1、A i j v:将树边(i,j)的权值增加v。
  2、Q a b:询问节点a到节点b的路径长度。

【输入格式】

  第1行为两个整数n,m,表示树的节点数目和指令数目。接下来的n-1行每行分别有3个整数u,v,w,u和v两个整数表示该边的节点编号,w表示边的权值。然后是m条指令,指令格式见题目描述。

【输出格式】

  对于每个查询指令,输出一个答案。

【输入样例】

6 3
1 2 3
2 5 2
6 2 1
3 1 4
1 4 5
Q 3 5
A 1 2 3
Q 6 4

【输出样例】

9
12

【数据范围】

  1 < n,m <= 100000
  任何时候,边权值为正整数,且不超过2*10^9。
  

题解:

  乍一看是一道难题,仔细一看还不是一般的难题!询问节点a和b的距离比较好做,先生成每个点到根节点的距离dist,再求一下最近公共祖先LCA(a,b)就行了。但将一条边的权值增加v后,这条边所连接的子树上每一个点的dist都要增加v,暴力维护dist的时间复杂度是相当高的,所以最后的难点落在了如何维护dist上
  我们将维护dist抽象成这样一个步骤:给定一个值v,使一个集合(也就是一颗子树)内的所有元素的值增加v。在竞赛范围内,我曾经接触过的用于维护子集合的值的数据结构,线段树和树状数组勉强算得上(序列也是集合嘛),但是如何吧树上的值转换到序列上去呢?
  这里介绍个东西:DFS序!DFS序,简单地说就是一棵树DFS遍历的顺序(一般是先序遍历)。如图:
  样例用树
  我们对这棵树从左往右先序遍历后,得到的节点编号序列为:
  1、2、5、6、9、10、11、3、7、4、8
  观察序列我们不难发现,以i为根节点的子树(包括i),在DFS序中是一段连续的区间。例:以2为根的子树,节点有2、5、6、9、10、11,在我们所得到的序列中是连续的一段,这是由DFS遍历的方式决定的
  于是,我们就可以把对子树的修改转化为对序列上一个区间内元素的修改了!
  具体实现方法可以用树状数组或线段树,这里选用了线段树

  代码实现:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn=100005;
struct edge{
    int to,next,w;
}e[maxn<<1];
int first[maxn]={0},np=0;
int fa[maxn][20]={0},dep[maxn]={0};
int l[maxn]={0},r[maxn]={0},idx[maxn]={0};
int n,m,tot=0;
int lc[maxn<<1],rc[maxn<<1],A[maxn],rt,nnp=0;
ll dist[maxn]={0},mv[maxn<<1],add[maxn<<1];
inline int in(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'|ch>'9'){if(ch=='-') f=-f;ch=getchar();}
    while(ch<='9'&&ch>='0') x=x*10+ch-'0',ch=getchar();
    return x*f;
}
void addedge(int u,int v,int w){
    e[++np]=(edge){v,first[u],w},first[u]=np;
}
void DFS(int i,int f){
    fa[i][0]=f;
    for(int k=1;k<=18;k++) fa[i][k]=fa[fa[i][k-1]][k-1];
    //l[i],r[i]表示以i为根的子树在DFS序上的区间,idx[i]表示DFS序上第i个元素对应的树上节点编号
    l[i]=++tot,idx[tot]=i;
    for(int p=first[i];p;p=e[p].next){
        int j=e[p].to,c=e[p].w;
        if(j==f) continue;
        dist[j]=dist[i]+c,dep[j]=dep[i]+1;
        //dist[i]表示i到根节点的距离,dep[i]表示i的深度
        DFS(j,i);
    }
    r[i]=tot;
}
//............................线段树实现.............................
void build(int &now,int L,int R){
    now=++nnp;
    if(L==R){
        mv[now]=dist[idx[L]];
        return;
    }
    int m=(L+R)>>1;
    build(lc[now],L,m);
    build(rc[now],m+1,R);
}
void pushdown(int now){     //下传lazy操作
    mv[lc[now]]+=add[now],mv[rc[now]]+=add[now];
    add[lc[now]]+=add[now],add[rc[now]]+=add[now];
    add[now]=0;
}
void update(int now,int L,int R,int i,int j,int d){
    if(L>=i&&R<=j){
        add[now]+=d,mv[now]+=d;
        return;
    }
    pushdown(now);
    int m=(L+R)>>1;
    if(j<=m) update(lc[now],L,m,i,j,d);
    else if(i>m) update(rc[now],m+1,R,i,j,d);
    else{
        update(lc[now],L,m,i,m,d);
        update(rc[now],m+1,R,m+1,j,d);
    }
}
ll query(int now,int L,int R,int i){
    if(L==i&&R==i) return mv[now];    //单点查找
    pushdown(now);
    int m=(L+R)>>1;
    if(i<=m) return query(lc[now],L,m,i);
    else return query(rc[now],m+1,R,i);
}
//..........................线段树实现完成...........................
int LCA(int u,int v){
    if(dep[u]<dep[v]) swap(u,v);
    int x=dep[u]-dep[v];
    for(int k=18;k>=0;k--) if(x&(1<<k)) u=fa[u][k];
    if(u==v) return u;
    for(int k=18;k>=0;k--) if(fa[u][k]!=fa[v][k]) u=fa[u][k],v=fa[v][k];
    return fa[u][0];
}
int main(){
    //freopen("in.txt","r",stdin);
    n=in(),m=in();
    int u,v,w;
    for(int i=1;i<n;i++){
        u=in(),v=in(),w=in();
        addedge(u,v,w);
        addedge(v,u,w);
    }
    DFS(1,0);
    build(rt,1,n);
    char op[5];
    ll du,dv,dz;
    int z;
    for(int i=1;i<=m;i++){
        scanf("%s",op);
        if(op[0]=='A'){
            u=in(),v=in(),w=in();
            if(dep[u]<dep[v]) update(rt,1,n,l[v],r[v],w);
            else update(rt,1,n,l[u],r[u],w);
        }
        else{       //两点间距离
            u=in(),v=in();
            z=LCA(u,v);
            du=query(rt,1,n,l[u]);
            dv=query(rt,1,n,l[v]);
            dz=query(rt,1,n,l[z]);
            printf("%lld\n",du+dv-2*dz);
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值