AIGC标识 可持久化线段树

zhuxi树(可持久化权值线段树)完全指南


第一章 可持久化数据结构概述

1.1 什么是可持久化

可持久化(Persistent) 是指数据结构能够保留其所有历史版本的状态,允许在任意时刻查询任意历史版本的信息,同时支持从某个历史版本派生出新版本。可持久化的核心思想是路径复制(Path Copying) :每次修改只复制被修改节点到根路径上的所有节点,未修改的部分则与旧版本共享。

1.2 zhuxi树的由来

zhuxi树全称是可持久化权值线段树(Persistent Weighted Segment Tree) ,因其引入者姓名缩写与某位国家zhuxi相同,故在中文OI圈被称为“zhuxi树”。它是一种基于权值线段树的可持久化数据结构,主要用于解决区间第 \(k\) 小/大值问题。

1.3 zhuxi树与普通线段树的区别

普通线段树在修改时会直接修改原有节点;而zhuxi树在修改时不会修改任何已有节点,而是创建新节点,新节点继承旧节点的左右儿子指针,仅修改的路径上的节点被复制并更新。


第二章 静态zhuxi树:区间第k小

2.1 问题模型

给定长度为 \(n\) 的序列 \(a_1, a_2, \dots, a_n\)\(m\) 次询问,每次询问区间 \([l, r]\) 中的第 \(k\) 小值。

2.2 核心思想

利用前缀和的思想:为每个前缀 \([1, i]\) 建立一棵权值线段树,第 \(i\) 棵线段树中存储了 \(a_1 \dots a_i\) 中每个值出现的次数。那么区间 \([l, r]\) 的权值线段树 = 第 \(r\) 棵线段树 - 第 \(l-1\) 棵线段树。

在查询时,我们从两棵线段树的根同时出发,比较左子树的差值来确定第 \(k\) 小在左子树还是右子树中。

2.3 离散化

由于权值可能很大,需要先对原数组离散化,将值映射到 \(1 \sim n\) 的范围内,这样权值线段树的大小就是 \(O(n)\) 的。

2.4 建树与插入

\(root_i\) 为第 \(i\) 个版本的根节点编号。插入第 \(i\) 个数时,从 \(root_{i-1}\) 复制路径,创建一个新版本 \(root_i\)。每次插入会新建 \(O(\log n)\) 个节点。

2.5 查询第k小

设当前查询区间为 \([l, r]\),当前节点区间为 \([L, R]\),令 \(mid = \frac{L+R}{2}\)。令 \(u = root_r\)\(v = root_{l-1}\),左子树中的元素个数为 \(cnt = tree[tree[u].ls].sum - tree[tree[v].ls].sum\)

  • \(k \le cnt\),则第 \(k\) 小在左子树中,递归查询左子树;
  • 否则第 \(k\) 小在右子树中,递归查询右子树,且 \(k \leftarrow k - cnt\)

\(L = R\) 时返回 \(L\)

2.6 复杂度分析

  • 建树:\(O(n \log n)\)
  • 查询:\(O(\log n)\)
  • 空间:\(O(n \log n)\)

2.7 模板代码(洛谷 P3834)

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m,a[N],b[N],root[N],tot;
struct Node{int ls,rs,sum;}tr[N*32];

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].sum++;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

int query(int u,int v,int l,int r,int k)
{
    if(l==r)return l;
    int mid=(l+r)>>1;
    int cnt=tr[tr[u].ls].sum-tr[tr[v].ls].sum;
    if(k<=cnt)return query(tr[u].ls,tr[v].ls,l,mid,k);
    else return query(tr[u].rs,tr[v].rs,mid+1,r,k-cnt);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    for(int i=1;i<=n;i++){cin>>a[i];b[i]=a[i];}
    sort(b+1,b+n+1);
    int len=unique(b+1,b+n+1)-b-1;
    for(int i=1;i<=n;i++)
    {
        int p=lower_bound(b+1,b+len+1,a[i])-b;
        update(root[i],root[i-1],1,len,p);
    }
    while(m--)
    {
        int l,r,k;
        cin>>l>>r>>k;
        int p=query(root[r],root[l-1],1,len,k);
        cout<<b[p]<<"\n";
    }
    return 0;
}

第三章 zhuxi树的数学基础

3.1 前缀和定理

定理 3.1(前缀和可减性)\(T_i\) 为前 \(i\) 个元素构成的权值线段树,则区间 \([l, r]\) 的权值线段树 \(T_{[l,r]}\) 满足:

\[T_{[l,r]} = T_r - T_{l-1} \]

即对任意节点,其值为 \(T_r\) 中该节点的值减去 \(T_{l-1}\) 中该节点的值。

证明\(T_i\) 中每个叶节点存储值 \(x\) 在前 \(i\) 个元素中出现的次数。设 \(f_i(x)\)\(x\) 在前 \(i\) 个元素中出现的次数,则 \(f_r(x) - f_{l-1}(x)\) 正是 \(x\)\([l, r]\) 中出现的次数。证毕。

3.2 zhuxi树的空间复杂度定理

定理 3.2(空间复杂度) 对长度为 \(n\) 的序列建立zhuxi树,若每次插入创建 \(O(\log n)\) 个节点,则总节点数为 \(O(n \log n)\)

推论 3.1 对于 \(n \le 2 \times 10^5\) 的题目,zhuxi树数组通常开 \(n \times 32\)\(n \times 40\) 大小。

3.3 第k小查询的正确性定理

定理 3.3 设区间 \([l, r]\) 的权值线段树上,左子树中元素个数为 \(c\)。若 \(k \le c\),则第 \(k\) 小在左子树;否则第 \(k-c\) 小在右子树。

证明:权值线段树按值域划分,左子树对应的值域全部小于右子树对应的值域。左子树中有 \(c\) 个元素,它们分别是第 \(1, 2, \dots, c\) 小。若 \(k \le c\),则第 \(k\) 小必然在左子树中;否则第 \(k\) 小是右子树中的第 \(k-c\) 小。证毕。


第四章 zhuxi树的常见变形

4.1 可持久化数组

可持久化数组是最基础的可持久化应用:支持单点修改,并保留历史版本,可在任意版本查询任意位置的值。实现时以数组下标为版本,用zhuxi树维护。

4.2 带修zhuxi树(树状数组套zhuxi树)

问题:支持单点修改,同时查询区间第 \(k\) 小。

核心思想:静态zhuxi树利用前缀和建树,修改 \(a_i\) 会影响 \(root_i, root_{i+1}, \dots, root_n\),复杂度不可接受。改用树状数组维护权值线段树的前缀和:树状数组的每个节点 \(C_i\) 维护一棵权值线段树,管理区间 \([i - \text{lowbit}(i) + 1, i]\)

查询:要得到 \([l, r]\) 的权值线段树,需要求出 \(r\) 处的前缀和与 \(l-1\) 处的前缀和。树状数组前缀和查询会分解为 \(O(\log n)\) 棵线段树,将这些线段树的根记录下来,在查询时同步二分。

复杂度:修改 \(O(\log^2 n)\),查询 \(O(\log^2 n)\)

4.3 树上zhuxi树(路径第k小)

问题:给定一棵树,每个节点有权值,查询 \(u\)\(v\) 路径上的第 \(k\) 小点权。

核心思想:从根节点 DFS,为每个节点 \(u\) 建立从其父节点 \(fa[u]\) 的zhuxi树继承而来的一棵新树。这样 \(root_u\) 就代表了从根到 \(u\) 路径上所有点的权值线段树。

查询 \(u \to v\) 路径时,设 \(lca = \text{LCA}(u, v)\),则路径上的权值线段树为:

\[T_{path} = T_u + T_v - T_{lca} - T_{fa[lca]} \]

即四个版本同时参与查询。

公式(查询时左子树的元素个数):

\[cnt = sum[ls_u] + sum[ls_v] - sum[ls_{lca}] - sum[ls_{fa[lca]}] \]

4.4 可持久化Trie(01-Trie)

可持久化Trie与zhuxi树的思想完全一致:每次插入时只复制路径上的节点。常用于解决区间最大异或和问题。

问题:给定区间 \([l, r]\) 和一个数 \(x\),求 \(a_p \oplus x\) 的最大值(\(l \le p \le r\))。

做法:为每个前缀建立一棵可持久化01-Trie,第 \(i\) 棵Trie包含 \(a_1 \dots a_i\)。查询时用第 \(r\) 棵和第 \(l-1\) 棵做差,从高位到低位贪心选择与 \(x\) 当前位相反的路径。

4.5 zhuxi树+区间合并(连续1)

某些问题需要维护区间内连续1的最长长度。线段树的每个节点维护三个值:

  • \(lmx\):从左端点开始的最长连续1长度
  • \(rmx\):从右端点结束的最长连续1长度
  • \(mx\):区间内最长连续1长度

合并公式:

\[lmx = lmx_L + (mx_L == \text{len}_L \ ? \ lmx_R : 0) \]

\[rmx = rmx_R + (mx_R == \text{len}_R \ ? \ rmx_L : 0) \]

\[mx = \max(mx_L, mx_R, rmx_L + lmx_R) \]

配合zhuxi树可以预处理出多个阈值下的01序列。

4.6 zhuxi树与二维数点

zhuxi树天然适合解决强制在线二维数点问题。以下标为版本,以权值为线段树下标,查询区间 \([l, r]\) 中值在 \([x, y]\) 范围内的元素个数。

4.7 可持久化并查集

用zhuxi树维护并查集的 \(fa\) 数组,每次合并时新建一个版本。不能使用路径压缩(因为路径压缩会修改多个节点),需使用按秩合并保证复杂度为 \(O(\log n)\)


第五章 典型例题精讲

5.1 abc453g G - Copy Query

题意:有 \(N\) 个长度为 \(M\) 的整数列,初始全为0。三种操作:

  1. 将数列 \(X\) 复制为数列 \(Y\)\(R_X \leftarrow R_Y\)
  2. 将数列 \(X\) 的第 \(p\) 个元素改为 \(v\)
  3. 求数列 \(X\) 区间 \([l, r]\) 的和

思路:维护每个数列对应的线段树根节点 \(R_i\)。操作1直接令 \(R_X = R_Y\);操作2在 \(R_X\) 基础上单点修改生成新根;操作3区间查询。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m,q,tot;
int root[N];
struct Node{int ls,rs,sum;}tr[N*40];

void update(int&u,int v,int l,int r,int pos,int val)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].sum+=val;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(pos<=mid)update(tr[u].ls,tr[v].ls,l,mid,pos,val);
    else update(tr[u].rs,tr[v].rs,mid+1,r,pos,val);
}

int query(int u,int l,int r,int ql,int qr)
{
    if(!u||ql>r||qr<l)return 0;
    if(ql<=l&&r<=qr)return tr[u].sum;
    int mid=(l+r)>>1;
    return query(tr[u].ls,l,mid,ql,qr)+query(tr[u].rs,mid+1,r,ql,qr);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)root[i]=0;
    while(q--)
    {
        int op,x;
        cin>>op>>x;
        if(op==1)
        {
            int y;
            cin>>y;
            root[x]=root[y];
        }
        else if(op==2)
        {
            int p,v;
            cin>>p>>v;
            update(root[x],root[x],1,m,p,v);
        }
        else
        {
            int l,r;
            cin>>l>>r;
            cout<<query(root[x],1,m,l,r)<<"\n";
        }
    }
    return 0;
}

5.2 jzyz522 静态区间第k小

标准静态zhuxi树模板,见第二章模板代码。

5.3 cses3163 Range Interval Queries

题意:查询区间 \([l, r]\) 中值在 \([c, d]\) 之间的元素个数。

思路:zhuxi树以数组下标为版本,权值为线段树下标。查询时统计第 \(r\) 棵和第 \(l-1\) 棵在值域 \([c, d]\) 上的差值。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m,a[N],b[N],root[N],tot;
struct Node{int ls,rs,sum;}tr[N*32];

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].sum++;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

int query(int u,int v,int l,int r,int ql,int qr)
{
    if(ql>r||qr<l)return 0;
    if(ql<=l&&r<=qr)return tr[u].sum-tr[v].sum;
    int mid=(l+r)>>1;
    return query(tr[u].ls,tr[v].ls,l,mid,ql,qr)+query(tr[u].rs,tr[v].rs,mid+1,r,ql,qr);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    for(int i=1;i<=n;i++){cin>>a[i];b[i]=a[i];}
    sort(b+1,b+n+1);
    int len=unique(b+1,b+n+1)-b-1;
    for(int i=1;i<=n;i++)
    {
        int p=lower_bound(b+1,b+len+1,a[i])-b;
        update(root[i],root[i-1],1,len,p);
    }
    while(m--)
    {
        int l,r,c,d;
        cin>>l>>r>>c>>d;
        int L=lower_bound(b+1,b+len+1,c)-b;
        int R=upper_bound(b+1,b+len+1,d)-b-1;
        if(L>R)cout<<"0\n";
        else cout<<query(root[r],root[l-1],1,len,L,R)<<"\n";
    }
    return 0;
}

5.4 BZOJ2588 Spoj 10628. Count on a tree

题意:树上路径第 \(k\) 小点权(强制在线)。

思路:DFS建树上zhuxi树,查询时用 \(u, v, lca, fa[lca]\) 四个版本差分。LCA使用倍增法。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m;
int val[N],b[N],root[N],tot;
int up[N][20],dep[N];
vector<int>g[N];
struct Node{int ls,rs,sum;}tr[N*40];

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].sum++;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

void dfs(int u,int fa)
{
    up[u][0]=fa;
    dep[u]=dep[fa]+1;
    for(int i=1;i<20;i++)up[u][i]=up[up[u][i-1]][i-1];
    int p=lower_bound(b+1,b+n+1,val[u])-b;
    update(root[u],root[fa],1,n,p);
    for(int v:g[u])if(v!=fa)dfs(v,u);
}

int lca(int u,int v)
{
    if(dep[u]<dep[v])swap(u,v);
    for(int i=19;i>=0;i--)if(dep[up[u][i]]>=dep[v])u=up[u][i];
    if(u==v)return u;
    for(int i=19;i>=0;i--)if(up[u][i]!=up[v][i]){u=up[u][i];v=up[v][i];}
    return up[u][0];
}

int query(int u,int v,int lc,int flc,int l,int r,int k)
{
    if(l==r)return l;
    int mid=(l+r)>>1;
    int cnt=tr[tr[u].ls].sum+tr[tr[v].ls].sum-tr[tr[lc].ls].sum-tr[tr[flc].ls].sum;
    if(k<=cnt)return query(tr[u].ls,tr[v].ls,tr[lc].ls,tr[flc].ls,l,mid,k);
    else return query(tr[u].rs,tr[v].rs,tr[lc].rs,tr[flc].rs,mid+1,r,k-cnt);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    for(int i=1;i<=n;i++){cin>>val[i];b[i]=val[i];}
    sort(b+1,b+n+1);
    int len=unique(b+1,b+n+1)-b-1;
    for(int i=1;i<n;i++)
    {
        int x,y;cin>>x>>y;
        g[x].push_back(y);g[y].push_back(x);
    }
    dfs(1,0);
    int last=0;
    while(m--)
    {
        int u,v,k;
        cin>>u>>v>>k;
        u^=last;
        int lc=lca(u,v);
        int p=query(root[u],root[v],root[lc],root[up[lc][0]],1,len,k);
        last=b[p];
        cout<<last<<"\n";
    }
    return 0;
}

5.5 P484E Sign on Fence

题意:给定数列,每次询问区间 \([l, r]\) 内选长度为 \(w\) 的连续子区间,使子区间最小数的最大值最大。

思路:二分答案 \(mid\),将 \(\ge mid\) 的数标为1,\(< mid\) 的数标为0,判断区间内最长连续1的长度是否 \(\ge w\)。由于不同阈值下的01序列只有单点差异,用zhuxi树预处理从大到小排序后的版本。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m,tot;
int root[N];
pair<int,int>p[N];
struct Node{int ls,rs,lmx,rmx,mx;}tr[N*40];

void pushup(int u,int l,int r)
{
    int mid=(l+r)>>1;
    int lc=tr[u].ls,rc=tr[u].rs;
    tr[u].lmx=tr[lc].lmx;
    if(tr[lc].mx==mid-l+1)tr[u].lmx=mid-l+1+tr[rc].lmx;
    tr[u].rmx=tr[rc].rmx;
    if(tr[rc].mx==r-mid)tr[u].rmx=r-mid+tr[lc].rmx;
    tr[u].mx=max({tr[lc].mx,tr[rc].mx,tr[lc].rmx+tr[rc].lmx});
}

void update(int&u,int v,int l,int r,int pos)
{
    u=++tot;
    tr[u]=tr[v];
    if(l==r){tr[u].lmx=tr[u].rmx=tr[u].mx=1;return;}
    int mid=(l+r)>>1;
    if(pos<=mid)update(tr[u].ls,tr[v].ls,l,mid,pos);
    else update(tr[u].rs,tr[v].rs,mid+1,r,pos);
    pushup(u,l,r);
}

Node query(int u,int l,int r,int ql,int qr)
{
    if(ql<=l&&r<=qr)return tr[u];
    int mid=(l+r)>>1;
    if(qr<=mid)return query(tr[u].ls,l,mid,ql,qr);
    if(ql>mid)return query(tr[u].rs,mid+1,r,ql,qr);
    Node L=query(tr[u].ls,l,mid,ql,qr);
    Node R=query(tr[u].rs,mid+1,r,ql,qr);
    Node res;
    res.lmx=L.lmx;
    if(L.mx==mid-max(l,ql)+1)res.lmx=mid-max(l,ql)+1+R.lmx;
    res.rmx=R.rmx;
    if(R.mx==min(r,qr)-mid)res.rmx=min(r,qr)-mid+L.rmx;
    res.mx=max({L.mx,R.mx,L.rmx+R.lmx});
    return res;
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n;
    for(int i=1;i<=n;i++){cin>>p[i].first;p[i].second=i;}
    sort(p+1,p+n+1,greater<pair<int,int>>());
    for(int i=1;i<=n;i++)
        update(root[i],root[i-1],1,n,p[i].second);
    cin>>m;
    while(m--)
    {
        int l,r,w;
        cin>>l>>r>>w;
        int lo=1,hi=n,ans=1;
        while(lo<=hi)
        {
            int mid=(lo+hi)>>1;
            Node res=query(root[mid],1,n,l,r);
            if(res.mx>=w){ans=mid;lo=mid+1;}
            else hi=mid-1;
        }
        cout<<p[ans].first<<"\n";
    }
    return 0;
}

5.6 BZOJ1901 Dynamic Rankings / P2617 动态区间第k小

题意:支持单点修改和区间第 \(k\) 小查询。

思路:树状数组套zhuxi树。树状数组的每个节点维护一棵权值线段树,修改时更新 \(O(\log n)\) 棵线段树,查询时将 \(O(\log n)\) 棵线段树的根记录到两个数组中,同步二分。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m,tot;
int a[N],b[N],root[N];
int tmp1[N],tmp2[N],cnt1,cnt2;
struct Node{int ls,rs,sum;}tr[N*200];
struct Q{int op,l,r,k;}q[N];

int lowbit(int x){return x&-x;}

void update(int&u,int l,int r,int pos,int val)
{
    if(!u)u=++tot;
    tr[u].sum+=val;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(pos<=mid)update(tr[u].ls,l,mid,pos,val);
    else update(tr[u].rs,mid+1,r,pos,val);
}

void modify(int x,int pos,int val)
{
    for(int i=x;i<=n;i+=lowbit(i))
        update(root[i],1,tot,pos,val);
}

int query(int l,int r,int k)
{
    if(l==r)return l;
    int mid=(l+r)>>1;
    int sum=0;
    for(int i=1;i<=cnt1;i++)sum+=tr[tr[tmp1[i]].ls].sum;
    for(int i=1;i<=cnt2;i++)sum-=tr[tr[tmp2[i]].ls].sum;
    if(k<=sum)
    {
        for(int i=1;i<=cnt1;i++)tmp1[i]=tr[tmp1[i]].ls;
        for(int i=1;i<=cnt2;i++)tmp2[i]=tr[tmp2[i]].ls;
        return query(l,mid,k);
    }
    else
    {
        for(int i=1;i<=cnt1;i++)tmp1[i]=tr[tmp1[i]].rs;
        for(int i=1;i<=cnt2;i++)tmp2[i]=tr[tmp2[i]].rs;
        return query(mid+1,r,k-sum);
    }
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    for(int i=1;i<=n;i++){cin>>a[i];b[++tot]=a[i];}
    for(int i=1;i<=m;i++)
    {
        char c;cin>>c;
        if(c=='Q'){q[i].op=0;cin>>q[i].l>>q[i].r>>q[i].k;}
        else{q[i].op=1;cin>>q[i].l>>q[i].r;b[++tot]=q[i].r;}
    }
    sort(b+1,b+tot+1);
    int len=unique(b+1,b+tot+1)-b-1;
    tot=len;
    for(int i=1;i<=n;i++)
    {
        int p=lower_bound(b+1,b+len+1,a[i])-b;
        modify(i,p,1);
    }
    for(int i=1;i<=m;i++)
    {
        if(q[i].op==0)
        {
            cnt1=cnt2=0;
            for(int j=q[i].r;j;j-=lowbit(j))tmp1[++cnt1]=root[j];
            for(int j=q[i].l-1;j;j-=lowbit(j))tmp2[++cnt2]=root[j];
            int p=query(1,len,q[i].k);
            cout<<b[p]<<"\n";
        }
        else
        {
            int pos=q[i].l,val=q[i].r;
            int old=lower_bound(b+1,b+len+1,a[pos])-b;
            modify(pos,old,-1);
            a[pos]=val;
            int ne=lower_bound(b+1,b+len+1,val)-b;
            modify(pos,ne,1);
        }
    }
    return 0;
}

5.7 jzyz1615 最大异或和(洛谷 P4735)

题意:维护序列,支持末尾添加数、查询 \([l, r]\)\(a_p \oplus x\) 的最大值。

思路:可持久化01-Trie。设前缀异或和 \(s_i = a_1 \oplus \dots \oplus a_i\),查询 \(a_p \oplus \dots \oplus a_n \oplus x = s_n \oplus s_{p-1} \oplus x\)。令 \(y = s_n \oplus x\),问题转化为在 \([l-1, r-1]\) 中找 \(s_{p-1}\) 使 \(s_{p-1} \oplus y\) 最大。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=6e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m,tot;
int s[N],root[N];
int tr[N*25][2],mxid[N*25];

void insert(int&u,int v,int x,int id)
{
    u=++tot;
    int p=u,q=v;
    for(int i=23;i>=0;i--)
    {
        int c=(x>>i)&1;
        tr[p][c]=++tot;
        tr[p][c^1]=tr[q][c^1];
        p=tr[p][c];
        q=tr[q][c];
        mxid[p]=id;
    }
}

int query(int u,int v,int x)
{
    int res=0;
    for(int i=23;i>=0;i--)
    {
        int c=(x>>i)&1;
        if(mxid[tr[v][c^1]]>mxid[tr[u][c^1]]||tr[v][c^1]!=tr[u][c^1])
        {
            if(tr[v][c^1]&&mxid[tr[v][c^1]]>mxid[tr[u][c^1]])
            {
                res|=(1<<i);
                u=tr[u][c^1];v=tr[v][c^1];
            }
            else
            {
                u=tr[u][c];v=tr[v][c];
            }
        }
        else
        {
            u=tr[u][c];v=tr[v][c];
        }
    }
    return res;
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    mxid[0]=-1;
    insert(root[0],0,0,0);
    for(int i=1;i<=n;i++)
    {
        int x;cin>>x;
        s[i]=s[i-1]^x;
        insert(root[i],root[i-1],s[i],i);
    }
    while(m--)
    {
        char op;cin>>op;
        if(op=='A')
        {
            int x;cin>>x;
            n++;
            s[n]=s[n-1]^x;
            insert(root[n],root[n-1],s[n],n);
        }
        else
        {
            int l,r,x;
            cin>>l>>r>>x;
            int y=s[n]^x;
            cout<<query(root[l-2],root[r-1],y)<<"\n";
        }
    }
    return 0;
}

5.8 loj2016 「SCOI2016」美味

题意:给定数列 \(a_1 \dots a_n\),每次询问 \(b, x, l, r\),求 \(\max_{i \in [l,r]} (b \oplus (a_i + x))\)

思路:按位贪心。从高位到低位,假设当前位为 \(d\),尝试让答案的第 \(d\) 位与 \(b\) 的第 \(d\) 位不同。若 \(b\) 的第 \(d\) 位为0,则希望 \(a_i + x\) 的第 \(d\) 位为1;反之亦然。在zhuxi树上查询值域区间 \([L, R]\) 内是否存在元素。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m,a[N],root[N],tot;
struct Node{int ls,rs,sum;}tr[N*32];

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].sum++;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

int query(int u,int v,int l,int r,int ql,int qr)
{
    if(ql>r||qr<l)return 0;
    if(ql<=l&&r<=qr)return tr[u].sum-tr[v].sum;
    int mid=(l+r)>>1;
    return query(tr[u].ls,tr[v].ls,l,mid,ql,qr)+query(tr[u].rs,tr[v].rs,mid+1,r,ql,qr);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    int maxv=0;
    for(int i=1;i<=n;i++){cin>>a[i];maxv=max(maxv,a[i]);}
    for(int i=1;i<=n;i++)update(root[i],root[i-1],0,maxv,a[i]);
    while(m--)
    {
        int b,x,l,r;
        cin>>b>>x>>l>>r;
        int ans=0;
        for(int d=17;d>=0;d--)
        {
            int want=((b>>d)&1)^1;
            int base=ans|(want<<d);
            int L=base-x,R=base+(1<<d)-1-x;
            L=max(L,0LL);
            if(L>R||query(root[r],root[l-1],0,maxv,L,R)==0)
            {
                int other=((b>>d)&1);
                ans|=other<<d;
            }
            else ans=base;
        }
        cout<<(ans^b)<<"\n";
    }
    return 0;
}

5.9 abc324gL G - Generate Arrays

题意:给定一个排列,维护多个序列。操作1:将序列 \(s_i\) 中下标 \(\ge x\) 的元素分离出来形成新序列;操作2:将序列 \(s_i\) 中值 \(> x\) 的元素分离出来形成新序列。输出新序列长度。

思路:用 Wavelet Matrix 或zhuxi树维护二维点 \((i, p_i)\)。每个序列对应一个矩形区域,操作相当于在矩形内按某一维切分。用zhuxi树可以 \(O(\log n)\) 统计矩形内点数。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,q,a[N],root[N],tot;
struct Node{int ls,rs,sum;}tr[N*32];
struct Seq{int xl,xr,yl,yr;};
vector<Seq>seqs;

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].sum++;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

int query(int u,int v,int l,int r,int ql,int qr)
{
    if(ql>r||qr<l)return 0;
    if(ql<=l&&r<=qr)return tr[u].sum-tr[v].sum;
    int mid=(l+r)>>1;
    return query(tr[u].ls,tr[v].ls,l,mid,ql,qr)+query(tr[u].rs,tr[v].rs,mid+1,r,ql,qr);
}

int count(int xl,int xr,int yl,int yr)
{
    if(xl>xr||yl>yr)return 0;
    return query(root[xr],root[xl-1],1,n,yl,yr);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n;
    for(int i=1;i<=n;i++){cin>>a[i];update(root[i],root[i-1],1,n,a[i]);}
    seqs.push_back({1,n,1,n});
    cin>>q;
    while(q--)
    {
        int t,s,x;
        cin>>t>>s>>x;
        Seq cur=seqs[s];
        if(t==1)
        {
            int mid=cur.xl+x-1;
            Seq left={cur.xl,mid,cur.yl,cur.yr};
            Seq right={mid+1,cur.xr,cur.yl,cur.yr};
            seqs.push_back(right);
            seqs[s]=left;
            cout<<right.xr-right.xl+1<<"\n";
        }
        else
        {
            int l=cur.yl,r=cur.yr;
            int lo=l,hi=r,ans=r;
            while(lo<=hi)
            {
                int mid=(lo+hi)>>1;
                if(count(cur.xl,cur.xr,l,mid)<=x){ans=mid;lo=mid+1;}
                else hi=mid-1;
            }
            Seq low={cur.xl,cur.xr,cur.yl,ans};
            Seq high={cur.xl,cur.xr,ans+1,cur.yr};
            seqs.push_back(high);
            seqs[s]=low;
            cout<<high.yr-high.yl+1<<"\n";
        }
    }
    return 0;
}

5.10 abc405fW F - Chord Crossing

题意:圆上 \(2N\) 个点,连 \(N\) 条弦。每次询问一条新弦,求它与已有弦的交点个数。

思路:弦 \((l_1, r_1)\)\((l_2, r_2)\) 相交当且仅当 \(l_1 < l_2 < r_1 < r_2\)\(l_2 < l_1 < r_2 < r_1\)。问题转化为二维数点:查询满足 \(l_i < L < r_i < R\) 的弦数加上满足 \(L < l_i < R < r_i\) 的弦数。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,q;
int L[N],R[N];
vector<int>vals;
int root[N],tot;
struct Node{int ls,rs,sum;}tr[N*32];

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].sum++;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

int query(int u,int v,int l,int r,int ql,int qr)
{
    if(ql>r||qr<l)return 0;
    if(ql<=l&&r<=qr)return tr[u].sum-tr[v].sum;
    int mid=(l+r)>>1;
    return query(tr[u].ls,tr[v].ls,l,mid,ql,qr)+query(tr[u].rs,tr[v].rs,mid+1,r,ql,qr);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>q;
    for(int i=1;i<=n;i++){cin>>L[i]>>R[i];vals.push_back(R[i]);}
    sort(vals.begin(),vals.end());
    vals.erase(unique(vals.begin(),vals.end()),vals.end());
    int sz=vals.size();
    vector<pair<int,int>>chords;
    for(int i=1;i<=n;i++)
    {
        int r=lower_bound(vals.begin(),vals.end(),R[i])-vals.begin()+1;
        chords.push_back({L[i],r});
    }
    sort(chords.begin(),chords.end());
    for(int i=0;i<n;i++)
        update(root[i+1],root[i],1,sz,chords[i].second);
    while(q--)
    {
        int l,r;cin>>l>>r;
        int cnt1=0,cnt2=0;
        int pos=lower_bound(chords.begin(),chords.end(),make_pair(l,0LL))-chords.begin();
        if(pos>0)cnt1=query(root[pos],root[0],1,sz,1,sz)-query(root[pos],root[0],1,sz,1,r);
        int pos2=lower_bound(chords.begin(),chords.end(),make_pair(r,0LL))-chords.begin();
        if(pos2>pos)cnt2=query(root[pos2],root[pos],1,sz,1,sz);
        cout<<cnt1+cnt2<<"\n";
    }
    return 0;
}

5.11 bpabc438g G - Sum of Min

题意:给定两个数组 \(A, B\),求 \(\sum_{i=0}^{n-1} \min(a_i, b_{(pn+i) \bmod m})\) 的某种形式。

思路:将问题转化为对每个 \(a_i\),统计满足 \(b_j < a_i\)\(b_j\) 之和以及个数,用zhuxi树维护 \(B\) 的权值线段树,查询值域前缀的和。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m;
int a[N],b[N];
int root[N],tot;
struct Node{int ls,rs,cnt,sum;}tr[N*32];

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].cnt++;
    tr[u].sum+=x;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

pair<int,int>query(int u,int v,int l,int r,int ql,int qr)
{
    if(ql>r||qr<l)return {0,0};
    if(ql<=l&&r<=qr)return {tr[u].cnt-tr[v].cnt,tr[u].sum-tr[v].sum};
    int mid=(l+r)>>1;
    auto L=query(tr[u].ls,tr[v].ls,l,mid,ql,qr);
    auto R=query(tr[u].rs,tr[v].rs,mid+1,r,ql,qr);
    return {L.first+R.first,L.second+R.second};
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=m;i++){cin>>b[i];update(root[i],root[i-1],1,N-5,b[i]);}
    int g=__gcd(n,m);
    int ans=0;
    for(int i=1;i<=n;i++)
    {
        int cnt=0,sum=0;
        for(int j=i;j<=m;j+=g)
        {
            auto res=query(root[m],root[0],1,N-5,1,a[i]-1);
            cnt+=res.first;sum+=res.second;
        }
        ans+=cnt*a[i]+sum;
    }
    cout<<ans<<"\n";
    return 0;
}

5.12 jzyz2420 [JSOI2018] line(军训列队)

题意:给定数列 \(a_1 \dots a_n\),每次询问将区间 \([l, r]\) 内的数重新排列到连续位置 \([k, k + r - l]\) 的最小代价。

思路:将 \(a[l..r]\) 排序后与 \(k, k+1, \dots, k+r-l\) 对应相减。在zhuxi树上二分找到分界点,使左边的数都 \(\le\) 对应位置,右边的数都 \(>\) 对应位置,然后分别计算。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=5e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m;
int a[N],root[N],tot;
struct Node{int ls,rs,cnt,sum;}tr[N*32];

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].cnt++;
    tr[u].sum+=x;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

int query(int u,int v,int l,int r,int ql,int qr)
{
    if(ql>r||qr<l)return 0;
    if(ql<=l&&r<=qr)return tr[u].sum-tr[v].sum;
    int mid=(l+r)>>1;
    return query(tr[u].ls,tr[v].ls,l,mid,ql,qr)+query(tr[u].rs,tr[v].rs,mid+1,r,ql,qr);
}

int querycnt(int u,int v,int l,int r,int ql,int qr)
{
    if(ql>r||qr<l)return 0;
    if(ql<=l&&r<=qr)return tr[u].cnt-tr[v].cnt;
    int mid=(l+r)>>1;
    return querycnt(tr[u].ls,tr[v].ls,l,mid,ql,qr)+querycnt(tr[u].rs,tr[v].rs,mid+1,r,ql,qr);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    for(int i=1;i<=n;i++){cin>>a[i];update(root[i],root[i-1],1,N-5,a[i]);}
    while(m--)
    {
        int l,r,k;
        cin>>l>>r>>k;
        int len=r-l+1;
        int lo=k,hi=k+len-1;
        int ans=0;
        while(lo<=hi)
        {
            int mid=(lo+hi)>>1;
            int cnt=querycnt(root[r],root[l-1],1,N-5,1,mid);
            if(cnt<=mid-k+1)
            {
                int sum=query(root[r],root[l-1],1,N-5,1,mid);
                int rest=len-cnt;
                int sum2=query(root[r],root[l-1],1,N-5,mid+1,N-5);
                int start2=k+cnt;
                int total2=rest*start2+rest*(rest-1)/2;
                ans=abs(sum-(cnt*k+cnt*(cnt-1)/2))+abs(sum2-total2);
                lo=mid+1;
            }
            else hi=mid-1;
        }
        cout<<ans<<"\n";
    }
    return 0;
}

5.13 BZOJ4369 [IOI2015] teams分组

题意\(N\) 个学生,每个学生愿意在规模 \([A_i, B_i]\) 的小组中。\(Q\) 次询问,每次给出 \(K\) 个小组的规模 \(k_1 \dots k_K\),判断能否分配。

思路:将学生看作二维点 \((A_i, B_i)\),每个小组 \(k\) 的需求对应矩形 \([0, k] \times [k, \infty]\)。用单调栈维护折线,zhuxi树做二维数点。

// 此题实现较复杂,核心是用zhuxi树统计矩形内点数
// 具体实现参考上述二维数点模板
// 每次询问按 k 排序后用单调栈处理合并
#include <bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int L=10000005,M=200005;
int n,N,cnt,st[M],K[M],rt[3*M],h[M],sz[M];
struct Tr{int num,ls,rs;}t[L];
struct node{int x,y;}e[L];
bool cmp(node a,node b){return a.x<b.x;}
inline int ms(int l,int r){return (r+l)>>1;}
inline void insert(int pre,int &id,int l,int r,int ID)
{
	id=++cnt;
	t[id].num=t[pre].num+1;
	t[id].ls=t[pre].ls;
	t[id].rs=t[pre].rs;
	if(l==r) return ;
	int mid=ms(l,r);
	if(ID<=mid) insert(t[pre].ls,t[id].ls,l,mid,ID);
	else insert(t[pre].rs,t[id].rs,mid+1,r,ID);
}
inline int query_K(int pre,int id,int l,int r,int k)
{
	if(l==r) return l;
	int mid=ms(l,r);
	int ans=t[t[id].rs].num-t[t[pre].rs].num;
	if(ans>=k) return query_K(t[pre].rs,t[id].rs,mid+1,r,k);
	else return query_K(t[pre].ls,t[id].ls,l,mid,k-ans);
}
inline int query(int pre,int id,int l,int r,int k)
{
	if(!id) return 0;
	if(l==r) return t[id].num-t[pre].num;
	int mid=ms(l,r);
	if(k<=mid) return t[t[id].rs].num-t[t[pre].rs].num+query(t[pre].ls,t[id].ls,l,mid,k);
	else return query(t[pre].rs,t[id].rs,mid+1,r,k);
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++) cin>>e[i].x>>e[i].y;
    sort(e+1,e+n+1,cmp);
    N=n+1;
    int head=1;
    for(int i=1;i<=N;i++)
    {
    	rt[i]=rt[i-1];
    	while(head<=n&&e[head].x==i) insert(rt[i],rt[i],1,N,e[head++].y);
	}
	int Q;
	cin>>Q;
	while(Q--)
	{
		int m;
		cin>>m;
		for(int i=1;i<=m;i++) cin>>K[i];
		sort(K+1,K+m+1);
		int top=0;
		for(int i=1;i<=m;i++)
		{
			while(top&&h[top]<K[i]) top--;
			int Sz=sz[top];
			Sz+=query(rt[K[st[top]]],rt[K[i]],1,N,K[i])-K[i];
			if(Sz<0)
			{
				cout<<"0\n";
				break;
			}
			else if(i==m)
			{
				cout<<"1\n";
				break;
			}
			int H=query_K(rt[K[st[top]]],rt[K[i]],1,N,Sz-sz[top]);
			while(H>h[top]&&top)
			{
				top--;
				H=query_K(rt[K[st[top]]],rt[K[i]],1,N,Sz-sz[top]);
			}
			st[++top]=i;
			sz[top]=Sz;
			h[top]=H;
		}
	}
    return 0;
}

5.14 cses2184 Missing Coin Sum Queries

题意:查询区间 \([l, r]\) 的最小不可表示和。

思路:经典贪心:维护当前可表示范围 \([1, x]\),每次找 \(\le x+1\) 的最小数加入。用zhuxi树在值域上查询区间内 \(\le x+1\) 的数的和,不断扩展直到无法扩展。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;

int n,m;
int a[N],root[N],tot;
struct Node{int ls,rs,sum;}tr[N*32];

void update(int&u,int v,int l,int r,int x)
{
    u=++tot;
    tr[u]=tr[v];
    tr[u].sum+=x;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(tr[u].ls,tr[v].ls,l,mid,x);
    else update(tr[u].rs,tr[v].rs,mid+1,r,x);
}

int query(int u,int v,int l,int r,int ql,int qr)
{
    if(ql>r||qr<l)return 0;
    if(ql<=l&&r<=qr)return tr[u].sum-tr[v].sum;
    int mid=(l+r)>>1;
    return query(tr[u].ls,tr[v].ls,l,mid,ql,qr)+query(tr[u].rs,tr[v].rs,mid+1,r,ql,qr);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    for(int i=1;i<=n;i++){cin>>a[i];update(root[i],root[i-1],1,N-5,a[i]);}
    while(m--)
    {
        int l,r;cin>>l>>r;
        int x=0;
        while(true)
        {
            int s=query(root[r],root[l-1],1,N-5,1,min(x+1,N-5LL));
            if(s<=x)break;
            x=s;
        }
        cout<<x+1<<"\n";
    }
    return 0;
}

第六章 高级话题与研究扩展

6.1 zhuxi树与整体二分

静态区间第 \(k\) 小也可以用整体二分解决,时间复杂度 \(O((n + q) \log n \log V)\),空间复杂度 \(O(n + q)\),优于zhuxi树的 \(O(n \log n)\) 空间。但整体二分是离线的,zhuxi树支持强制在线。

6.2 树套树的其他形式

除了树状数组套zhuxi树,还有线段树套平衡树树状数组套Trie等。zhuxi树套树状数组适合解决带修区间第 \(k\) 小,而树状数组套Trie适合解决带修区间最大异或和。

6.3 可持久化平衡树

可持久化Treap(无旋Treap)也可实现可持久化,且常数通常优于zhuxi树。无旋Treap的Split和Merge操作天然支持路径复制,实现可持久化非常自然。

6.4 zhuxi树在二维问题中的应用

zhuxi树可以解决许多二维数点问题:

  • 矩形内点数查询
  • 矩形内权值和查询
  • 区间内值在 \([L, R]\) 的元素个数

核心思想是以下标为版本,权值为线段树下标,利用前缀和差分。

6.5 zhuxi树的常数优化技巧

  1. 离散化:减少值域大小,降低树高。
  2. 节点回收:带修zhuxi树中,被覆盖的节点可以回收利用。
  3. 数组开大:通常开 \(n \times 32\)\(n \times 40\) 避免RE。
  4. 快读快写:对于 \(2 \times 10^5\) 级别的数据,使用快读能显著提速。
  5. 递归改迭代:查询操作可以改为非递归,减少函数调用开销。

6.6 zhuxi树的典型错误与调试

错误1:数组越界。zhuxi树节点数估算不足。对于 \(n = 2 \times 10^5\),插入操作产生 \(n \log n \approx 3.6 \times 10^6\) 个节点,建议开 \(n \times 32\)

错误2:离散化后忘记用 \(b\) 数组映射回原值。查询返回的是离散化后的下标,输出时需要 b[ans]

错误3:树上zhuxi树中LCA查询错误。注意 up[lc][0]\(lc\) 的父节点,如果 \(lc\) 是根节点,则 up[lc][0] = 0,需要保证 root[0] 是一棵空树。

错误4:带修zhuxi树中 tmp1tmp2 数组大小。树状数组每次查询会涉及 \(O(\log n)\) 棵线段树,数组大小要开到 \(O(\log n)\) 以上。


第七章 总结与学习路线

7.1 学习路线

  1. 入门:静态区间第 \(k\) 小(P3834)
  2. 进阶:可持久化数组(P3919)
  3. 树上应用:树上路径第 \(k\) 小(P2633 / BZOJ2588)
  4. 带修改:动态区间第 \(k\) 小(P2617)
  5. 可持久化Trie:最大异或和(P4735)
  6. 复杂应用:二维数点、区间合并(CF484E)
  7. 高级:可持久化并查集(P3402)

7.2 核心公式速查

场景 公式
静态区间第k小 \(cnt = sum[ls_r] - sum[ls_{l-1}]\)
树上路径第k小 \(cnt = sum[ls_u] + sum[ls_v] - sum[ls_{lca}] - sum[ls_{fa[lca]}]\)
带修zhuxi树 \(cnt = \sum sum[ls_{tmp1}] - \sum sum[ls_{tmp2}]\)
区间合并lmx \(lmx = lmx_L + (mx_L == len_L ? lmx_R : 0)\)
区间合并mx \(mx = \max(mx_L, mx_R, rmx_L + lmx_R)\)

7.3 复杂度总表

操作 静态zhuxi树 带修zhuxi树 树上zhuxi树
建树 \(O(n \log n)\) \(O(n \log^2 n)\) \(O(n \log n)\)
修改 不支持 \(O(\log^2 n)\) 不支持
查询 \(O(\log n)\) \(O(\log^2 n)\) \(O(\log n)\)
空间 \(O(n \log n)\) \(O(n \log^2 n)\) \(O(n \log n)\)

zhuxi树是可持久化数据结构中最重要、最常用的成员之一。掌握它不仅能解决区间第 \(k\) 小问题,还能推广到树上路径、二维数点、带修改查询等多种场景。建议从模板题开始,逐步深入,理解“路径复制”和“前缀和差分”这两个核心思想,就能举一反三,应对各种变形。

posted @ 2026-09-19 18:43  shenchen2011  阅读(0)  评论(0)    收藏  举报