动态树算法概述及习题

动态树概述

一、适用问题

动态树主要用于解决操作中带有加边、删边、换根的一系列问题,即树结构发生变化的问题,理论上来说,树链剖分的问题都能用 L C T LCT 进行解决。

二、函数解析

L C T LCT 本质是上对树进行实链剖分,实链剖分的意思就是将一棵树分成多条链,链中的边称为实边,链与链之间的边则称为虚边,每条链都是一个 s p l a y splay ,在 s p l a y splay 中进行中序遍历即可还原原来的树结构。而 L C T LCT 就是不断进行虚边、实边转换的一个算法。

L C T LCT 中一共有 c l e a r clear p u s h U p pushUp p u s h D o w n pushDown u p d a t e update r o t a t e rotate s p l a y splay a c c e s s access m a k e R o o t makeRoot l i n k link c u t cut f i n d find s p l i t split 等函数,下面大致介绍一下每个函数的具体作用以及一些坑点,更多的是提纲挈领的作用,想要从最基础的地方开始学的话,推荐 oiwiki

简单函数(仅操作单个 s p l a y splay 的函数)

  1. c l e a r ( x ) clear(x) :清除一个点的信息,如父亲、左右儿子、标记、维护信息等信息。
  2. p u s h U p ( x ) pushUp(x) :由左右儿子的信息更新父节点的信息,与线段树的 p u s h U p ( ) pushUp() 函数没有太大差别。
  3. p u s h D o w n ( x ) pushDown(x) :将当前节点的标记下放到儿子节点,如加、减、翻转等标记。
  4. u p d a t e ( x ) update(x) :一直递归到根节点,然后把标记信息不断下放,没有涉及任何虚实边的转换。
 void update(int p){ //递归地从上到下pushDown信息
   if(!isRoot(p)) update(f[p]);
   pushDown(p);
 }
  1. r o t a t e ( x ) rotate(x) :将当前节点向上旋转一层,可以自己模拟一下。此处改变了 s p l a y splay 的内部结构,即子节点发生了改变,因此需要进行 p u s h U p pushUp ,但是仍然没有进行任何虚实边的转换。
 inline void rotate(int x){ //将x向上旋转一层的操作
   int y = f[x], z = f[y], k = Get(x);
   if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
   ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
   ch[x][!k] = y, f[y] = x, f[x] = z;
   pushUp(y); //要先pushUp(y)
   pushUp(x);
 }
  1. s p l a y ( x ) splay(x) :将当前点旋转到 s p l a y splay 的根节点, s p l a y splay 到根节点作用在于不需要在向上进行更新。比如你现在要修改 x x 的点权,但是每个节点还要维护子树 s u m sum 的信息,如果 x x 不是其所在 s p l a y splay 的根节点,那么修改 x x 的点权势必影响到其祖先节点的 s u m sum 信息,因此需要将 x x 旋转为其所在 s p l a y splay 的根后再进行单点修改。注意 s p l a y splay 函数也没有进行实边和虚边的转换。
 inline void splay(int x){ //把x旋转到当前splay的根
   update(x); //将上面的标记完全下放
   for(int fa; fa = f[x], !isRoot(x); rotate(x)){
     if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
   }
 }

以上函数都属于 L C T LCT 函数中的简单函数,因为这些函数都只是在单个 s p l a y splay 中进行操作,不涉及任何虚实边的转换。

复杂函数(涉及多个 s p l a y splay 的操作,进行虚实边转换)

  1. a c c e s s ( x ) access(x) :将点 x x 到根的路径经过的点放入同一个 s p l a y splay 中,且这个 s p l a y splay 中仅包含从 x x 到根路径上经过的点。具体操作即是将 x x 点不断转成其所在 s p l a y splay 的根,然后再进行虚实边转换一直到根。此处 a c c e s s access 函数有返回值,返回值为最后构成的 s p l a y splay 的根节点。
inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
    int p; //每次改变右儿子的值,因为整棵树是中序遍历,放入右儿子才能保证先遍历父亲再遍历儿子
    for(p = 0; x; p = x, x = f[x]){
      splay(x), ch[x][1] = p, pushUp(x);
    }
    return p;
}
  1. m a k e R o o t ( x ) makeRoot(x) :换根操作,将点 x x 变成当前树的根。具体过程为先 a c c e s s access x x ,然后再将点 x x 旋转为其 s p l a y splay 所在根,然后将所有节点的左右儿子翻转即可。
inline void makeRoot(int p){ //使x点成为整棵树的根
    access(p); splay(p);
    swap(ch[p][0],ch[p][1]); //把整条链反向
    rev[p] ^= 1;
}
  1. s p l i t ( x , y ) split(x,y) :从树中拎出 x y x\rightarrow y 的路径,返回该路径的 s p l a y splay 根节点,可以查询路径最大值、点权和、边权和等信息。
inline int split(int x,int y){
	makeRoot(x); 
	return access(y);
} 
  1. f i n d ( x ) find(x) :即返回点 x x 所在树的根节点,不是所在 s p l a y splay 中的根节点,用于判断两点是否连通。
 inline int find(int p){ //找到x所在树的根节点编号
   access(p), splay(p);
   while(ls) pushDown(p), p = ls;
   return p;
 }
  1. l i n k ( x , y ) link(x,y) :连接树中 x x y y 两点之间的边,无边变虚边,如果题目中没有保证操作一定合法,则需要自行判断 x x y y 是否已经连通。
inline void link(int x,int y){ //在x、y两点间连一条边,连接了虚边
   if (find(x) != find(y)) makeRoot(x), f[x] = y;
 }
  1. c u t ( x , y ) cut(x,y) :断开树中 x x y y 两点之间的实边,两个点同时断开即可。
inline void cut(int x,int p){ //把x、y两点间边删掉,此处删除的是实边,注意实边和虚边的区别
   makeRoot(x), access(p), splay(p);
   if (ls == x && !rs) ls = f[x] = 0;
 }

三、具体细节

单点修改

由于 L C T LCT 中维护了多个 s p l a y splay ,因此单点修改需要把该点修改的信息不断上传,所以我们需要先将点 x x 旋转到 s p l a y splay 的根或者整棵树的根,然后再进行单点修改。

如果题中只需要维护实链信息,则只需要旋转到 s p l a y splay 的根,如果需要同时维护实链和虚链信息,即整棵子树的信息的话,则需要令该点为树根,即调用 m a k e R o o t ( ) makeRoot() 函数。

维护边权

由于 L C T LCT 是不断地进行虚边、实边转换,因此没有固定的边结构,所以直接维护边权十分困难,因此我们将边转成点,边 ( a , b ) (a,b) 成为一个点 x x l i n k ( a , x ) link(a,x) l i n k ( x , b ) link(x,b) 即可。

维护子树信息

普通 L C T LCT 只能维护具有可减性的子树信息,比如子树大小,子树贡献等,而子树 m a x max m i n min 等问题则不具有可减性,难以维护。

维护子树信息主要在于维护实边信息和虚边信息,而进行实虚转换的函数只有 m a k e R o o t ( ) makeRoot() a c c e s s ( ) access() l i n k ( ) link() 三个函数,只需要在该三个函数进行一定的修改即可,下面习题中包含了该问题可供参考。


动态树习题

1. [国家集训队] Tree II(模板题)

题意: n n 个点一棵树,支持四种操作。 ( 1 n , q 1 0 5 , 0 c 1 0 4 ) (1\leq n,q\leq 10^5,0\leq c\leq 10^4)

  • +   u   v   c +\ u \ v \ c ,将 u u v v 的路径上的点的权值都加上 c c
  •   u 1   v 1   u 2   v 2 -\ u_1 \ v_1 \ u_2\ v_2 ,将树中原有的边 ( u 1 , v 1 ) (u_1,v_1) 删除,加入一条新边 ( u 2 , v 2 ) (u_2,v_2) ,保证操作完之后仍然是一颗树。
  • \   u   v   c *\ u \ v \ c ,将 u u v v 的路径上的点的权值都乘上 c c
  • / u   v u \ v ,询问 u u v v 的路径上的点的权值和,答案 m o d   51061 mod\ 51061

思路: 三个涉及到路径的操作,都是先把 u u 变成树根,然后 a c c e s s ( v ) access(v) ,即拉起一条 u u v v 的路径,使得 u u v v 路径上的点都在一个 s p l a y splay 中,然后获得这个 s p l a y splay 的根节点,即可对根节点打标记完成。

删边则是令 u u 为根,拉起 u u v v 的路径,将 v v 旋转成 s p l a y splay 的根,然后儿子与父亲双向断开联系。加边则是令 u u 为根,然后使 u u 的父亲变成 v v

总结: 这题应该算是 L C T LCT 的模板题,涉及的操作都是基础操作,没有太多思维上的难点。

代码:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
#define int long long
const int N = 100010;
const int mod = 51061;
int n, q, u, v, c;
char op;

struct LCT{
  #define ls ch[p][0]
  #define rs ch[p][1]
  #define Get(p) (ch[f[p]][1] == p)
  int ch[N][2], f[N], sum[N], val[N], siz[N], rev[N], add[N], mul[N];

  inline void clear(int p){ //清除这个点的信息
    ch[p][0] = ch[p][1] = f[p] = siz[p] = val[p] = sum[p] = rev[p] = add[p] = 0;
    mul[p] = 1;
  }

  inline int isRoot(int p){
    clear(0);
    return ch[f[p]][0] != p && ch[f[p]][1] != p;
  }

  inline void pushUp(int p){
    clear(0);
    siz[p] = (siz[ls] + 1 + siz[rs]) % mod;
    sum[p] = (sum[ls] + val[p] + sum[rs]) % mod;
  }

  inline void pushDown(int p){
    clear(0);
    if(mul[p] != 1){ //乘法
      if(ls){ //左儿子
        mul[ls] = (mul[ls] * mul[p]) % mod;
        val[ls] = (val[ls] * mul[p]) % mod;
        sum[ls] = (sum[ls] * mul[p]) % mod;
        add[ls] = (add[ls] * mul[p]) % mod;
      }
      if(rs){ //右儿子
        mul[rs] = (mul[rs] * mul[p]) % mod;
        val[rs] = (val[rs] * mul[p]) % mod;
        sum[rs] = (sum[rs] * mul[p]) % mod;
        add[rs] = (add[rs] * mul[p]) % mod;
      }
      mul[p] = 1;
    }
    if(add[p]){
      if(ls){
        add[ls] = (add[ls] + add[p]) % mod;
        val[ls] = (val[ls] + add[p]) % mod;
        sum[ls] = (sum[ls] + add[p] * siz[ls] % mod) % mod;
      }
      if(rs){
        add[rs] = (add[rs] + add[p]) % mod;
        val[rs] = (val[rs] + add[p]) % mod;
        sum[rs] = (sum[rs] + add[p] * siz[rs] % mod) % mod;
      }
      add[p] = 0;
    }
    if(rev[p]){ 
      if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
      if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
      rev[p] = 0;
    }
  }

  void update(int p){ //递归地从上到下pushDown信息
    //没有将实边变成虚边
    if(!isRoot(p)) update(f[p]);
    pushDown(p);
  }

  inline void rotate(int x){ //将x向上旋转一层的操作
    //没有将实边变成虚边
    int y = f[x], z = f[y], k = Get(x);
    if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
    ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
    ch[x][!k] = y, f[y] = x, f[x] = z;
    pushUp(y); //要先pushUp(y)
    pushUp(x);
  }

  inline void splay(int x){ //把x旋转到当前splay的根
    //没有将实边变成虚边
    update(x); //将上面的标记完全下放
    for(int fa; fa = f[x], !isRoot(x); rotate(x)){
      if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
    }
  }

  inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
    //进行了边的虚实变换
    int p; //每次改变右儿子的值,因为整棵树是中序遍历,放入右儿子才能保证先遍历父亲再遍历儿子
    for(p = 0; x; p = x, x = f[x]){
      splay(x), ch[x][1] = p, pushUp(x);
    }
    return p;
  }

  inline void makeRoot(int p){ //使x点成为整棵树的根
    access(p); splay(p);
    swap(ch[p][0],ch[p][1]); //把整条链反向
    rev[p] ^= 1;
  }

  inline void link(int x,int y){ //在x、y两点间连一条边,连接了虚边
    if (find(x) != find(y)) makeRoot(x), f[x] = y;
  }

  inline void cut(int x,int p){ //把x、y两点间边删掉,此处删除的是实边,注意实边和虚边的区别
    makeRoot(x), access(p), splay(p);
    if (ls == x && !rs) ls = f[x] = 0;
  }

  inline int find(int p){ //找到x所在树的根节点编号
    access(p), splay(p);
    while(ls) pushDown(p), p = ls;
    return p;
  }
  //中序遍历即可还原树结构
  void print(int p){
    if(!p) return;
    pushDown(p);
    print(ls);
    printf("%lld ",p);
    print(rs);
  }
}st;

signed main() {
  scanf("%lld%lld", &n, &q);
  for (int i = 1; i <= n; i++) st.val[i] = 1;
  for (int i = 1; i < n; i++) {
    scanf("%lld%lld", &u, &v);
    st.link(u,v);
  }
  while (q--) {
    scanf(" %c%lld%lld", &op, &u, &v);
    if (op == '+') { //+ u v c, u到v的路径上的点权值+c
      scanf("%lld", &c);
      //u变成树根,拉起v到u的链,把v旋到splay的根
      st.makeRoot(u); v = st.access(v);
      st.val[v] = (st.val[v] + c) % mod;
      st.sum[v] = (st.sum[v] + st.siz[v] * c % mod) % mod;
      st.add[v] = (st.add[v] + c) % mod;
    }
    if (op == '-') { //- u1 v1 u2 v2, 删除(u1,v1), 加上(u2,v2)
      st.cut(u,v);
      scanf("%lld%lld", &u, &v);
      st.link(u,v);
    }
    if (op == '*') { //* u v c, u到v的路径乘上c
      scanf("%lld", &c);
      st.makeRoot(u); v = st.access(v);
      st.val[v] = st.val[v] * c % mod;
      st.sum[v] = st.sum[v] * c % mod;
      st.mul[v] = st.mul[v] * c % mod;
    }
    if (op == '/'){ //u v, 询问u到v路径权值和
      st.makeRoot(u); v = st.access(v);
      printf("%lld\n", st.sum[v]);
    }
  }
  return 0;
}
2. Query on a tree

题意: n n 个点一棵树,支持两种操作。 ( 1 n , q 1 0 4 ) (1\leq n,q\leq 10^4)

  • CHANGE \text{CHANGE} i i t i t_i ,将第 i i 条边的边权改为 t i t_i
  • QUERY \text{QUERY} a a b b ,查询树中点 a a 到点 b b 的路径中的边权最大值。

思路: 边权 L C T LCT ,需要对于每一条边建一个节点,即树中一共有 2 n 1 2*n-1 个节点,每个边节点与上下两个节点连边。

建边需要先确定每个节点的边权之后再进行 l i n k link ,因为点修改会对该节点的祖先节点产生影响,需要将该点旋至 s p l a y splay 端点后才能进行修改。

总结: 总结一下构建 L C T LCT 构建的关键,构建 L C T LCT 需要先对各个顶点赋值然后再进行 l i n k link 操作,若是 l i n k link 之后再赋值相当于点修改,而点修改需要将点旋为 s p l a y splay 根之后才能进行更改。

代码:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
const int N = 20000+10;
int n,val[N];

struct LCT{
  #define ls ch[p][0]
  #define rs ch[p][1]
  #define Get(p) (ch[f[p]][1] == p)
  int ch[N][2], f[N], maxn[N], val[N], siz[N], rev[N];

  inline void clear(int p){ //清除这个点的信息
    ch[p][0] = ch[p][1] = f[p] = siz[p] = val[p] = maxn[p] = 0;
  }

  inline int isRoot(int p){
    clear(0);
    return ch[f[p]][0] != p && ch[f[p]][1] != p;
  }

  inline void pushUp(int p){
    clear(0);
    siz[p] = siz[ls] + 1 + siz[rs];
    maxn[p] = max(val[p],max(maxn[ls],maxn[rs]));
  }

  inline void pushDown(int p){
    clear(0);
    if(rev[p]){ 
      if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
      if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
      rev[p] = 0;
    }
  }

  void update(int p){ //递归地从上到下pushDown信息
    if(!isRoot(p)) update(f[p]);
    pushDown(p);
  }

  inline void rotate(int x){ //将x向上旋转一层的操作
    int y = f[x], z = f[y], k = Get(x);
    if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
    ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
    ch[x][!k] = y, f[y] = x, f[x] = z;
    pushUp(y); //要先pushUp(y)
    pushUp(x);
  }

  inline void splay(int x){ //把x旋转到当前splay的根
    update(x); //将上面的标记完全下放
    for(int fa; fa = f[x], !isRoot(x); rotate(x)){
      if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
    }
  }

  inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
    int p;
    for(p = 0; x; p = x, x = f[x]){
      splay(x), ch[x][1] = p, pushUp(x);
    }
    return p;
  }

  inline void makeRoot(int p){ //使x点成为整棵树的根
    access(p); splay(p);
    swap(ch[p][0],ch[p][1]); //把整条链反向
    rev[p] ^= 1;
  }

  inline void link(int x,int y){ //在x、y两点间连一条边
    makeRoot(x), f[x] = y; //dfs建树, 每条边都是有效的, 因此不需要判断是否有效
  }
}st;

int main(){
  int _; scanf("%d",&_);
  while(_--){
    scanf("%d",&n);
    rep(i,0,2*n) st.clear(i);
    rep(i,1,n-1){
      int a,b,c; scanf("%d%d%d",&a,&b,&c);
      st.val[i+n] = c;
      st.link(a,i+n);
      st.link(i+n,b);
    }
    while(1){
      char s[20]; scanf("%s",s);
      if(s[0] == 'D') break;
      else if(s[0] == 'C'){
        int a,b; scanf("%d%d",&a,&b);
        st.splay(a+n); //先转为splay根节点
        st.val[a+n] = b;
      }
      else{
        int a,b; scanf("%d%d",&a,&b);
        st.makeRoot(a);
        b = st.access(b);
        printf("%d\n",st.maxn[b]);
      }
    }
  }
  return 0;
}
3. Can you answer these queries VII

题意: n n 个点一棵树,每个节点有一个值,支持两种操作。 ( 1 n , q 1 0 5 ) (1\leq n,q\leq 10^5)

  • 1 1 a a b b ,查询树中点 a a 到点 b b 的路径中最大连续和
  • 2 2 a a b b c c ,将树中点 a a 到点 b b 路径中所有点的值改为 c c

思路: 对每一个点维护一个 l c [ i ] lc[i] r c [ i ] rc[i] m a x n [ i ] maxn[i] 表示点 i i 子树中左连续的最大值、右连续的最大值以及整棵子树中的最大连续值。

需要注意一点,交换左右儿子的时候,还需要把每个节点的 l c lc r c rc 进行交换,其余细节见代码。

代码:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const ll inf = 1e9+100;
const int N = 1e5+10;
int n,Q;

struct LCT{
  #define ls ch[p][0]
  #define rs ch[p][1]
  #define Get(p) (ch[f[p]][1] == p)
  int ch[N][2], f[N];
  ll maxn[N], sum[N], lc[N], rc[N], val[N], siz[N], lazy[N];
  bool rev[N];

  inline void clear(int p){ //清除这个点的信息
    ch[p][0] = ch[p][1] = f[p] = val[p] = maxn[p] = lc[p] = sum[p] = rc[p] = siz[p] = 0;
  }

  inline int isRoot(int p){
    return ch[f[p]][0] != p && ch[f[p]][1] != p;
  }

  inline void pushUp(int p){
    siz[p] = siz[ls] + 1 + siz[rs];
    sum[p] = val[p] + sum[ls] + sum[rs]; //ls、rs可能为0
    maxn[p] = max(maxn[ls],max(maxn[rs],rc[ls]+lc[rs]+val[p]));
    lc[p] = max(lc[ls],sum[ls]+val[p]+lc[rs]);
    rc[p] = max(rc[rs],sum[rs]+val[p]+rc[ls]);
  }

  inline void pushDown(int p){
    if(rev[p]){ 
      if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
      if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
      swap(lc[ls],rc[ls]); swap(lc[rs],rc[rs]); //交换左右儿子时还要交换左右连续最大值
      rev[p] = 0;
    }
    if(lazy[p] != -inf){
      if(ls){
        sum[ls] = siz[ls]*lazy[p]; 
        val[ls] = lazy[ls] = lazy[p];
        lc[ls] = rc[ls] = maxn[ls] = lazy[p] > 0 ? sum[ls]:0;
      }
      if(rs){
        sum[rs] = siz[rs]*lazy[p]; 
        val[rs] = lazy[rs] = lazy[p];
        lc[rs] = rc[rs] = maxn[rs] = lazy[p] > 0 ? sum[rs]:0;
      }
      lazy[p] = -inf; 
    }
  }

  void update(int p){ //递归地从上到下pushDown信息
    if(!isRoot(p)) update(f[p]);
    pushDown(p);
  }

  inline void rotate(int x){ //将x向上旋转一层的操作
    int y = f[x], z = f[y], k = Get(x);
    if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
    ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
    ch[x][!k] = y, f[y] = x, f[x] = z;
    pushUp(y); //要先pushUp(y)
    pushUp(x);
  }

  inline void splay(int x){ //把x旋转到当前splay的根
    update(x); //将上面的标记完全下放
    for(int fa; fa = f[x], !isRoot(x); rotate(x)){
      if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
    }
  }

  inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
    int p;
    for(p = 0; x; p = x, x = f[x]){
      splay(x), ch[x][1] = p, pushUp(x);
    }
    return p;
  }

  inline void makeRoot(int p){ //使x点成为整棵树的根
    access(p);
    splay(p);
    swap(ch[p][0],ch[p][1]); //把整条链反向
    rev[p] ^= 1;
  }

  inline void link(int x,int y){ //在x、y两点间连一条边
    makeRoot(x), f[x] = y; //dfs建树, 每条边都是有效的, 因此不需要判断是否有效
  }
}st;

int main(){
  scanf("%d",&n);
  rep(i,1,n){
    scanf("%lld",&st.val[i]);
    st.siz[i] = 1; st.sum[i] = st.val[i];
    st.lazy[i] = -inf;
    st.lc[i] = st.rc[i] = st.maxn[i] = st.val[i] > 0 ? st.val[i]:0;
  }
  rep(i,1,n-1){
    int a,b; scanf("%d%d",&a,&b);
    st.link(a,b);
  }
  scanf("%d",&Q);
  while(Q--){ 
    int op; scanf("%d",&op);
    if(op == 1){ //a->b max
      int a,b; scanf("%d%d",&a,&b);
      st.makeRoot(a);
      b = st.access(b);
      printf("%lld\n",st.maxn[b]);
    }
    else{ //a->b to c
      int a,b; ll c; scanf("%d%d%lld",&a,&b,&c);
      st.makeRoot(a);
      b = st.access(b);
      st.val[b] = st.lazy[b] = c;
      st.sum[b] = st.siz[b]*c;
      st.lc[b] = st.rc[b] = st.maxn[b] = c > 0 ? st.sum[b]:0;
    }
  }
  return 0;
}
4. [ZJOI2008] 树的统计 Count

题意: 一棵树上有 n n 个节点,每个节点都有一个权值 w w 。支持三种操作: C H A N G E CHANGE u u t t :把结点 u u 的权值改为 t t Q M A X QMAX u u v v :询问从点 u u 到点 v v 的路径上的节点的最大权值; Q S U M QSUM u u v v :询问从点 u u 到点 v v 的路径上的节点的权值和。 ( 1 n 3 1 0 4 , 1 q 2 1 0 5 ) (1\leq n\leq 3*10^4,1\leq q\leq 2*10^5)

思路: 单点查询 + 路径最大值 + 路径 s u m sum 和,非常裸的题目,纯当练习。

代码:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const int inf = 1e9+100;
const int N = 40000+10;
int n,Q,A[N],B[N];

struct LCT{
  #define ls ch[p][0]
  #define rs ch[p][1]
  #define Get(p) (ch[f[p]][1] == p)
  int ch[N][2], f[N];
  int maxn[N], sum[N], val[N];
  bool rev[N];

  inline void clear(int p){ //清除这个点的信息
    ch[p][0] = ch[p][1] = f[p] = val[p] = maxn[p] = 0;
  }

  inline int isRoot(int p){
    return ch[f[p]][0] != p && ch[f[p]][1] != p;
  }

  inline void pushUp(int p){
    sum[p] = val[p] + sum[ls] + sum[rs]; //ls、rs可能为0
    maxn[p] = max(val[p],max(maxn[ls],maxn[rs]));
  }

  inline void pushDown(int p){
    if(rev[p]){ 
      if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
      if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
      rev[p] = 0;
    }
  }

  void update(int p){ //递归地从上到下pushDown信息
    if(!isRoot(p)) update(f[p]);
    pushDown(p);
  }

  inline void rotate(int x){ //将x向上旋转一层的操作
    int y = f[x], z = f[y], k = Get(x);
    if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
    ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
    ch[x][!k] = y, f[y] = x, f[x] = z;
    pushUp(y); //要先pushUp(y)
    pushUp(x);
  }

  inline void splay(int x){ //把x旋转到当前splay的根
    update(x); //将上面的标记完全下放
    for(int fa; fa = f[x], !isRoot(x); rotate(x)){
      if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
    }
  }

  inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
    int p;
    for(p = 0; x; p = x, x = f[x]){
      splay(x), ch[x][1] = p, pushUp(x);
    }
    return p;
  }

  inline void makeRoot(int p){ //使x点成为整棵树的根
    access(p);
    splay(p);
    swap(ch[p][0],ch[p][1]); //把整条链反向
    rev[p] ^= 1;
  }

  inline void link(int x,int y){ //在x、y两点间连一条边
    makeRoot(x), f[x] = y; //dfs建树, 每条边都是有效的, 因此不需要判断是否有效
  }
}st;

int main(){
  scanf("%d",&n);
  st.maxn[0] = -inf;
  rep(i,1,n-1) scanf("%d%d",&A[i],&B[i]);
  rep(i,1,n){
    int hp; scanf("%d",&hp);
    st.val[i] = st.maxn[i] = st.sum[i] = hp;
  }
  rep(i,1,n-1) st.link(A[i],B[i]);
  scanf("%d",&Q);
  while(Q--){ 
    char s[20]; int u,v;
    scanf("%s%d%d",s,&u,&v);
    if(s[0] == 'C'){
      st.splay(u);
      st.val[u] = v;
      st.pushUp(u);
    }
    else if(s[1] == 'M'){
      st.makeRoot(u);
      v = st.access(v);
      printf("%d\n",st.maxn[v]);
    }
    else{
      st.makeRoot(u);
      v = st.access(v);
      printf("%d\n",st.sum[v]);
    }
  }
  return 0;
}
5. 最小差值生成树

题意: n n 个点, m m 条边的一个无向图,求边权最大值与最小值的差值最小的生成树。 ( 1 n 5 1 0 4 , 1 m 2 1 0 5 ) (1\leq n\leq 5*10^4,1\leq m\leq 2*10^5)

思路: 关于这类特殊生成树问题,一般考虑用 L C T LCT 动态维护树结构然后更新答案。

此题也可以这样考虑。将边按边权从小到大排序,如果 ( a , b ) (a,b) 两点不连通,则加上该边,如果 ( a , b ) (a,b) 两点连通,则将 a b a\rightarrow b 路径上边权最小的边去除,然后连上当前的边。维护过程不断更新最大值与最小值的差值,不断取 m i n min 即可。

因此只需要维护一个边权 L C T LCT ,并且维护路径最小值以及最小值点的编号,然后动态加边删边即可。还需要对在树中的边打上标记,去除的时候删去标记,用于查找整棵树中的最小边权。

代码:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
const int N = 2e5+5e4+100;
const int M = 2e5+10;
int n, m, vis[N], pos = 1, num, ans = 1e9;
struct Edge{
  int a,b,w;
  bool operator < (Edge xx) const {
    return w < xx.w;
  }
}e[N];

void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}

struct LCT{
  #define ls ch[p][0]
  #define rs ch[p][1]
  #define Get(p) (ch[f[p]][1] == p)
  int ch[N][2], f[N], val[N], minn[N], mpos[N], rev[N];

  inline void clear(int p){ //清除这个点的信息
    ch[p][0] = ch[p][1] = f[p] = val[p] = mpos[p] = minn[p] = 0;
  }

  inline int isRoot(int p){
    return ch[f[p]][0] != p && ch[f[p]][1] != p;
  }

  inline void pushUp(int p){
    minn[p] = val[p]; mpos[p] = p;
    if(ls && minn[ls] < minn[p]) minn[p] = minn[ls], mpos[p] = mpos[ls];
    if(rs && minn[rs] < minn[p]) minn[p] = minn[rs], mpos[p] = mpos[rs];
  }

  inline void pushDown(int p){
    if(rev[p]){ 
      if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
      if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
      rev[p] = 0;
    }
  }

  void update(int p){ //递归地从上到下pushDown信息
    if(!isRoot(p)) update(f[p]);
    pushDown(p);
  }

  inline void rotate(int x){ //将x向上旋转一层的操作
    int y = f[x], z = f[y], k = Get(x);
    if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
    ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
    ch[x][!k] = y, f[y] = x, f[x] = z;
    pushUp(y); //要先pushUp(y)
    pushUp(x);
  }

  inline void splay(int x){ //把x旋转到当前splay的根
    update(x); //将上面的标记完全下放
    for(int fa; fa = f[x], !isRoot(x); rotate(x)){
      if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
    }
  }

  inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
    int p = 0;
    for(p = 0; x; p = x, x = f[x]){
      splay(x), ch[x][1] = p, pushUp(x);
    }
    return p;
  }

  inline void makeRoot(int p){ //使x点成为整棵树的根
    access(p); splay(p);
    swap(ch[p][0],ch[p][1]); //把整条链反向
    rev[p] ^= 1;
  }

  inline void link(int x,int y){ //在x、y两点间连一条边
    // if (find(x) != find(y)) 
    makeRoot(x), f[x] = y;
  }

  inline void cut(int x,int p){ //把x、y两点间边删掉
    makeRoot(x), access(p), splay(p);
    if (ls == x && !rs) ls = f[x] = 0;
  }

  inline int find(int p){ //找到x所在树的根节点编号
    access(p), splay(p);
    while(ls) pushDown(p), p = ls;
    return p;
  }
}st;

signed main() {
  scanf("%d%d", &n, &m);
  rep(i,0,n) st.val[i] = st.minn[i] = 1e5;
  rep(i,1,m) scanf("%d%d%d",&e[i].a,&e[i].b,&e[i].w);
  sort(e+1,e+1+m);
  rep(i,1,m) st.val[i+n] = st.minn[i+n] = e[i].w, st.mpos[i+n] = i+n;
  rep(i,1,m){
    if(e[i].a == e[i].b) continue;
    if(st.find(e[i].a) != st.find(e[i].b)){
      st.link(e[i].a,i+n); st.link(i+n,e[i].b);
      num++; vis[i] = 1;
      while(!vis[pos]) pos++;
      if(num == n-1) ans = min(ans,e[i].w-e[pos].w);
    }
    else{
      st.makeRoot(e[i].a);
      int p1 = st.access(e[i].b);
      p1 = st.mpos[p1];
      st.cut(e[p1-n].a,p1); st.cut(p1,e[p1-n].b); vis[p1-n] = 0;
      st.link(e[i].a,i+n); st.link(i+n,e[i].b); vis[i] = 1;
      while(!vis[pos]) pos++;
      if(num == n-1) ans = min(ans,e[i].w-e[pos].w);
    }
  }
  printf("%d\n",ans);
  return 0;
}
6. [BJOI2014] 大融合

题意: n n 个点,一共 q q 次操作。一共有两种操作类型, A   x   y A \ x \ y 表示连通 ( x , y ) (x,y) ,保证操作合法,且始终是棵森林。 Q   x   y Q\ x\ y 表示查询去除 ( x , y ) (x,y) 边之后, x x 所在树的节点数 * y y 所在树的节点数。 ( 1 n , q 1 0 5 ) (1\leq n,q\leq 10^5)

思路: 我们一般遇到的都是维护链上节点个数的问题,而此题要求这颗树上的节点个数,因此我们需要同时维护虚边和实边的信息。

我们令 s z [ x ] sz[x] 表示节点 x x 子树中节点个数, s z 2 [ x ] sz2[x] 表示节点 x x 虚儿子的节点个数和。因此 s z [ x ] = s z [ l s ] + s z [ r s ] + 1 + s z 2 [ x ] sz[x]=sz[ls]+sz[rs]+1+sz2[x] ,而这也正是 p u s h U p pushUp 函数。

因此我们只需要维护 s z 2 [ x ] sz2[x] 即可,然后观察哪些函数会改变 s z 2 [ x ] sz2[x] 的值,不难发现,只有 m a k e R o o t makeRoot a c c e s s access l i n k link c u t cut 会改变边的虚实关系,其中 m a k e R o o t makeRoot 主要修改在于调用了 a c c e s s access 函数,而 c u t cut 只是删除实边不会修改虚边,因此真正关键的函数即为 l i n k link a c c e s s access 函数,具体操作见代码,不难思考。

这里主要讲解 l i n k link 中修改 s z 2 [ y ] sz2[y] 信息时为什么需要将节点 y y m a k e R o o t ( y ) makeRoot(y) ,原因在于单点修改之后,其祖先节点维护的信息都会发生变化,因此一般的问题需要 s p l a y ( y ) splay(y) ,因为一般问题只需要维护实链信息。然后在该问题中还维护了虚链信息,因此需要 m a k e R o o t ( y ) makeRoot(y) 而不是 s p l a y ( y ) splay(y)

代码:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const int N = 100010;
int n, q;

struct LCT{
  #define ls ch[p][0]
  #define rs ch[p][1]
  #define Get(p) (ch[f[p]][1] == p)
  int ch[N][2], f[N], siz[N], siz2[N], rev[N];

  inline void clear(int p){ //清除这个点的信息
    ch[p][0] = ch[p][1] = f[p] = siz[p] = siz2[p] = rev[p] = 0;
  }

  inline int isRoot(int p){
    clear(0);
    return ch[f[p]][0] != p && ch[f[p]][1] != p;
  }

  inline void pushUp(int p){
    clear(0);
    siz[p] = siz[ls] + 1 + siz[rs] + siz2[p];
  }

  inline void pushDown(int p){
    clear(0);
    if(rev[p]){ 
      if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
      if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
      rev[p] = 0;
    }
  }

  void update(int p){ //递归地从上到下pushDown信息
    if(!isRoot(p)) update(f[p]);
    pushDown(p);
  }

  inline void rotate(int x){ //将x向上旋转一层的操作
    int y = f[x], z = f[y], k = Get(x);
    if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
    ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
    ch[x][!k] = y, f[y] = x, f[x] = z;
    pushUp(y); //要先pushUp(y)
    pushUp(x);
  }

  inline void splay(int x){ //把x旋转到当前splay的根
    update(x); //将上面的标记完全下放
    for(int fa; fa = f[x], !isRoot(x); rotate(x)){
      if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
    }
  }

  inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
    int p; //每次改变右儿子的值,因为整棵树是中序遍历,放入右儿子才能保证先遍历父亲再遍历儿子
    for(p = 0; x; p = x, x = f[x]){
      splay(x), siz2[x] += siz[ch[x][1]]-siz[p], ch[x][1] = p, pushUp(x);
    }
    return p;
  }

  inline void makeRoot(int p){ //使x点成为整棵树的根
    access(p); splay(p);
    swap(ch[p][0],ch[p][1]); //把整条链反向
    rev[p] ^= 1;
  }

  inline void link(int x,int y){ //在x、y两点间连一条边
    //makeRoot(x)的作用是使得x无父亲
    //makeRoot(y)的作用是使得y无父亲,因此可以修改y的信息,不用去更新y的祖先
    makeRoot(x), makeRoot(y), f[x] = y, siz2[y] += siz[x]; pushUp(y);
  }

  inline void cut(int x,int p){ //把x、y两点间边删掉,此处删除的是实边,注意实边和虚边的区别
    makeRoot(x), access(p), splay(p);
    if (ls == x && !rs) ls = f[x] = 0;
  }

  inline int find(int p){ //找到x所在树的根节点编号
    access(p), splay(p);
    while(ls) pushDown(p), p = ls;
    return p;
  }
}st;

signed main() {
  scanf("%d%d", &n, &q);
  rep(i,1,n) st.siz[i] = 1;
  while (q--) {
    char op[10]; int x,y; scanf("%s%d%d",op,&x,&y);
    if(op[0] == 'A') st.link(x,y);
    else{
      st.cut(x,y);
      st.makeRoot(x); st.splay(x);
      int a1 = st.siz[x];
      st.makeRoot(y); st.splay(y);
      int a2 = st.siz[y];
      st.link(x,y);
      printf("%lld\n",(ll)a1*(ll)a2);
    }
  }
  return 0;
}
7. Sone1

题意: 支持 12 12 种操作,包括链 m a x max m i n min s u m sum ,子树 m a x max m i n min s u m sum ,换根,换边,子树和链的修改与加值。 ( 1 n , m 1 0 5 ) (1\leq n,m\leq 10^5)

思路: T o p T r e e TopTree 典型例题,主要思路是对于每个点维护了一个 s p l a y splay ,详情看 c l a r i s claris 的题解

代码:
贴上 c l a i r s clairs 的代码。

/*
Toptree即为可以维护子树信息的lct升级版
*/
#include<cstdio>
#define N 200010
const int inf=~0U>>1;
inline void swap(int&a,int&b){int c=a;a=b;b=c;}
inline int max(int a,int b){return a>b?a:b;}
inline int min(int a,int b){return a<b?a:b;}
inline void read(int&a){
  char c;bool f=0;a=0;
  while(!((((c=getchar())>='0')&&(c<='9'))||(c=='-')));
  if(c!='-')a=c-'0';else f=1;
  while(((c=getchar())>='0')&&(c<='9'))(a*=10)+=c-'0';
  if(f)a=-a;
}
struct tag{
  int a,b;//ax+b
  tag(){a=1,b=0;}
  tag(int x,int y){a=x,b=y;}
  inline bool ex(){return a!=1||b;}
  inline tag operator+(const tag&x){return tag(a*x.a,b*x.a+x.b);}
};
inline int atag(int x,tag y){return x*y.a+y.b;}
struct data{
  int sum,minv,maxv,size;
  data(){sum=size=0,minv=inf,maxv=-inf;}
  data(int x){sum=minv=maxv=x,size=1;}
  data(int a,int b,int c,int d){sum=a,minv=b,maxv=c,size=d;}
  inline data operator+(const data&x){return data(sum+x.sum,min(minv,x.minv),max(maxv,x.maxv),size+x.size);}
};
inline data operator+(const data&a,const tag&b){return a.size?data(a.sum*b.a+a.size*b.b,atag(a.minv,b),atag(a.maxv,b),a.size):a;}
//son:0-1:重链儿子,2-3:AAA树儿子
int f[N],son[N][4],a[N],tot,rt,rub,ru[N];bool rev[N],in[N];
int val[N];
data csum[N],tsum[N],asum[N];
tag ctag[N],ttag[N];
inline bool isroot(int x,int t){
  if(t)return !f[x]||!in[f[x]]||!in[x];
  return !f[x]||(son[f[x]][0]!=x&&son[f[x]][1]!=x)||in[f[x]]||in[x];
}
inline void rev1(int x){
  if(!x)return;
  swap(son[x][0],son[x][1]);rev[x]^=1;
}
inline void tagchain(int x,tag p){
  if(!x)return;
  csum[x]=csum[x]+p;
  asum[x]=csum[x]+tsum[x];
  val[x]=atag(val[x],p);
  ctag[x]=ctag[x]+p;
}
inline void tagtree(int x,tag p,bool t){
  if(!x)return;
  tsum[x]=tsum[x]+p;
  ttag[x]=ttag[x]+p;
  if(!in[x]&&t)tagchain(x,p);else asum[x]=csum[x]+tsum[x];
}
inline void pb(int x){
  if(!x)return;
  if(rev[x])rev1(son[x][0]),rev1(son[x][1]),rev[x]=0;
  if(!in[x]&&ctag[x].ex())tagchain(son[x][0],ctag[x]),tagchain(son[x][1],ctag[x]),ctag[x]=tag();
  if(ttag[x].ex()){
    tagtree(son[x][0],ttag[x],0),tagtree(son[x][1],ttag[x],0);
    tagtree(son[x][2],ttag[x],1),tagtree(son[x][3],ttag[x],1);
    ttag[x]=tag();
  }
}
inline void up(int x){
  tsum[x]=data();
  for(int i=0;i<2;i++)if(son[x][i])tsum[x]=tsum[x]+tsum[son[x][i]];
  for(int i=2;i<4;i++)if(son[x][i])tsum[x]=tsum[x]+asum[son[x][i]];
  if(in[x]){
    csum[x]=data();
    asum[x]=tsum[x];
  }else{
    csum[x]=data(val[x]);
    for(int i=0;i<2;i++)if(son[x][i])csum[x]=csum[x]+csum[son[x][i]];
    asum[x]=csum[x]+tsum[x];
  }
}
inline int child(int x,int t){pb(son[x][t]);return son[x][t];}
inline void rotate(int x,int t){
  int y=f[x],w=(son[y][t+1]==x)+t;
  son[y][w]=son[x][w^1];
  if(son[x][w^1])f[son[x][w^1]]=y;
  if(f[y])for(int z=f[y],i=0;i<4;i++)if(son[z][i]==y)son[z][i]=x;
  f[x]=f[y];f[y]=x;son[x][w^1]=y;up(y);
}
inline void splay(int x,int t=0){
  int s=1,i=x,y;a[1]=i;
  while(!isroot(i,t))a[++s]=i=f[i];
  while(s)pb(a[s--]);
  while(!isroot(x,t)){
    y=f[x];
    if(!isroot(y,t)){if((son[f[y]][t]==y)^(son[y][t]==x))rotate(x,t);else rotate(y,t);}
    rotate(x,t);
  }
  up(x);
}
inline int newnode(){
  int x=rub?ru[rub--]:++tot;
  son[x][2]=son[x][3]=0;in[x]=1;
  return x;
}
inline void setson(int x,int t,int y){son[x][t]=y;f[y]=x;}
inline int pos(int x){for(int i=0;i<4;i++)if(son[f[x]][i]==x)return i;return 4;}
inline void add(int x,int y){//从x连出一条虚边到y
  if(!y)return;
  pb(x);
  for(int i=2;i<4;i++)if(!son[x][i]){
    setson(x,i,y);
    return;
  }
  while(son[x][2]&&in[son[x][2]])x=child(x,2);
  int z=newnode();
  setson(z,2,son[x][2]);
  setson(z,3,y);
  setson(x,2,z);
  splay(z,2);
}
inline void del(int x){//将x与其虚边上的父亲断开
  if(!x)return;
  splay(x);
  if(!f[x])return;
  int y=f[x];
  if(in[y]){
    int s=1,i=y,z=f[y];a[1]=i;
    while(!isroot(i,2))a[++s]=i=f[i];
    while(s)pb(a[s--]);
    if(z){
      setson(z,pos(y),child(y,pos(x)^1));
      splay(z,2);
    }
    ru[++rub]=y;
  }else{
    son[y][pos(x)]=0;
    splay(y);
  }
  f[x]=0;
}
inline int fa(int x){//x通过虚边的父亲
  splay(x);
  if(!f[x])return 0;
  if(!in[f[x]])return f[x];
  int t=f[x];
  splay(t,2);
  return f[t];
}
inline int access(int x){
  int y=0;
  for(;x;y=x,x=fa(x)){
    splay(x);
    del(y);
    add(x,son[x][1]);
    setson(x,1,y);
    up(x);
  }
  return y;
}
inline int lca(int x,int y){
  access(x);
  return access(y);
}
inline int root(int x){
  access(x);
  splay(x);
  while(son[x][0])x=son[x][0];
  return x;
}
inline void makeroot(int x){
  access(x);
  splay(x);
  rev1(x);
}
inline void link(int x,int y){
  makeroot(x);
  add(y,x);
  access(x);
}
inline void cut(int x){
  access(x);
  splay(x);
  f[son[x][0]]=0;
  son[x][0]=0;
  up(x);
}
inline void changechain(int x,int y,tag p){
  makeroot(x);
  access(y);
  splay(y);
  tagchain(y,p);
}
inline data askchain(int x,int y){
  makeroot(x);
  access(y);
  splay(y);
  return csum[y];
}
inline void changetree(int x,tag p){
  access(x);
  splay(x);
  val[x]=atag(val[x],p);
  for(int i=2;i<4;i++)if(son[x][i])tagtree(son[x][i],p,1);
  up(x);
  splay(x);
}
inline data asktree(int x){
  access(x);
  splay(x);
  data t=data(val[x]);
  for(int i=2;i<4;i++)if(son[x][i])t=t+asum[son[x][i]];
  return t;
}
int n,m,x,y,z,k,i,ed[N][2];
int main(){
  read(n);read(m);
  tot=n;
  for(i=1;i<n;i++)read(ed[i][0]),read(ed[i][1]); //连边
  for(i=1;i<=n;i++)read(val[i]),up(i); //先赋点权,再连边
  for(i=1;i<n;i++)link(ed[i][0],ed[i][1]); //每个点的权值
  read(rt); //给出根
  makeroot(rt);
  while(m--){
    read(k);
    if(k==1){//换根,x变成根
      read(rt);
      makeroot(rt);
    }
    if(k==9){//x的父亲变成y,x父亲换成y
      read(x),read(y);
      if(lca(x,y)==x)continue;
      cut(x);
      link(y,x);
      makeroot(rt);
    }
    if(k==0){//子树赋值,以x为根的子树点权值改为y
      read(x),read(y);
      changetree(x,tag(0,y));
    }
    if(k==5){//子树加,x为根子树点权值加上y
      read(x),read(y);
      changetree(x,tag(1,y));
    }
    if(k==3){//子树最小值,x为根子树中点权值求min
      read(x);
      printf("%d\n",asktree(x).minv);
    }
    if(k==4){//子树最大值,x为根子树中点权值求max
      read(x);
      printf("%d\n",asktree(x).maxv);
    }
    if(k==11){//子树和,x为根子树中点权sum
      read(x);
      printf("%d\n",asktree(x).sum);
    }
    if(k==2){//链赋值,x-y路径上点权值改为z
      read(x),read(y),read(z);
      changechain(x,y,tag(0,z));
      makeroot(rt);
    }
    if(k==6){//链加,x-y路径上点权值加上z
      read(x),read(y),read(z);
      changechain(x,y,tag(1,z));
      makeroot(rt);
    }
    if(k==7){//链最小值,x-y路径上点权值求min
      read(x),read(y);
      printf("%d\n",askchain(x,y).minv);
      makeroot(rt);
    }
    if(k==8){//链最大值,x-y路径上点权值求max
      read(x),read(y);
      printf("%d\n",askchain(x,y).maxv);
      makeroot(rt);
    }
    if(k==10){//链和,x-y路径上点权值求sum
      read(x),read(y);
      printf("%d\n",askchain(x,y).sum);
      makeroot(rt);
    }
  }
  return 0;
}

后记

本篇博客到这里就结束了,祝大家 A C AC 愉快,一起爱上 L C T LCT 把!(๑•̀ㅂ•́)و✧

发布了244 篇原创文章 · 获赞 115 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_41552508/article/details/100974440