D157 最近公共祖先+树链剖分+数状数组 P4949 最短距离

D157 最近公共祖先+树链剖分+数状数组 P4949 最短距离_哔哩哔哩_bilibili

 

P4949 最短距离 - 洛谷

n 个点 n 条边的无向连通图, m 次操作。支持两种操作:1. 修改 第 x 条边的长度为 y;2. 查询 点 x 到点 y 的最短距离。

思路

n 个点 n 条边的无向连通图就是基环树(即环套树),直接考虑不好做

如果砍掉环上的一条边,变成一棵树,该怎样做呢?

树上求两点之间的距离,经典做法就是 $d(x,y)=d[x]+d[y]−2∗d[lca]$,其中 d[i] 为各点到根的距离,可以预处理

修改边权 $(x,y)$ 操作,实际上会影响以 $y$ 为根的子树内每个点的 $d$ 值,是区间修改

经典做法就是用树链剖分,把树上区间转化为线性区间,然后用线段树或树状数组,$logn$ 的时间维护 $d$ 值的区间修改

然后,考虑环上的那条断边对修改和查询有什么影响?

做树链剖分时,顺便找出环上的一条边断掉,记住断边 $(u,v,w)$

如果修改的是断边,直接更改其边权;如果不是断边,用 树状数组 维护区间修改,就在区间两端做一下增量的差分

如果是查询 $(x,y)$,答案是 $min \{ d(x,y),\;d(x,u)+d(y,v)+w,\;d(x,v)+d(y,u)+w \}$,如图

 

相关板子:

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

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

D28 基环树 树形DP P2607 [ZJOI2008] 骑士 - 董晓 - 博客园

D29 基环树 树的直径 P1399 [NOI2013] 快餐店 - 董晓 - 博客园

D30 基环树 遍历最小字典序 P5022 [NOIP2018 提高组] 旅行 - 董晓 - 博客园

 

// 最近公共祖先+树链剖分+数状数组
#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N=100010;
int idx=1,h[N],to[N<<1],ww[N<<1],ne[N<<1],del;
void add(int x,int y,int z){
  to[++idx]=y;ww[idx]=z;ne[idx]=h[x];h[x]=idx;
}
struct node{
  int x,y,z,id; //给每一条边编号
}e[N];
int n,m;
int dfn[N],siz[N],dep[N],son[N],fa[N],top[N];
ll d[N];
bool vis[N];

void dfs1(int x,int f){ //树链剖分 更新dep,fa,son,siz,d,e
  dep[x]=dep[f]+1; fa[x]=f; siz[x]=1;
  for(int i=h[x]; i; i=ne[i]){
    int y=to[i];
    if(y==f) continue;
    if(vis[y]){del=i>>1; continue;} //del记录那条断环边
    vis[y]=true;
    e[i>>1].id=i; //记录树边的编号i/2
    d[y]=d[x]+ww[i]; //d:记录y点到根的距离
    dfs1(y,x);
    siz[x]+=siz[y];
    if(siz[y]>siz[son[x]]) son[x]=y;
  }
}
void dfs2(int x,int t){ //树链剖分 更新dfn,top
  dfn[x]=++dfn[0]; //dfs序,用于BIT的下标
  top[x]=t;
  if(son[x]) dfs2(son[x],t); //搜重儿子
  for(int i=h[x]; i; i=ne[i]){
    int y=to[i];
    if(y==fa[x]||y==son[x]||
       y==e[del].x&&x==e[del].y||
       y==e[del].y&&x==e[del].x) continue; //排除断环边
    dfs2(y,y); //搜轻儿子
  }
}
int lca(int x,int y){ //求lca
  while(top[x]!=top[y]){
    if(dep[top[x]]<dep[top[y]]) swap(x,y);
    x=fa[top[x]];
  }
  return dep[x]>dep[y]?y:x;
}

struct BIT{ //数状数组
  ll s[N];
  void upd(int x,int C){ //点更新
    for(;x<=n;x+=x&-x) s[x]+=C;
  }
  void upd(int x,int y,int C){ //差分更新
    upd(x,C); upd(y+1,-C);
  }
  void change(int u,int C){ //区间更新
    upd(dfn[u], dfn[u]+siz[u]-1, C);
  }
  ll ask(int x){ //前缀和
    ll sum=0;
    for(;x;x-=x&-x) sum+=s[x];
    return sum;
  }
  ll dis(int u,int v){ //两点之间的距离
    return ask(dfn[u])+ask(dfn[v])-2*ask(dfn[lca(u,v)]);
  }
}B;

int main(){
  ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1,x,y,z; i<=n; i++){
    cin>>x>>y>>z;
    add(x,y,z); add(y,x,z);
    e[i]={x,y,z,0};
  }
  
  vis[1]=true; //标记访问1
  dfs1(1,0);
  dfs2(1,1); //树链剖分
  for(int i=1; i<=n; i++) B.upd(dfn[i],dfn[i],d[i]); //初始化BIT
  for(int op,x,y;m--;){
    cin>>op>>x>>y;
    if(op==1){
      if(x==del){
        e[x].z=y; //对断环边只更新边权
        continue;
      }
      int dy=y-e[x].z; //把修改的边权转化为增加量
      e[x].z=y;        //记录新边权
      int v=to[e[x].id]; //第x条边的终点
      B.change(v,dy); //v子树增加点权
    }
    else{
      ll d1=B.dis(x,y);
      ll d2=B.dis(x,e[del].x)+B.dis(y,e[del].y)+e[del].z;
      ll d3=B.dis(x,e[del].y)+B.dis(y,e[del].x)+e[del].z;
      cout<<min({d1,d2,d3})<<'\n';
    }
  }
}

 

posted @ 2026-05-16 23:26  董晓  阅读(69)  评论(0)    收藏  举报