树上那些事——树上问题 I (树的直径、性质、重心、差分、DFN 序)

树上那些事——树上问题 I

树论,作为图论的一个分支,讨论的是树这一有着许多优秀性质的图。

树有那些优秀的性质呢?

树的秘密

现实中的树都有根,这里的树也有吗?

不一定,但是无论如何,树是这样的一个图:

  • \(n\) 个节点,\(n-1\) 条边的无向连通图
  • 没有环的无向连通图。
  • 任意两个节点之间均只有一条简单路径的无向连通图。

若一棵树没有根,我们称这种树为 无根树,有根则称 有根树。树的根规定了树上节点之间的父子关系。

树都有叶子,在这里,我们把树的叶子定义为与这个点相连的边的数量不超过一(即度数不超过一)的点。

树的每条茎秆都是在另一个茎秆的基础上长出的,定义一个树的父亲节点为从该结点到根路径上的第二个结点。根结点没有父结点。

认祖归宗,衣锦还乡,定义一个点的祖先为这个结点到根结点的路径上,除了它本身外的结点。根节点没有祖先。

子承父业,昌盛后嗣,定义一个点的子树为删掉与父亲相连的边后,该结点所在的子图。

如此兴旺的家族,怎能没有自己的族谱?在计算机中存树的方法有多种,但用 vector 建图就够用了。

序列上的问题我们常常用推向前缀或差分数组的方式解决,这是因为序列有一个共同的起点。但在树上,我们如果还想用这种操作维护两个点之间的关系,寻找两个节点之间的最近公共祖先显然是必须的。

树上最近公共祖先问题

如果能从一个图中提取出一棵树,是不是更有利于维护一些问题了呢?

最小生成树问题

问题时间!

P9433 [NAPC-#1] Stage5 - Conveyors - 洛谷]

考虑两两关键点之间的路径,由于树的性质,你会发现,不论你从哪一个关键点出发,以什么样的顺序经过各个关键点,只要不重不漏的经过了每个关键点并且回到了出发点,你走过的路程一定是两倍的 走过的边权值的和。

这样,问题似乎不怎么复杂了,现在我们要求的,就是出发点和结束点各自到最近的一个关键点的距离,吗?

问题在于,你要从 \(s\)\(t\),而非回到原来的节点,在途径关键节点部分的时候,有一段路你是不用重复走的。问题是,这段不用重复走的路有多长?而且,走过的边权值的和 ,由于这是一棵无根树,很好算吗?

为了解决这些问题,我们任取一个关键点为根。这样你会发现,关键点们构成了一个包含根节点的连通块。找这个连通块很简单,一遍从根开始的 DFS 即可。

而且,这样也方便了我们寻找 出发点和结束点各自到最近的一个关键点。考虑倍增。我们把在连通块内的节点标记,向上跳时的限制改一下就成了。

当然了,LCA 还是要求的,因为我们能省下的路程显然是这两个关键点之间的路径,也很容易求出。且此时,如果节点已经是连通块内的一个节点,就不再需要先跳到连通块里,省下的路程是这起点和终点之间距离。

把求出的这些东西算一算,你会很惊喜的发现,这样的维护方法不需要讨论这些点与连通块的关系,这题直接就做完啦!

code:

Show me the code
 #define psb push_back
 #define mkp make_pair
 #define rep(i,a,b) for( int i=(a); i<=(b); ++i)
 #define per(i,a,b) for( int i=(a); i>=(b); --i)
 #define rd read()
 #include<bits/stdc++.h>
 using namespace std;
 typedef long long ll;
 ll read(){
   ll x=0,f=1;
   char c=getchar();
   while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
   while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
   return x*f;
 }
 const int N=1e5+5;
 const int lgr=25;
 int n,q,k;int root;
 struct e{
   int v;
   int w;
 };
 vector<e> edge[N]; bool key[N];bool vis[N];int tot=0;
 bool dfs1(int u,int fa){
   bool sa=key[u];
   for(int i=0;i<edge[u].size();i++){
     int v=edge[u][i].v;
     int w=edge[u][i].w;
     if(v==fa)continue;
     sa=sa|dfs1(v,u);
   }
   if(sa==1)vis[u]=1;
   return sa;
 }
 void dfs2(int u,int fa){
   for(int i=0;i<edge[u].size();i++){
     int v=edge[u][i].v;
     int w=edge[u][i].w;
     if(v==fa)continue;
     dfs2(v,u);
     if(vis[v]&&vis[u])tot+=w;
   }
   return ;
 }
 int dep[N];int fa[N][30];int dis[N];int dist[N][30];
 void bfs(int r){
   memset(dep,0x3f,sizeof dep);
   dep[r]=1;dep[0]=0;
   fa[r][0]=0;fa[0][0]=0;
   dis[r]=0;
   queue<int> q;
   q.push(r);
   while(q.size()){
     int u=q.front();
     q.pop();
     for(int i=0;i<edge[u].size();i++){
       int v=edge[u][i].v;
       int w=edge[u][i].w;
       if(dep[v]>dep[u]+1){
         dep[v]=dep[u]+1;
         dis[v]=dis[u]+w;
         fa[v][0]=u;
         q.push(v);
         for(int j=1;j<=lgr;j++)fa[v][j]=fa[fa[v][j-1]][j-1];
       }
     }
   }
   return ;
 }
 int getf(int u){
   if(vis[u])return u;
   for(int i=lgr;i>=0;i--)if(!vis[fa[u][i]])u=fa[u][i];
   u=fa[u][0];
   return u;
 }
 int lca(int u,int v){
   if(u==v)return u;
   if(dep[u]<dep[v])swap(u,v);
   for(int i=lgr;i>=0;i--)if(dep[fa[u][i]]>=dep[v])u=fa[u][i];
   if(u==v)return u;
   for(int i=lgr;i>=0;i--)if(fa[u][i]!=fa[v][i]){u=fa[u][i];v=fa[v][i];}
   return fa[u][0];
 }
 int main(){
 
   cin>>n>>q>>k;
   for(int i=1;i<n;i++){
     int u,v,w;
     cin>>u>>v>>w;
     edge[u].push_back(e{v,w});
     edge[v].push_back(e{u,w});
   }
   for(int i=1;i<=k;i++){
     int u;
     cin>>u;root=u;
     key[u]=1;
   }
   vis[0]=1;
   dfs1(root,-1); dfs2(root,-1); bfs(root);
   for(int i=1;i<=q;i++){
     int s,t;
     cin>>s>>t;
     int u=getf(s),v=getf(t);
     int lcaa=lca(u,v);
     int ls=dis[s]-dis[u],lt=dis[t]-dis[v];
     int lu=dis[u]-dis[lcaa],lv=dis[v]-dis[lcaa];
     cout<<tot*2-lu-lv+ls+lt<<'\n';
   }
 
   return 0;
 }

P9432 [NAPC-#1] rStage5 - Hard Conveyors - 洛谷]

不再需要经过全部的关键点了,有一个很一眼的想法:在走起点和终点之间的路径的时候,到最近的关键点转一圈再回来。答案就是这两个点之间的距离加上两倍的这个最近的关键点之间的距离。

关于每个点到最近的关键点之间的距离。这个很好办,Dijstra 多源最短路就行(好像都不用 Dij,但是好写)。

问题是这个路径上的最近关键点距离。事实上,这东西倍增也可以维护,就像 fa 数组那样维护即可,也没什么难的。

所以这是 hard ver.?

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e5+5;
const int lgr=23;
int n,qi,k;
struct e{
  int v;
  int w;
};
vector<e> edge[N];
int dep[N];
int f[N][26];
int minw[N][26];
bool vis[N];
int dist[N];
int dis[N];
struct work{
  int u;
  ll w;
  bool operator<(const work &x) const{
    return x.w<w;
  }
};
void bfs(int r){
  memset(dep,0x3f,sizeof dep);
  memset(minw,0x3f,sizeof minw);
  dis[r]=0;
  dep[r]=1;dep[0]=0;
  queue<int> q;
  q.push(r);
  f[r][0]=0;f[0][0]=0;
  while(q.size()){
    int u=q.front();
    q.pop();
    for(int i=0;i<edge[u].size();i++){
      int v=edge[u][i].v;
      int w=edge[u][i].w;
      if(dep[v]>dep[u]+1){
        dep[v]=dep[u]+1;
        f[v][0]=u;
        minw[v][0]=dist[u];
        q.push(v);
        dis[v]=dis[u]+w;
        for(int j=1;j<=lgr;j++){
          f[v][j]=f[f[v][j-1]][j-1];
          minw[v][j]=min(minw[v][j-1],minw[f[v][j-1]][j-1]);//像 f 一样维护
        }
      }
    }
  }
}
pair<int,int> lca(int u,int v){
  if(u==v){
    return make_pair(u,min(dist[u],dist[v]));
  }
  if(dep[u]<dep[v]){
    swap(u,v);
  }
  int res=min(dist[u],dist[v]);
  for(int i=lgr;i>=0;i--){
    if(dep[f[u][i]]>=dep[v]){
      res=min(minw[u][i],res);
      u=f[u][i];
    }
  }
  if(u==v){
    return mkp(u,res);
  }
  for(int i=lgr;i>=0;i--){
    if(f[u][i]!=f[v][i]){
      res=min({res,minw[u][i],minw[v][i]});
      u=f[u][i];
      v=f[v][i];
    }
  }
  return mkp(f[u][0],min({res,minw[u][0],minw[v][0]}));
}
int main(){
  
  cin>>n>>qi>>k;
  for(int i=1;i<n;i++){
    int u,v,w;
    cin>>u>>v>>w;
    edge[u].push_back(e{v,w});
    edge[v].push_back(e{u,w});
  }
  vector<int> kp;
  priority_queue<work> q;
  memset(dist,0x3f,sizeof dist);
  for(int i=1;i<=k;i++){
    int u;
    cin>>u;
    kp.push_back(u);
    q.push(work{u,0});
    dist[u]=0;
  }
  while(q.size()){
    int u=q.top().u;
    int w=q.top().w;
    q.pop();
    if(vis[u])continue;
    vis[u]=1;
    dist[u]=w;
    for(int i=0;i<edge[u].size();i++){
      int v=edge[u][i].v;
      int w=edge[u][i].w;
      if(dist[v]>dist[u]+w){
        dist[v]=dist[u]+w;
        q.push(work{v,dist[v]});
      }
    }
  }
  bfs(1);
  for(int i=1;i<=qi;i++){
    int u,v;
    cin>>u>>v;
    pair<int,int> lcaa=lca(u,v);
    cout<<dis[u]+dis[v]-2*dis[lcaa.first]+2*lcaa.second<<'\n';
  }
  
  return 0;
}

树上差分

序列有差分,树上可以差分吗?

当然,首先考虑我们在序列上差分的原理:对于一般的从左向右的差分,我们把在差分数组上的修改,认为是给原数组的后缀修改。这样,对于一个区间修改,我们需要在开始处加,相当于后缀加,在结束位置之后减相同的数以消除多余的后缀对其它值构成的影响。这是序列差分的基本原理。

树上的差分,我们认为是对从节点到根的这一条路径上的修改。对于一条路径上的修改,我们可以先在路径的起点处加,这样会对两点的所有祖先产生双倍的值的影响,但是事实上路径修改时 LCA 也会被影响但只会被影响一次,于是我们在 LCA 上先减,代表保留一次对 LCA 的影响,在 LCA 的父节点处再减代表完全消除剩下的影响,这就是树上差分的基本原理。

现在考虑差分后如何恢复原来的树的值。由于我们定义的差分是从节点到根的这一条路径上的修改,于是我们就要把这个点的的差分值连带着一起加上去一直到根 。然后这就是一个类似于对树链求前缀和,用 DFS 递归回溯的时候就能做到了。

最后,既然有了恢复树的值的方法,把原始树变成差分树的方法也很显然了。DFS 递归到一个点 \(u\) 时,把 \(u\) 的点权与邻接的所有儿子的点权做差,即得 \(u\) 的差分值,之后再去递归儿子即可。(话说这个不应该是第一条吗)

问题时间!

P3258 [JLOI2014] 松鼠 的新家 - 洛谷]

题意大致是你要用题目给的顺序一次经过树上的点,问你每一个点会被经过多少次。

树上差分的模板题啦,但是要注意上一次到达并再从这个点出发的过程算作一次到达,所以这里如果无脑加的话会算重。

那怎么办?我们还得去找父亲加吗?显然这里会牵扯到一大堆的分讨,而且并不好实现。

此时有两种办法:试着把点权化成边权,或者看看多算的那一部分有什么特点。

容易发现这题中每个出现在路径中的点除了初始点之外都只会被多算一次,于是你把无脑算的答案除了顺序里的第一个数之外的都减 \(1\) 就是答案了。

code

Show me the code
const int N=3e5+5;
const int lgr=25;
int o[N];
vector<int> edge[N];
int dep[N];int fa[N][30];int dis[N];int dist[N][30];int pref[N];
void bfs(int r){
  memset(dep,0x3f,sizeof dep);
  dep[r]=1;dep[0]=0;
  fa[r][0]=0;fa[0][0]=0;
  dis[r]=0;
  queue<int> q;
  q.push(r);
  while(q.size()){
    int u=q.front()
    q.pop();
    for(int i=0;i<edge[u].size();i++){
      int v=edge[u][i];
      if(dep[v]>dep[u]+1){
        dep[v]=dep[u]+1;
        fa[v][0]=u;
        q.push(v);
        for(int j=1;j<=lgr;j++)fa[v][j]=fa[fa[v][j-1]][j-1];
      }
    }
  }
}
int lca(int u,int v){
  if(u==v)return u;
  if(dep[u]<dep[v])swap(u,v);
  int cu=u;
  // int res=0;
  for(int i=lgr;i>=0;i--)if(dep[fa[u][i]]>=dep[v])u=fa[u][i];
  if(u==v)return u;
  for(int i=lgr;i>=0;i--){
    if(fa[u][i]!=fa[v][i]){
      u=fa[u][i];
      v=fa[v][i];
    }
  }
  return fa[u][0];
}
void dfs(int u,int fa){
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs(v,u);
    pref[u]+=pref[v];
  }
}
int main(){
  
  int n;
  cin>>n;
  for(int i=1;i<=n;i++){
    cin>>o[i];
  }
  for(int i=1;i<n;i++){
    int u,v;
    cin>>u>>v;
    edge[u].push_back(v);
    edge[v].push_back(u);
  }
  bfs(1);
  for(int i=1;i<n;i++){
    int u=o[i];
    int v=o[i+1];
    int lcaa=lca(u,v);
    pref[lcaa]--;pref[fa[lcaa][0]]--;
    pref[u]++;pref[v]++;
  }
  dfs(1,-1)
  for(int i=2;i<=n;i++){
    cout<<pref[i]-1<<'\n';
  }
  
  
  return 0;
}

P3066 [USACO12DEC] Running Away From the Barn G - 洛谷]

给定一颗 \(n\) 个点的有根树,边有边权,节点从 \(1\)\(n\) 编号,\(1\) 号节点是这棵树的根。

再给出一个参数 \(t\),对于树上的每个节点 \(u\),请求出 \(u\) 的子树中有多少节点满足该节点到 \(u\) 的距离不大于 \(t\)

初见有点像 DP 啊,但是这里的 \(t\) 很大,每条边又有边权,DP 并不可行。

注意到题目中说要求出每个节点 \(u\) 的答案,考虑转化成一个点的贡献。一个点只会贡献所有与它距离不超过 \(t\) 的祖先,距离祖先的距离也是可以在倍增时候维护的,于是可以找到最远的祖先并在这个树链上 \(+1\) 代表贡献,然后就做完了。

code

Show me the code
const int N=2e5+65;
const int lgr=25;
struct e{
  ll v;
  ll w;
};
vector<e> edge[N];
ll dep[N];ll fa[N][30];ll dis[N];ll dist[N][30];ll pref[N];
void bfs(int r){
  memset(dep,0x3f,sizeof dep);
  dep[r]=1;dep[0]=0;
  fa[r][0]=0;fa[0][0]=0;
  dis[r]=0;
  queue<int> q;
  q.push(r);
  while(q.size()){
    int u=q.front();
    q.pop();
    for(int i=0;i<edge[u].size();i++){
      int v=edge[u][i].v;
      ll w=edge[u][i].w;
      if(dep[v]>dep[u]+1){
        dep[v]=dep[u]+1;
        dist[v][0]=w;
        fa[v][0]=u;
        q.push(v);
        for(int j=1;j<=lgr;j++){
          fa[v][j]=fa[fa[v][j-1]][j-1];
          dist[v][j]=dist[v][j-1]+dist[fa[v][j-1]][j-1];
    }}}}
  return ;
}
ll n,d;
int p[N];
int jump(int u){
  ll k=d;
  for(int i=lgr;i>=0;i--){
    if(k-dist[u][i]>=0){
      k-=dist[u][i];
      u=fa[u][i];
      if(u==0)return 0;
    }
  }
  return u;
}
void dfs(int u,int fa){
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i].v;
    if(v==fa)continue;
    dfs(v,u);
    p[u]+=p[v];
  }
}
int main(){
  
  cin>>n>>d;
  for(int i=2;i<=n;i++){
    ll v,w;
    cin>>v>>w;
    edge[i].push_back(e{v,w});
    edge[v].push_back(e{i,w});
  }
  bfs(1);
  for(int i=1;i<=n;i++){
    int t=jump(i);
    p[fa[t][0]]--;p[i]++;
  }
  dfs(1,-1);
  for(int i=1;i<=n;i++)cout<<p[i]<<'\n';

  return 0;
}

树的直径

圆方树?

其实树的直径的定义与圆的直径的定义挺像的。定义一棵树的直径为树上两点之间距离的最大值。

若一棵树的边权都为正,则树的直径有这样一些性质:

  • 树的直径可能有多条,但这些直径必然有重复的一段或一点。
  • 树上任意一点,距离它最远的节点是直径两个端点中的其中一个。

问题来了,怎么算树的直径?

DFS 求解正边权树的直径

当树的边权都非负时,可以使用两次 DFS 优雅的求解出树的直径。

由上面的性质二,我们任取一个节点并对这个节点进行 DFS,找到距离其最远的那个节点。则这个节点一定是这棵树直径的一端。此时再从这个点开始进行 DFS,找到的那个最远的点就是直径的另一端了,整棵树的直径也就找到了。

关于性质二的证明[在这里](树的直径 - OI Wiki)。

树形 DP 求解任意树的直径

定义 \(dp(u)\) 为以 \(u\) 为根的子树中,从 \(u\) 出发的最长路径。

显然地,直径是由从两个点不重复的最长链与这两点之间的边拼成的,于是我们在用 \(u\) 的儿子 \(v\) 更新 \(dp(u)\) 时一边更新一边求出 \(max{dp(u)+dp(v)+w(u,v)}\) 的值。DFS 完成后即得树的直径。

这种方法对于任意的树都可以正确求解。

下面给个 code:

int n, d = 0;
int dp[N];
vector<int> edge[N];

void dfs(int u, int fa) {
  for (int v : E[u]) {
    if (v == fa) continue;
    dfs(v, u);
    d = max(d, dp[u] + dp[v] + 1);// 一边更新一边求
    dp[u] = max(dp[u], dp[v] + 1);
  }
}

int main() {
  scanf("%d", &n);
  for (int i = 1; i < n; i++) {
    int u, v;
    scanf("%d %d", &u, &v);
    edge[u].push_back(v), edge[v].push_back(u);
  }
  dfs(1, 0);
  printf("%d\n", d);
  return 0;
}

问题时间!

[AGC033C - Removing Coins](C - Removing Coins)

高桥和青木将在一棵树上玩一个游戏。这棵树上有 \(N\) 个顶点,编号为 \(1\)\(N\)\(N-1\) 条边中的 \(i\) -th 连接顶点 \(a_i\) 和顶点 \(b_i\)

游戏开始时,每个顶点都有一枚硬币。从高桥开始,他和青木将交替执行以下操作:

  • 选择包含一枚或多枚硬币的顶点 \(v\) ,并移除 \(v\) 上的所有硬币。
  • 然后,将树上剩余的每枚硬币移动到硬币当前顶点的相邻顶点中最靠近 \(v\) 的顶点。

无法下棋的玩家输掉游戏。也就是说,当树上没有硬币时,轮到他的玩家输掉游戏。当双方都以最佳方式下棋时,确定游戏的获胜者。

translated by DeepL , powered by AtCoder Better

考虑选点移硬币的过程其实是减少树的直径的过程,树的直径要么减少一,要么减少二,然后就变成了一个普通的博弈问题啦。

AGC005C - Tree Restoring

青木喜欢数字序列和树。

有一天,高桥给了他一个长度为 \(N\) , \(a_1, a_2, ..., a_N\) 的整数序列,这让他想构建一棵树。

青木想要构建一棵树,树上有 \(N\) 个顶点,编号为 \(1\)\(N\) ,假设每条边的长度为 \(1\) ,则每个 \(i = 1,2,...,N\) 的顶点 \(i\) 与离它最远的顶点之间的距离为 \(a_i\)

请判断是否存在这样一棵树。

translated by DeepL , powered by AtCoder Better

每个顶点都有要求距离最远的顶点的距离,考虑要求最大的两个点。容易发现,这样的点规定了树的直径的长度及两个端点,且这样的点不能只有一个。

接下来考虑树的直径上的点,直径上的点对最远距离的要求是一个类似于谷的等差数列的形式从两边向中间的,直径的长度的奇偶性关系着中间节点的情况,此时要分类讨论。

具体的,在确定直径之后,我们必须要从点集里选出两个公差为一,首项为两端点的值的等差数列组成直径,长度需要分讨,但是对答案没有影响。

树的直径已经被我们确定,接下来需要把剩余的节点挂在树上。显然地,不论此时树的形态如何,只要对于最远距离的要求不小于直径的一半,就一定可以挂在树上,针对这个再判断一下,这个题就做完了。

树的重心

一棵有 \(n\) 个点的树的重心是这样的点 \(g\)

  • 当树以 \(g\) 为根时,\(g\) 的所有子树的大小不超过 \(\left \lfloor \frac{n}{2} \right \rfloor\)
  • \(g\) 是距离树上每个点之间距离最小的那个点。

对于一棵有 \(n\) 个点的树,若 \(n\) 为偶数,则树可能有两个重心,且两个重心必然相邻。若 \(n\) 为奇数,必然只有一个重心。树的重心必然存在。

树的重心有这样的性质:

  • 若在两棵树任意两点间添加一条路径将两树合并,则新树的重心在合并后两重心的简单路径上。
  • 在树中任意插入或者删除一个叶子,重心最多移动一条边。
  • 不断删除树的重心并递归每一棵子树,时间复杂度 \(O(\log n)\)

求解树的重心

树的重心也可以用一次简单的 DFS 求解,若令树上的点权都为 \(1\),以 \(u\) 为根的子树中的点权和为 \(s_u\),总点权和为 \(n\),则树的重心为取到下式最小值的点:

\[\max\left \{ n-s_u,\max_{\forall v\in son\ of\ u } \left \{ s_v \right \} \right \} \]

显然用一遍 DFS 就搞定了。

若树上点的点权是非负的,该算法也是正确的。

问题时间!

CF685B Kay and Snowflake

输入一棵树,判断每一棵子树的重心是哪一个节点.

神秘的一句话题

要用到性质 1 了,考虑自底向上递推,已知一个点所有儿子的重心,则这个点的重心在儿子们重心的连线上。

然后你会发现由于每个重心的深度都大于这个点,你一定会从重心往上跳,这意味着每个点至多会经过一次,这保证了这个算法正确的时间复杂度。

DFN 序

树上路径操作很简单了,但是如果我还想对一个子树操作怎么办?

别忘了,序列上还有个很强的东西叫线段树。我们能否通过给每个节点特殊的编号的情况下,让一个子树的编号连续从而达到子树修改放到区间修改的效果呢?

这种编号叫做 DFN 序,它保证了一个点子树的 DFN 值是连续的。

构造这种序列也很简单。实际上,对树的 DFS 顺序就是 DFN 序。

问题时间!

#144. DFS 序 1

给一棵有根树,这棵树由编号为 \(1\dots N\)\(N\) 个结点组成。根结点的编号为 \(R\)。每个结点都有一个权值,结点 \(i\) 的权值为 \(v_i\)
接下来有 \(M\) 组操作,操作分为两类:

  • 1 a x,表示将结点 \(a\) 的权值增加 \(x\)
  • 2 a,表示求结点 \(a\) 的子树上所有结点的权值之和。

用一遍 DFS 给树上个 DFN 序,根据 DFN 序建线段树维护即可,当然也可以用 fenwick 树(就是树状数组啦,但是如果你常去打 AT 或 CF 就会发现他们是这么叫树状数组的)。

code

Show me the code
int n,m,r;
const int N=1e6+6;
vector<int> edge[N];
ll nodew[N];int cnt=0;int dfn[N];int adfn[N];bool vis[N];
pair<int,int> si[N];
int dfs(int u,int fa){
  cnt++;dfn[u]=cnt;adfn[cnt]=u;
  si[u].first=cnt;
  int li=0;
  bool ii=0;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    if(dfn[v]==0x3f3f3f3f){
      li=max(li,dfs(v,u));
      ii=1;
    }
  }
  if(ii==0){si[u].second=cnt;return cnt;}
  else{si[u].second=li;return li;}
}
struct seg{
  int l;int r;ll v;ll lzt;
};
seg t[N<<4];
void buildt(int p,int l,int r){
  t[p].l=l;t[p].r=r;t[p].lzt=0;
  if(l==r){
    t[p].v=nodew[adfn[l]];
    return ;
  }
  int mid=l+r>>1;
  buildt(p*2,l,mid);
  buildt(p*2+1,mid+1,r);
  t[p].v=t[p*2].v+t[p*2+1].v;
  return ;
}
void pushdown(int u){
  if(t[u].lzt==0)return ;
  ll k=t[u].lzt;
  t[u*2].v+=(t[u*2].r-t[u*2].l+1)*k;t[u*2].lzt+=k;
  t[u*2+1].v+=(t[u*2+1].r-t[u*2+1].l+1)*k;t[u*2+1].lzt+=k;
  t[u].lzt=0;
  return ;
}
void add(int p,int l,int r,ll k){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].v+=(t[p].r-t[p].l+1)*k;
    t[p].lzt+=k;
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  if(l<=mid)add(p*2,l,r,k);
  if(mid<r)add(p*2+1,l,r,k);
  t[p].v=t[p*2].v+t[p*2+1].v;
  return ;
}
ll query(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r)return t[p].v;
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  ll sub=0;
  if(l<=mid)sub+=query(p*2,l,r);
  if(mid<r)sub+=query(p*2+1,l,r);
  return sub;
}
int main(){
  
  cin>>n>>m>>r;
  for(int i=1;i<=n;i++)cin>>nodew[i];
  for(int i=1;i<n;i++){
    int u,v;
    cin>>u>>v;
    edge[u].push_back(v);
    edge[v].push_back(u);
  }
  memset(dfn,0x3f,sizeof dfn);
  dfs(r,-1);
  buildt(1,1,cnt);
  for(int i=1;i<=m;i++){
    int op;cin>>op;
    if(op==1){
      int a,x;cin>>a>>x;
      add(1,dfn[a],dfn[a],x);
    }
    if(op==2){
      int a;cin>>a;
      cout<<query(1,si[a].first,si[a].second)<<'\n';
    }
  }
  
  return 0;
}

#145. DFS 序 2

给一棵有根树,这棵树由编号为 \(1\dots N\)\(N\) 个结点组成。根结点的编号为 \(R\)。每个结点都有一个权值,结点 \(i\) 的权值为 \(v_i\)
接下来有 \(M\) 组操作,操作分为两类:

  • 1 a x,表示将结点 \(a\) 的子树上所有结点的权值增加 \(x\)
  • 2 a,表示求结点 \(a\) 的子树上所有结点的权值之和。

单点加变区间加而已啦。

code

Show me the code
int n,m,r;
const int N=1e6+6;
vector<int> edge[N];
ll nodew[N];int cnt=0;int dfn[N];int adfn[N];bool vis[N];
pair<int,int> si[N];
int dfs(int u,int fa){
  cnt++;
  dfn[u]=cnt;adfn[cnt]=u;
  si[u].first=cnt;
  int li=0;
  bool ii=0;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    if(dfn[v]==0x3f3f3f3f){
      li=max(li,dfs(v,u));
      ii=1;
    }
  }
  if(ii==0){si[u].second=cnt;return cnt;}
  else{si[u].second=li;return li;}
}
struct seg{
  int l;int r;ll v;ll lzt;
};
seg t[N<<4];
void buildt(int p,int l,int r){
  t[p].l=l;t[p].r=r;t[p].lzt=0;
  if(l==r){
    t[p].v=nodew[adfn[l]];
    return ;
  }
  int mid=l+r>>1;
  buildt(p*2,l,mid);
  buildt(p*2+1,mid+1,r);
  t[p].v=t[p*2].v+t[p*2+1].v;
  return ;
}
void pushdown(int u){
  if(t[u].lzt==0)return ;
  ll k=t[u].lzt;
  t[u*2].v+=(t[u*2].r-t[u*2].l+1)*k;t[u*2].lzt+=k;
  t[u*2+1].v+=(t[u*2+1].r-t[u*2+1].l+1)*k;t[u*2+1].lzt+=k;
  t[u].lzt=0;
  return ;
}
void add(int p,int l,int r,ll k){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].v+=(t[p].r-t[p].l+1)*k;
    t[p].lzt+=k;
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  if(l<=mid)add(p*2,l,r,k);
  if(mid<r)add(p*2+1,l,r,k);
  t[p].v=t[p*2].v+t[p*2+1].v;
  return ;
}
ll query(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r)return t[p].v;
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  ll sub=0;
  if(l<=mid)sub+=query(p*2,l,r);
  if(mid<r)sub+=query(p*2+1,l,r);
  return sub;
}
int main(){
  
  cin>>n>>m>>r;
  for(int i=1;i<=n;i++)nodew[i]=rd;
  for(int i=1;i<n;i++){
    int u,v;
    u=rd;v=rd;
    edge[u].push_back(v);
    edge[v].push_back(u);
  }
  memset(dfn,0x3f,sizeof dfn);
  dfs(r,-1);
  buildt(1,1,cnt);
  for(int i=1;i<=m;i++){
    int op;
    op=rd;
    if(op==1){
      int a,x;a=rd;x=rd;
      add(1,si[a].first,si[a].second,x);
    }
    if(op==2){
      int a;a=rd;
      cout<<query(1,si[a].first,si[a].second)<<'\n';
    }
  }
  
  return 0;
}

#146. DFS 序 3,树上差分 1

给一棵有根树,这棵树由编号为 \(1\dots N\)\(N\) 个结点组成。根结点的编号为 \(R\)。每个结点都有一个权值,结点 \(i\) 的权值为 \(v_i\)
接下来有 \(M\) 组操作,操作分为三类:

  • 1 a b x,表示将「结点 \(a\) 到结点 \(b\) 的简单路径」上所有结点的权值都增加 \(x\)
  • 2 a,表示求结点 \(a\) 的权值。
  • 3 a,表示求 \(a\) 的子树上所有结点的权值之和。

这题挺有意思的,我们细细说。

你可能会问这不就是一次树剖的事吗,但是其实它用纯 DFN + 树上差分也可以解决,就像上次我们用树状数组实现区间加区间求和一样。

首先当然是对原树差分,这个方法上文说过,接下来考虑一次路径修改对子树的影响。

一次对 \(u,v\) 节点的修改,令其增加的值为 \(k\),我们还是把它们的最近公共祖先拎出来,由于我们已经对树做了差分,因此我们只能对一些点进行修改。考虑差分的实际影响:对从这个点到树根的链上的每个点有增加某些值,但是由于我们还要查询子树,因此这个影响要变成:增加增加的值,也就是增加值乘以各点的深度。仿照平凡的差分,我们对 \(u,v,lca\)\(lca\) 的父节点都对应的加上、减去 \(k \times dep\)(增加值乘以各点的深度)。

但是这样做完了吗?要知道我们的每次查询可不止查询 \(lca\),若访问到了 \(lca\) 子树内部,这些部分点的贡献可就不是 \(k \times \Delta dep\) 了。

因此,我们需要适时的减去部分 \(k\) 的影响,减多少呢?很显然,应减去查询点的深度与增加值的乘积。由于查询点的深度是不定的,此时我们还要另开一个线段树维护子树内原始增加值的情况。

但是,只在 \(u,v\) 上更改原始增加值是不行的,如果访问到了 \(lca\) 外的点,这些原始增加值依然会发挥作用。那么我们还是用树上差分的思路,在 \(lca\)\(lca\) 的父节点都减去 \(k\)。这样,即使访问到了 \(lca\) 外的点,由于增加值乘以各点的深度已经能够正确的维护和了,原始增加值们就互相抵消掉了。

流程化的,解决此问题需要维护两个线段树,分别维护 增加值乘以各点的深度和,原始增加值之和。分别对于 \(u,v\)\(lca\) 与其父节点进行两棵线段树上的操作。查询子树和时,用 增加值乘以各点的深度和 减去 原始增加值乘以查询子树根节点深度。查询单点和时,简单的计算 原始增加值 的子树和即可。

参考 code:

Show me the code
const int N=1e6+5;
const int lgr=25;
int n,m,r;
ll nodew[N];
vector<int> edge[N];
ll num[N];
int cnt=0;int dfn[N];int adfn[N];bool vis[N];
pair<int,int> si[N];
int dfs(int u,int fa){
  cnt++;
  dfn[u]=cnt;adfn[cnt]=u;si[u].first=cnt;
  int li=0;bool ii=0;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    num[u]-=num[v];
    if(dfn[v]==0x3f3f3f3f){
      li=max(li,dfs(v,u));ii=1;
    }
  }
  if(ii==0){si[u].second=cnt;return cnt;}
  else{si[u].second=li;return li;}
}
int dep[N];
int fa[N][31];
struct seg{int l;int r;ll v;ll wv;ll realv;ll lzt;};
seg t[N<<4];
void buildt(int p,int l,int r){
  t[p].l=l;t[p].r=r;t[p].lzt=0;
  if(l==r){t[p].wv=num[adfn[l]]*(dep[adfn[l]]+1);t[p].v=num[adfn[l]];t[p].realv=num[adfn[l]];return ;}
  int mid=l+r>>1;
  buildt(p*2,l,mid);
  buildt(p*2+1,mid+1,r);
  t[p].v=t[p*2].v+t[p*2+1].v;
  t[p].realv=t[p*2].realv+t[p*2+1].realv;
  t[p].wv=t[p*2].wv+t[p*2+1].wv;
  return ;
}
void pushdown(int u){
  if(t[u].lzt==0)return ;
  ll k=t[u].lzt;
  t[u*2].v+=(t[u*2].r-t[u*2].l+1)*k;
  t[u*2].lzt+=k;
  t[u*2+1].v+=(t[u*2+1].r-t[u*2+1].l+1)*k;
  t[u*2+1].lzt+=k;
  t[u].lzt=0;
  return ;
}
void addv(int p,int l,int r,ll k){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].v+=(t[p].r-t[p].l+1)*k;
    t[p].realv+=(t[p].r-t[p].l+1)*k;
    t[p].lzt+=k;
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  if(l<=mid)addv(p*2,l,r,k);
  if(mid<r)addv(p*2+1,l,r,k);
  t[p].v=t[p*2].v+t[p*2+1].v;
  t[p].realv=t[p*2].realv+t[p*2+1].realv;
  t[p].wv=t[p*2].wv+t[p*2+1].wv;
  return ;
}
void addwv(int p,int pos,ll k){
  if(t[p].l==pos&&t[p].r==pos){
    t[p].wv+=(dep[adfn[pos]]+1)*k;
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  if(pos<=mid)addwv(p*2,pos,k);
  if(mid<pos)addwv(p*2+1,pos,k);
  t[p].wv=t[p*2].wv+t[p*2+1].wv;
  return;
}
ll querywv(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r)return t[p].wv;
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  ll sub=0;
  if(l<=mid)sub+=querywv(p*2,l,r);
  if(mid<r)sub+=querywv(p*2+1,l,r);
  return sub;
}
ll query(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r)return t[p].v;
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  ll sub=0;
  if(l<=mid)sub+=query(p*2,l,r);
  if(mid<r)sub+=query(p*2+1,l,r);
  return sub;
}
void addrealv(int p,int l,int r,ll k){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].realv+=(t[p].r-t[p].l+1)*k;
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  if(l<=mid)addrealv(p*2,l,r,k);
  if(mid<r)addrealv(p*2+1,l,r,k);
  t[p].realv=t[p*2].realv+t[p*2+1].realv;
  return ;
}
ll queryrealv(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r)return t[p].realv;
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  ll sub=0;
  if(l<=mid)sub+=queryrealv(p*2,l,r);
  if(mid<r)sub+=queryrealv(p*2+1,l,r);
  return sub;
}
void bfs(int s){
  memset(dep,0x3f,sizeof dep);
  dep[s]=0;
  queue<int> q;
  q.push(s);
  fa[s][0]=0;
  fa[0][0]=0;
  while(q.size()){
    int u=q.front();
    q.pop();
    for(int i=0;i<edge[u].size();i++){
      int v=edge[u][i];
      if(dep[v]>dep[u]+1){
        dep[v]=dep[u]+1;
        fa[v][0]=u;
        q.push(v);
        for(int k=1;k<=lgr;k++){
          fa[v][k]=fa[fa[u][k-1]][k-1];
        }
      }
    }
  }
  return ;
}
int lca(int u,int v){
  if(u==v)return u;
  if(dep[u]<dep[v])swap(u,v);
  for(int i=lgr;i>=0;i--)if(dep[fa[u][i]]>=dep[v])u=fa[u][i];
  if(u==v)return u;
  for(int i=lgr;i>=0;i--){
    if(dep[fa[u][i]]!=dep[fa[v][i]]){
      u=fa[u][i];
      v=fa[u][i];
    }
  }
  return fa[u][0];
}
int main(){
  
  cin>>n>>m>>r;
  for(int i=1;i<=n;i++){
    nodew[i]=rd;
    num[i]=nodew[i];
  }
  for(int i=1;i<n;i++){
    int u,v;
    u=rd;v=rd;
    edge[u].push_back(v);edge[v].push_back(u);
  }
  dfs(r,-1);
  bfs(r);
  buildt(1,1,cnt);
  for(int i=1;i<=m;i++){
    int op;
    cin>>op;
    if(op==1){
      int a,b;
      ll x;
      cin>>a>>b>>x;
      int lcaa=lca(a,b);
      addwv(1,dfn[a],x);
      addwv(1,dfn[b],x);
      addv(1,dfn[a],dfn[a],x);
      addv(1,dfn[b],dfn[b],x);
      addrealv(1,dfn[lcaa],dfn[lcaa],-x);
      addrealv(1,dfn[fa[lcaa][0]],dfn[fa[lcaa][0]],-x);
    }
    if(op==2){
      int a;cin>>a;
      cout<<queryrealv(1,si[a].first,si[a].second)<<'\n';
    }
    if(op==3){
      int a;cin>>a;
      cout<<querywv(1,si[a].first,si[a].second)-query(1,si[a].first,si[a].second)*dep[a]<<'\n';
    }
  }
  
  return 0;
}

#147. DFS 序 4

给一棵有根树,这棵树由编号为 \(1\dots N\)\(N\) 个结点组成。根结点的编号为 \(R\)。每个结点都有一个权值,结点 \(i\) 的权值为 \(v_i\)
接下来有 \(M\) 组操作,操作分为三类:

  • 1 a x,表示将结点 \(a\) 的权值增加 \(x\)
  • 2 a x,表示将 \(a\) 的子树上所有结点的权值增加 \(x\)
  • 3 a b,表示求「结点 \(a\) 到结点 \(b\) 的简单路径」上所有结点的权值之和。

因为是访问路径和,肯定要用到最近公共祖先的维护到根的距离的技巧,接下来考虑如何在带修改的情况下维护到根的距离。

对于一次对子树的修改,若子树的根节点为 \(u\),则子树内每一个节点到根增加的距离为 \(k\times \Delta dep\)\(\Delta dep\) 即为子树根节点到目标节点的距离。考虑这个东西能不能用线段树快速维护,发现是可以的,然后就做完啦。

比上一个题好像多了。但没有 code,原因是被卡常卡到自闭了。


当然了,对于更加复杂的树上问题,例如树链修改,树链查询问题,再用几个维护不同类别的线段树维护是不现实的。有没有一种方式,可以把无所不能的线段树真的应用在树上的链上呢?

有的朋友,请看下一篇:树上那些事——树上问题 II (重链剖分,长链剖分)

last upd on 2025 03 28

posted @ 2025-03-23 22:15  hm2ns  阅读(276)  评论(0)    收藏  举报