自顶向下的 leaf 的 rake 的全局平衡二叉树

· · 算法·理论

前置科技

(阅读本文不需要,但是看懂我写的标程需要看一下。)

rake

全局平衡二叉树不支持子树修改,这十分傻,怎么解决呢?

我会三度化

三度化后每个节点最多只有一个轻子树,可以暴力 push_down,push_up。 注意,三度化可能会让 lca 选到虚点,此时我们需要将这个虚点对应的实点手动加入路径,下文会介绍一个诡异的实现方式。

我会 top tree

我们可以直接对每个节点建个 rake tree,而在重链仍然使用全局平衡二叉树的结构,这样跟 top tree 的区别就是不用点集化了,因为它对重链是按点建的。

我会不管

考虑直接建 rake tree,然后把他视为一个对重链建的全局平衡二叉树,我们声称这就是对的。

具体来说,建 rake tree 就是为了三度化,此时 rake tree 就对应一条只包含虚点的重链。

leaf

leaf 的全局平衡二叉树想必大家都知道,leaf 的 rake tree 也是好实现的,于是我们就得到了一个 leaf 的这东西。

组合意义就是非叶子节点对应一条重边,叶子节点对应点,rake tree 的叶子对应轻边,非叶子对应它在原树上的父亲这个点,所以你维护边相关信息时会跟平衡树长得差不多,且你不用再写边集化了。

目前,似乎只有这个东西能同时干掉点集化和边集化。

自顶向下

传统的全局平衡二叉树是自下向顶的,导致链操作实现非常反人类。

有一个结论,就是重链上的一个区间,其对应的轻子树在 dfs 序也是一个区间,这个读者自证不难,不过要注意,dfs 序小的节点对应的轻子树 dfs 序反而更大,想要解决这个问题可以考虑翻转一下 dfs 序,不过翻转过后它对应的 dfs 序区间还是两个。

因此,我们就可以 O(1) 判断链的左右端点是在左节点还是在右节点。

如果我们发现查询的链就是这个节点对应的重链,就直接返回。

发现这条链在轻子树 + 当前节点中就算上这个点的贡献,向轻子树递归。

如果都完全在左右节点中的一个,直接递归,反之就从中间劈开,再递归。

搭配上 leaf,我们就可以只写 5 种情况,而不是 9 种。

注意,这个东西你无法保证 dfs 序小的一定在左边,大的一定在右边,因为其没有祖孙关系。

这个复杂度看起来有点错,但是实际上是对的。

考虑设 F(x) 表示总大小为 x 时的复杂度,G(x) 表示要求链的一个端点在界点的复杂度。

然后就可以列出

F(x)= \max (F(x/2)+1,2G(x/2)) G(x)=G(x/2)+1

显然是一个老哥的,从而,我们就可以实现自顶向下,而且这个东西不需要求个 lca 再分开,也不需要对 lca 所在的重链做特判。

子树修改就等价于对点到对应重链底这条链本身和其轻子树做修改。

同时这个东西可以顺带求 lca,因为 lca 是路径上 dfs 序最小的点。

细节

这个东西做一些点相关的东西可能不能完全不管,就是push_up,push_down 必须得管一下。

如果在 rake tree 中记一下对应的实点,在 rake tree 中分治时如果发现链的两个端点分别在左边和右边且都为实点,就在中间加入对应的实点,这个显然不会算重,也不需要求 lca,且可以非常方便的实现严格的按顺序访问。

非常奇特的,我们可以将实点视作 rake tree 的非叶子节点对应的边,将轻边视作 rake tree 的非叶子节点对应的点,就会发现这个东西大一统了,虽然将点视为边,边视为点有点奇怪,但是这样似乎没有破坏原树的性质,因为两个点中间有边和两个边中间有点看起来是差不多的。

这个东西应该是不弱于 top tree 的。

例题:

一道绝世好题,std。

所有静态且在树上的 top tree 可以做的题。

本来 top tree 做太石现在可以做的题(如某乘凉州题,提交记录,虽然因为我的常数有点大没能夺得最优解,但强制在线已经赢麻了,且在格式化后跟树剖做法长度几乎相同)。

一些细节:

该代码轻子树和重链是分开维护的,想要子树信息需要合并,好处就是支持不满足可减性的链改,坏处就是你要维护一个不连通的簇的信息。

翻转 dfs 序后这个东西几乎就跟线段树一模一样。

这个东西维护树上邻域就跟 top tree 几乎一模一样,考虑上界点和下界点的邻域,合并。

这个东西常数有点大,但是在常规数据跑得过树剖,大约是树剖的两三倍快,卡些常或者数据比较强可能差的多一点。

下述代码采用了一个十分神奇的建树方式,就是将重链对应的区间和轻子树对应的区间直接传进去,如果重链上只有一个点就递归建轻子树,反之就正常分治。

采用了完全无到儿子的指针的写法,跟前置科技说的 dfs 序线段树差不多。

采用了很多前置科技提到的写法,可能需要学习一下。

这里有一个非常 modern c++ 的模板树链剖分的代码,不长,也就 170 行。


#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int sz[N],ndf[N],dfn[N],dfr[N],ed[N],son[N],fw[N];
int es[N],ef[N],n,q,w,sd[N],mod,a[N];
vector<int> e[N],g[N];
int M(const int& x){
  return x - ((-(x >= mod)) & mod);
}
struct node{
  int s,ss,ad,as,l,mid,r,sl,sr;
  int que(int l,int r){
    return sd[r] - sd[l - 1];
  }
  #define lc (*(this + 1))
  #define rc (*(this + (lc.qsz() << 1)))
  #define mc (*(this + 1))
  // 中儿子
  int qsz(){
    return (sl == 0)?r - l + 1:r - l + 1 + sr - sl + 1;
  }
  void push_up(){
    if (l == r){
      ss = M(mc.s + mc.ss);
      return;
    }
    s = M(lc.s + rc.s);
    ss = M(lc.ss + rc.ss);

  }
  void tag(int x,int y){
    ss = (ss + 1ll * que(sl,sr) * y) % mod;
    s = (s + 1ll * que(l,r) * x) % mod;
    as = M(as + y),ad = M(ad + x);
  }
  void push_down(){
    if (l == r){
      return mc.tag(as,as),ad = as = 0,void();
    }
    lc.tag(ad,as),rc.tag(ad,as);
    ad = as = 0;
  }
  bool in(int x){
    return ((l <= x) && (r >= x)) || ((sl <= x) && (sr >= x));
  }
  void upd(int u,int v,const auto& Do){
    (u > v) && (swap(u,v),1);
    if ((l == u) && (r == v)){
      return Do(*this);
    }
    push_down();
    if (l == r){
      return (l == u) && (Do(*this),1),mc.upd(mc.in(u)?u:mc.l,v,Do),push_up();
    }
    if (lc.in(u) && lc.in(v)){
      lc.upd(u,v,Do);
    }
    else if (rc.in(u) && rc.in(v)){
      rc.upd(u,v,Do);
    }
    else if (lc.in(u) && rc.in(v)){
      lc.upd(u,mid,Do),rc.upd(mid + 1,v,Do);
    }
    else if (lc.in(v) && rc.in(u)){
      lc.upd(v,mid,Do),rc.upd(mid + 1,u,Do);
    }
    push_up();
  }
  int qsz(int l,int r){
    return sz[ndf[l]] - sz[son[ndf[r]]];
  }
  void build(int _l,int _r,int _sl,int _sr){
    l = _l,r = _r,sl = _sl,sr = _sr;
    if (l == r){
      s = a[ndf[l]];
      if (sl <= sr){
        return mc.build(sl,ed[sl],ed[sl] + 1,sr),push_up();
      }
      return;
    }
    int ls = l,rs = r;
    while (ls <= rs){
      int mid = (ls + rs) >> 1;
      (qsz(l,mid) * 2>= qsz(l,r))?rs = mid - 1:ls = mid + 1;
    }
    mid = max(l,min(ls,r - 1));
    lc.build(l,mid,sl,dfr[mid]);
    rc.build(mid + 1,r,dfr[mid] + 1,sr);
    push_up();
  }
}t[N << 2];
int gm,tot,cnt,own[N];
void sdh(int x,int fa){
  vector<int > tmp;
  for (auto &v : g[x]){
    if (v ^ fa){
      sdh(v,x);
      tmp.push_back(v);
    }
  }
  while (tmp.size() > 2){
    ++cnt;
    auto v1 = tmp.back();
    tmp.pop_back();
    auto v2 = tmp.back();
    tmp.pop_back();
    e[cnt].push_back(v1);
    e[cnt].push_back(v2);
    own[cnt] = x;
    tmp.push_back(cnt);
  }
  e[x] = tmp;
}
void dfs1(int x){
  sz[x] = 1;
  for (auto &v : e[x]){
    dfs1(v);
    sz[x] += sz[v];
    (sz[v] > sz[son[x]]) && (son[x] = v);
  }
}
void dfs2(int x){
  for (int i = x;i;i = son[i]){
    ndf[dfn[i] = ++tot] = i;
    sd[tot] = sd[tot - 1] + (i <= n);
  }
  int ned = tot;
  for (int i = x;i;i = son[i]){
    for (auto &v : e[i]){
      if (v ^ son[i]){
        dfs2(v);
      }
    }
    dfr[dfn[i]] = tot;
    ed[dfn[i]] = ned;
  }
}
int Ed(int x){
  return ndf[ed[dfn[x]]];
}
signed main(){
  ios::sync_with_stdio(0),cin.tie(0);
  int rt;
  cin >> n >> q >> rt >> mod;
  for (int i = 1;i <= n;i++){
    cin >> a[i];
  }
  for (int i = 1,u,v,w;i < n;i++){
    cin >> u >> v;
    g[u].push_back(v);
    g[v].push_back(u);
  }
  cnt = n,sdh(rt,0);
  dfs1(rt),dfs2(rt);
  t[0].build(1,ed[1],ed[1] + 1,cnt);
  for (int i = 1,op,x,y,z;i <= q;i++){
    auto to_do = [&](int u,int v,const auto& Do){
      u = dfn[u],v = dfn[v];
      (u > v) && (swap(u,v),1);
      int lca = 1e9;
      t[0].upd(u,v,[&](node &a){Do(a),lca = min(lca,a.l);});
      (own[ndf[lca]]) && (lca = dfn[own[ndf[lca]]],t[0].upd(lca,lca,Do),1);
    };
    cin >> op >> x;
    (op == 1) && (cin >> y >> z,to_do(x,y,[&](node &a){a.tag(z,0);}),1);
    (op == 2) && (cin >> y,z = 0,to_do(x,y,[&](node &a){z = (z + a.s) % mod;}),cout << z << "\n",1);
    (op == 3) && (cin >> z,to_do(x,Ed(x),[&](node & a){a.tag(z,z);}),1);
    (op == 4) && (z = 0,to_do(x,Ed(x),[&](node &a){z = (z + a.s + a.ss) % mod;}),cout << z << "\n",1);
  }

}

这里有一个 p6666 的不完全管的代码