D60 树的直径 树剖+树状数组+线段树 P6845 [CEOI 2019] Dynamic Diameter

D60 树的直径 树剖+树状数组+线段树 P6845 [CEOI 2019] Dynamic Diameter_哔哩哔哩_bilibili

 

P6845 [CEOI 2019] Dynamic Diameter - 洛谷

给了一颗带权树,每次更改一条边的边权,每次修改后求出最大直径。强制在线。

思路

性质:更改一条边的边权后求直径,相当于合并两颗树的直径。新直径的端点一定是原四个端点中的两个。

  • 如何支持在线修改边权?

由 树上前缀和 可知,两点间距离为 dis(x)+dis(y)−2∗dis(lca(x,y)) 。

把一条边的边权 w 更改为 v,实际上是把子树内的节点到子树外的节点的距离增加 (v-w),那么子树就是区间加。

我们先做树链剖分,然后对原树节点的 DFS 序(子树区间编号连续)用树状数组维护修改和查询。

  • 如何维护新树的直径端点?

我们先做树链剖分,然后对原树节点的 DFS 序(子树区间编号连续)用线段树维护子树内的直径端点。

发现子树内和子树外的最大直径不会变化,只需要更改子树内与外之间合并的最大直径。

在合并子树内与外的直径时,只要覆盖就立刻返回(因为子树内的直径不变),这样单次区修就可以做到 O(log2n) 的复杂度。

如图,红色数字为原树节点的 DFS 序编号。修改 1~2 之间的边权,会影响以 2 为根的子树内与子树外的节点之间的直径。

对树状数组做点差分,通过查询前缀和,计算新直径。对线段树做区修,pushup 更新新直径的端点。总的时间复杂度为 O(qlog2n)。

   

                                             

如果以上分析看不懂,可能需要先复习一下这 4 个板子:

D11【模板】树链剖分 最近公共祖先(LCA) - 董晓 - 博客园

D12【模板】重链剖分/树链剖分 - 董晓 - 博客园

C81【模板】树状数组 点修+区查 区修+点查 - 董晓 - 博客园

C02【模板】线段树+懒标记 P3372 线段树 1 - 董晓 - 博客园

 

// 树剖+树状数组+线段树 O(qlognlogn)
#include<bits/stdc++.h>
using namespace std;

long long read(){
  long long x=0; int f=0; char c=getchar();
  while(!isdigit(c)) f|=c=='-',c=getchar();
  while(isdigit(c)) x=x*10+c-'0',c=getchar();
  return f?-x:x;
}

const int N=100005;
int n,q,idx=1,hd[N],to[N<<1],ne[N<<1],go[N];
long long W,w[N],d[N];
void add(){
  int a=read(),b=read();
  to[++idx]=b,ne[idx]=hd[a],hd[a]=idx;
  to[++idx]=a,ne[idx]=hd[b],hd[b]=idx;
  w[idx>>1]=read(); //记录边权,保证正反边除2后是同一条边的编号{2,3}/2=1,{4,5}/2=2
}

//树剖: 预处理原树节点的 LCA; DFS序; rnk
int dep[N],fa[N],top[N],siz[N],son[N],dfn[N],rnk[N];
void dfs1(int x,int f){
  fa[x]=f; dep[x]=dep[f]+1; siz[x]=1;
  for(int y,i=hd[x];i;i=ne[i])if((y=to[i])!=f){
    d[y]=d[x]+w[i>>1]; //记录从根到y的距离
    dfs1(y,x);
    siz[x]+=siz[y];
    if(siz[son[x]]<siz[y]) son[x]=y;
    go[i>>1]=y; //记录当前边的出点是y。边权下移给点权,映射为树状数组和线段树,方便区修
  }
}
void dfs2(int x,int t){
  top[x]=t; dfn[x]=++dfn[0]; rnk[dfn[0]]=x; //链顶; DFS序; DFS序的逆映射
  if(son[x]) dfs2(son[x],t);
  for(int y,i=hd[x];i;i=ne[i])if((y=to[i])!=fa[x] && y!=son[x]) dfs2(y,y);
}
int lca(int x,int y){
  while(top[x]!=top[y]) dep[top[x]]>dep[top[y]]?(x=fa[top[x]]):(y=fa[top[y]]);
  return dep[x]<dep[y]?x:y;
}

struct BIT{ //树状数组维护点权的差分、前缀和、求直径,节点对应 DFS序
  long long c[N];
  void upd(int x,long long k){ //点加
    for(;x<=n;x+=x&-x) c[x]+=k;
  }
  void upd(int x,int y,long long w){ //差分点加
    upd(x,w), upd(y+1,-w);
  }
  long long ask(int x){ //求前缀和
    long long s=d[rnk[x]];
    for(;x;x-=x&-x) s+=c[x];
    return s;
  }
  long long dis(pair<int,int> a){ //求直径
    return ask(dfn[a.first])+ask(dfn[a.second])-2*ask(dfn[lca(a.first,a.second)]);
  }    
}B;

struct SGT{ //线段树维护子树区间的直径端点,节点对应 DFS序
  #define ls (u<<1)
  #define rs (u<<1|1)
  #define mid ((l+r)>>1)
  #define pii pair<int,int>
  pii pt[N<<2]; //保存每颗子树的直径的两个端点
  
  pii pushup(const pii& a, const pii& b){ //合并两子树的直径端点
    pii p[6]={a,b,{a.first,b.first},{a.first,b.second},
                  {a.second,b.first},{a.second,b.second}};
    long long d[6];
    for(int i=0;i<6;i++) d[i]=B.dis(p[i]); //计算每对点之间的距离
    return p[max_element(d,d+6)-d]; //查找指定范围内的最大元素(即直径的端点)
  }
  
  void build(int u=1,int l=1,int r=n){ //建线段树
    if(l==r) return pt[u]={rnk[l],rnk[l]},void();
    build(ls,l,mid),build(rs,mid+1,r);
    pt[u]=pushup(pt[ls],pt[rs]);
  }
  void upd(int x,int y,int u=1,int l=1,int r=n){ //区修
    if(x>r || y<l) return;   //越界返回
    if(x<=l && r<=y) return; //覆盖立刻返回,保证一次区修为logn。因为子树内部的直径不受子树根权的影响
    upd(x,y,ls,l,mid);
    upd(x,y,rs,mid+1,r);
    pt[u]=pushup(pt[ls],pt[rs]); //直径的两个端点维护到树根上
  }
}S;

signed main(){
  n=read(),q=read(),W=read(); //点的个数,询问的个数,边权的上限
  for(int i=1;i<n;i++) add();    
  dfs1(1,0);
  dfs2(1,1); //树剖
  S.build(); //建线段树
  long long last=0;
  while(q--){
    int i=(read()+last)%(n-1)+1; //第 i 条边
    long long v=(read()+last)%W; //新边权
    B.upd(dfn[go[i]], dfn[go[i]]+siz[go[i]]-1, v-w[i]); //更新边权:边权给点权,原点权w加(v-w),即修改为v
    w[i]=v; //将第 i 条边的边权改为 v
    S.upd(dfn[go[i]], dfn[go[i]]+siz[go[i]]-1); //更新直径端点
    printf("%lld\n",last=B.dis(S.pt[1]));       //计算新直径长度
  }
}

 

posted @ 2026-02-04 09:51  董晓  阅读(90)  评论(0)    收藏  举报