P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并

题意

Luogu P4556

维护一个无根树, 每个节点可容纳不同物品, 实现路径修改单个物品数量, 离线查询每个节点最后的存在最多的物品.

倍增求 LCA

最近公共祖先 (LCA), 指树上两个点深度最大的公共祖先, 也是两点路径上深度最小的点. 很显然, 树上两点路径唯一, LCA 是必然经过的点, 也是解决此问题必须求出的点.

倍增法是求 LCA 的一种常用方法. 其思想是记录每一个点的 \(2^k\) 代祖先, 通过往深度小的节点跳最后找到 LCA.

LCA 是本题前置知识, 请务必保证熟练掌握.

树上差分

一般的线性差分是在操作区间左端点操作, 然后在右端点右边反向操作, 最后差分数组前缀和就是操作后数组对应位置的值.

对于树上差分, 在路径两端进行操作, 并且分别在 LCA 和 LCA 的父亲上反向操作, 则一个节点操作后的值是它所在子树的差分值总和. 也就是说, 树上差分将前缀和改为 "叶缀和", 不少初学者会理解为 "根缀和" (就是我). 至于为什么要这样做, 可以画个图感性理解一下, 有几分容斥原理的味道.

动态开点权值线段树

由于是维护区间最值和最值位置, 所以要开权值线段树. 而且由于空间紧迫, 必须动态开点. 前几篇讨论线段树的博文已经多次提到这种操作了, 这里也就不再赘述.

值得一提的是它维护的最值, 由于一个节点的两个子节点存储的信息互不交叉, 所以这个节点的最值必然是两个子节点的最值其中之一. 这个说法看似弱智, 但使本题和蒲公英有了本质区别, 蒲公英维护的是有先后顺序的数组的区间众数, 和地址有关就不能建权值线段树. 而本题求的是集合的众数, 所以只要在值域上开权值线段树就能解决问题.

树套线段树

对于无根树, 直接规定第一个节点为根, 因为哪个点为根不影响结果.

和以往不同的是, 这次要把树上差分和线段树结合起来, 一个差分操作加减的不是一个数, 而是一棵线段树. 所以在树上每个点开一个动态开点权值线段树, 虽然看起来很浪费空间, 但是一开始每个点只是留了一个树根指针而已.

线段树合并

路径修改操作用差分处理是对于四个节点上的线段树的单独修改, 所以这个阶段线段树之间没有往来, 就像一般动态开点线段树一样.

但是最后, 为了求线段树的和, 我们需要在回溯时把儿子节点的信息都汇聚到他们的父亲上, 便引出了线段树合并. 实际上, 之前有一道题不光涉及了线段树合并, 还涉及了线段树分裂, 这便是Luogu P5494, 我同样写了一篇博客来记录.

实现

本题的局部如 LCA, 线段树合并, 在之前的题目中都遇到过, 但是对于这种两种东西套起来的接口实现, 这还是第一次尝试, 所以有些代码部分注释可能会很少.

存储

三个结构体, 分别是线段树节点, 题目中树的节点, 边. 仍然采用内存池 + 指针的方式操作.

unsigned int M, n, Ans[100005] /*存答案*/;
int Lst, A, B, C, D;  //全局变量存操作参数和答案防止调用函数时浪费时间
struct Node {
  Node *L, *R;            //左右儿子指针
  int Mxn, Mx;            //众数出现次数, 众数
} N[10000005], *Cntn(N);  //内存池 + 指针
struct Edge;
struct Grp {
  Edge *Fst;
  int Dep;      //深度
  Grp *Fa[18];  // 2^k代祖先
  Node *Tre;    //对应的线段树根指针
} G[100005], *Aci /*求出来的 LCA 指针暂存处*/;
struct Edge {
  Edge *Nxt;
  Grp *To;
} E[200005], *Cnte(E);  //邻接表存树

线段树上传 Udt

由于是动态开点, 所以分类讨论避免访问空指针

inline void Udt(Node *x) {
  if (x->L && x->R) {              //两儿都有
    if (x->L->Mxn >= x->R->Mxn) {  //左儿众数计数较多
      x->Mx = x->L->Mx;
      x->Mxn = x->L->Mxn;
      return;
    }
    x->Mx = x->R->Mx;  //右儿众数计数较多
    x->Mxn = x->R->Mxn;
    return;
  }
  if (x->L) {  //有左无右
    x->Mx = x->L->Mx;
    x->Mxn = x->L->Mxn;
    return;
  }
  if (x->R) {  //有右无左
    x->Mx = x->R->Mx;
    x->Mxn = x->R->Mxn;
    return;
  }
  return;  //一个儿子都没有就什么也不做
}

线段树修改 Chg

这里的修改是单点修改, 无需Tag, 以叶节点为边界, 一个节点最多递归调用一个子节点

void Chg(Node *x, unsigned int l, const unsigned int &r) {
  if (l == r) {   //边界
    x->Mx = l;    //自己就是众数
    x->Mxn += D;  //对出现次数的差分值进行修改
    return;
  }
  unsigned int m((l + r) >> 1);
  if (m >= C) {  //单点在左子树
    if (!(x->L)) {
      Chg(x->L = ++Cntn, l, m);  //要开就开
    } else {
      Chg(x->L, l, m);  //本来就有, 无需开点
    }
    return Udt(x);  //将影响上传
  }
  if (!(x->R)) {                   //单点在右子树
    Chg(x->R = ++Cntn, m + 1, r);  //要开就开
  } else {
    Chg(x->R, m + 1, r);  //本来就有, 无需开点
  }
  return Udt(x);  //将影响上传
}

线段树合并 Into

同步遍历两棵线段树 (不知道这个概念的请参见我的这一篇博客), 根据两个节点的儿子重合情况分类讨论, 直接继承或继续递归.

边界是叶子节点, 当叶子节点重合时, 将两个同位叶子的差分值取和, 众数都是叶子代表的单点.

回溯时更新祖先.

void Into(Node *x /*To*/, Node *y /*From*/, unsigned int l,
          const unsigned int &r) {
  if (l == r) {        //边界
    x->Mxn += y->Mxn;  //加和
    x->Mx = l;         //统一
    return;
  }
  unsigned int m((l + r) >> 1);
  if (x->L) {                  //目标节点有左儿
    if (y->L) {                //源节点也有左儿
      Into(x->L, y->L, l, m);  //继续递归
    }
  } else {  //目标节点无左儿, 无论源节点如何, 继承总是正确的
    x->L = y->L;
  }
  if (x->R) {  //同上
    if (y->R) {
      Into(x->R, y->R, m + 1, r);
    }
  } else {
    x->R = y->R;
  }
  Udt(x);  //儿子最后更新自己
  return;
}

建树 Lnk Bld

邻接表存树, 双向连边, 规定节点 \(1\) 为根, DFS并处理深度, \(2^k\) 代祖先等信息

void Lnk(Grp *x, Grp *y) {  // x, y 间连边
  Cnte->Nxt = x->Fst;       //先指
  x->Fst = Cnte;            //再断
  (Cnte++)->To = y;         //最后接上终点
  return;
}
void Bld(Grp *x) {  // DFS建树
  Edge *Sid(x->Fst);
  while (Sid) {                   //枚举边
    if (Sid->To != x->Fa[0]) {    //不是父亲, 就是儿子
      Sid->To->Fa[0] = x;         //初代祖先(父亲)
      Sid->To->Dep = x->Dep + 1;  //深度
      unsigned int i(0);
      while (Sid->To->Fa[i]) {
        Sid->To->Fa[i + 1] = Sid->To->Fa[i]->Fa[i];  //倍增祖先
        ++i;
      }
      Bld(Sid->To);  //继续深入
    }
    Sid = Sid->Nxt;  //下一条
  }
  return;
}

倍增 LCA

求两点的 LCA, 先将深度大的点单独跳至同一深度, 再将两点同时向上, 直到两点成为其 LCA 的两个儿子为止.

这里注意一个特判, 当两点之一成为 LCA 时, 第一步单独跳会使两点直接重合, 这是直接返回两点所重合的指针即可.

Grp *LCA(Grp *x, Grp *y) {
  if (x->Dep < y->Dep) {  //保证 x 的深度大
    swap(x, y);
  }
  unsigned int i(17);
  while (x->Dep > y->Dep) {  //单独跳
    if (x->Fa[i]) {
      if (x->Fa[i]->Dep >= y->Dep) {
        x = x->Fa[i];
      }
    }
    --i;
  }
  if (x == y) {  //特判
    return x;
  }
  i = 17;
  while (i <= 17) {
    if (x->Fa[i] != y->Fa[i]) {
      x = x->Fa[i];
      y = y->Fa[i];
    }
    --i;
  }
  return x->Fa[0];  // LCA恰好是公共父亲
}

"叶缀和" DFS

DFS 遍历整棵树, 回溯时将子节点的线段树合并到自己的线段树上, 自己线段树的根节点记录的众数就是答案.

需要注意的是, 不能指望最后直接输出对应节点的线段树根的相应值, 因为线段树合并时会将子树继承给父亲, 导致这棵子树在后面要不断被更新, 破坏了之前求得的答案, 所以要开个数组在刚求出答案时存下来.

还有一个特判, 即这个点没有任何哪种物品, 答案不存在, 输出 \(0\).

void DFS(Grp *x) {
  Edge *Sid(x->Fst);
  while (Sid) {  // DFS
    if (Sid->To != x->Fa[0]) {
      DFS(Sid->To);
      if (Sid->To->Tre) {
        if (x->Tre) {
          Into(x->Tre, Sid->To->Tre, 1, 100000);
        } else {
          x->Tre =
              Sid->To->Tre;  //注意有些节点没有线段树, 所以直接继承儿子的整棵树
        }
      }
    }
    Sid = Sid->Nxt;
  }
  if (x->Tre) {  //记录答案
    if (x->Tre->Mxn) {
      Ans[x - G] = x->Tre->Mx;
    } else {
      Ans[x - G] = 0;
    }
  } else {
    Ans[x - G] = 0;
  }
  return;
}

主函数 main

操作基本上都封装好了, 主函数的主体便是每次操作的树上差分了. 对于差分的第 \(4\) 次单点修改, 这也提醒我们, 一定要防止访问空指针, 避免 RE.

int main() {
  memset(N, 0, sizeof(N));
  memset(G, 0, sizeof(G));
  n = RD();
  M = RD();  //输入和初始化
  for (register unsigned int i(1); i < n; ++i) {
    A = RD();
    B = RD();
    Lnk(G + A, G + B);
    Lnk(G + B, G + A);  //双向连边
  }
  G[1].Dep = 1;                           //深度边界
  Bld(G + 1);                             //建树
  for (register int i(1); i <= M; ++i) {  //操作
    A = RD();
    B = RD();
    C = RD();
    D = 1;
    Aci = LCA(G + A, G + B);  //求LCA
    if (!G[A].Tre) {          //保证没有空指针
      G[A].Tre = ++Cntn;
    }
    if (!G[B].Tre) {
      G[B].Tre = ++Cntn;
    }
    if (!Aci->Tre) {
      Aci->Tre = ++Cntn;
    }
    Chg(G[A].Tre, 1, 100000);  //树上差分
    Chg(G[B].Tre, 1, 100000);
    D = -1;
    Chg(Aci->Tre, 1, 100000);
    if (Aci->Fa[0]) {
      if (!Aci->Fa[0]->Tre) {
        Aci->Fa[0]->Tre = ++Cntn;
      }
      Chg(Aci->Fa[0]->Tre, 1,
          100000);  //如果根节点是 LCA, 根节点没有父亲, 这一步就要省去
    }
  }
  DFS(G + 1);  //整理答案
  for (register int i(1); i <= n; ++i) {
    printf("%d\n", Ans[i]);
  }
  return 0;
}
posted @ 2021-02-01 16:07  Wild_Donkey  阅读(145)  评论(0编辑  收藏  举报