**格式**:英文/公式两边接汉字注意空格,有些地方未使用公式,如 $k$,$[l,r]$。 **内容**:写得很棒。 =======概述======= 树套树,就是在一个树型数据结构上,每个点不再是一个节点,而是另外一个树形数据结构。 经常应用在一些普通的数据结构外套上区间操作或者动态操作时候。没有固定的套路,根据题目来选不同的树型数据结构组合。 下面介绍一些常用的树套树 ===== 树状数组套线段树 ===== ==== 例题 [CQOI2011]动态逆序对 ==== 现在给出 $1∼n $ 的一个排列,按照某种顺序依次删除 $m$ 个元素,你的任务是在每次删除一个元素**之前**统计整个序列的逆序对数。 ​ 我们可以建立树状数组,第 $i$ 位维护 $a[i-lowbit(i)+1] ∼ a[i]$ 的权值线段树。插入和普通的求逆序对方法相同,删除 $x$ 的时候查询位置在后面比 $x$ 大的数有多少即可。 为了防止内存爆找,线段树采用动态开点。 #include #include #include #include #include #define ll long long using namespace std; inline int read() { int k=0,f=1;char c=getchar(); while(!isdigit(c)) {if(c=='-') f=-1;c=getchar();} while(isdigit(c)) k=k*10+c-'0',c=getchar();return f*k; } const int N=100055; int root[N],lch[N*91],rch[N*91],sum[N*91],cnt; int n,m,pos[N]; ll anss; void add(int &k,int l,int r,int x) { if(!k) k=++cnt;sum[k]++; if(l==r) return ; int mid=l+r>>1; if(x<=mid) add(lch[k],l,mid,x); else add(rch[k],mid+1,r,x); } void del(int k,int l,int r,int x) { if(!k) return ; sum[k]--; if(l==r) return ; int mid=l+r>>1; if(x<=mid) del(lch[k],l,mid,x); else del(rch[k],mid+1,r,x); } int query(int k,int l,int r,int a,int b) { if(!k) return 0; if(a<=l&&b>=r) return sum[k]; int mid=l+r>>1,ans=0; if(a<=mid) ans+=query(lch[k],l,mid,a,b); if(b>mid) ans+=query(rch[k],mid+1,r,a,b); return ans; } int main() { n=read();m=read(); for(int i=1;i<=n;i++) { int a=read();pos[a]=i; for(int j=i;j<=n;j+=j&-j) add(root[j],1,n,a); for(int j=i;j;j-=j&-j) anss+=query(root[j],1,n,a+1,n); } for(int i=1;i<=m;i++) { printf("%lld\n",anss); int a=read(); if(a!=n) { for(int j=pos[a];j;j-=j&-j) anss-=query(root[j],1,n,a+1,n); } if(a!=1) { for(int j=n;j;j-=j&-j) anss-=query(root[j],1,n,1,a-1); for(int j=pos[a]-1;j;j-=j&-j) anss+=query(root[j],1,n,1,a-1); } for(int j=pos[a];j<=n;j+=j&-j) del(root[j],1,n,a); } return 0; } ===== 线段树套平衡树 ===== ==== 例题 洛谷P3380 二逼平衡树 ==== 让你维护一个有序数列,有以下操作: 1.查询 $k$ 在区间内的排名 2.查询区间内排名为 $k$ 的值 3.修改某一位值上的数值 4.查询 $k$ 在区间内的前驱 5.查询 $k$ 在区间内的后继 就是平衡时问题加上了区间限制。我们外层建线段树,每一个节点维护该节点包含区间的平衡树。 操作一和操作三对线段树上包含区间的节点全部进行操作。 操作四,五,外层线段树区间查询,对每个包含 $[l,r]$ 的节点得到的前驱/后继求一个最大值/最小值即可。 操作二 我们二分答案,和操作一类似,利用小于某数的个数进行二分。复杂度多一个 $log$ 。 #include #include #include #include #include using namespace std; inline int read() { int k=0,f=1;char c=getchar(); while(!isdigit(c)) {if(c=='-') f=-1;c=getchar();} while(isdigit(c)) k=k*10+c-'0',c=getchar();return f*k; } const int N=100005,inf=2147483647; struct T { int ch[2],fa,size,cnt,v; }tr[N*200]; int tot,rt[N*20],n,m,a[N]; #define l(x) tr[x].ch[0] #define r(x) tr[x].ch[1] void pu(int x) { tr[x].size=tr[l(x)].size+tr[r(x)].size+tr[x].cnt; } void rotate(int x) { int y=tr[x].fa,z=tr[y].fa,k=tr[y].ch[1]==x; tr[z].ch[tr[z].ch[1]==y]=x;tr[x].fa=z; tr[y].ch[k]=tr[x].ch[k^1];tr[tr[x].ch[k^1]].fa=y; tr[y].fa=x;tr[x].ch[k^1]=y; pu(y);pu(x); } void splay(int x,int r,int pos) { while(tr[x].fa!=pos) { int y=tr[x].fa,z=tr[y].fa; if(z!=pos) (l(z)==y)^(l(y)==x)?rotate(x):rotate(y); rotate(x); } if(!pos) rt[r]=x; } void ins(int x,int r) { int u=rt[r],f=0; if(!u) { tr[++tot].v=x;tr[tot].size=tr[tot].cnt=1;rt[r]=tot;return ; } while(u&&tr[u].v!=x) f=u,u=tr[u].ch[tr[u].v1) tr[u].size--,tr[u].cnt--; else if(!tr[u].ch[0]) rt[r]=r(u),tr[r(u)].fa=0; else if(!tr[u].ch[1]) rt[r]=l(u),tr[l(u)].fa=0; else { splay(find_max(l(u)),rt[r],u); tr[tr[u].ch[0]].ch[1]=tr[u].ch[1]; tr[r(u)].fa=l(u);tr[l(u)].fa=0; rt[r]=l(u);pu(l(u)); } ins(y,r); } int ran(int x,int r) { int u=rt[r],ans=0; while(u) { if(tr[u].v>x) u=l(u); else if(tr[u].v>1; build(lson);build(rson); } int sol1(int k,int l,int r,int a,int b,int x) { if(a<=l&&b>=r) { return ran(x,k); } int mid=l+r>>1,ans=0; if(a<=mid) ans+=sol1(lson,a,b,x); if(b>mid) ans+=sol1(rson,a,b,x); return ans; } int sol2(int l,int r,int k) { int L=0,R=1e8; while(R>L) { int mid=L+R>>1; if(sol1(1,1,n,l,r,mid)>1; if(c<=mid) sol3(lson,c,x); else sol3(rson,c,x); } int sol4(int k,int l,int r,int a,int b,int x) { if(a<=l&&b>=r) return lxt(x,k); int mid=l+r>>1,ans=-inf; if(a<=mid) ans=max(ans,sol4(lson,a,b,x)); if(b>mid) ans=max(ans,sol4(rson,a,b,x)); return ans; } int sol5(int k,int l,int r,int a,int b,int x) { if(a<=l&&b>=r) { return rxt(x,k); } int mid=l+r>>1,ans=inf; if(a<=mid) ans=min(ans,sol5(lson,a,b,x)); if(b>mid) ans=min(ans,sol5(rson,a,b,x)); return ans; } int main() { int opt,b,c,d; n=read();m=read(); for(int i=1;i<=n;i++) a[i]=read(); build(1,1,n); for(int i=1;i<=m;i++) { opt=read();b=read();c=read();if(opt!=3) d=read(); if(opt==1) printf("%d\n",sol1(1,1,n,b,c,d)+1); else if(opt==2) printf("%d\n",sol2(b,c,d)); else if(opt==3) sol3(1,1,n,b,c); else if(opt==4) printf("%d\n",sol4(1,1,n,b,c,d)); else printf("%d\n",sol5(1,1,n,b,c,d)); } return 0; } ===== 线段树套线段树 ===== ==== 例题 P3332 [ZJOI2013]K大数查询 ==== === 题目大意 === 有$N$个位置,$M$个操作,操作有2种。 * 操作1:''%%1 a b c%%'' 在第 $a$ 个位置到第 $b$个位置,每个位置加入一个数$c$ * 操作2:''%%2 a b c%%'' 询问从第 $a$ 个位置到第 $b$ 个位置中第 $c$ 大的数。 $n,m\leq5*10^4,|c|\leq n$ === 题解 === 权值线段树套位置线段树。其中第一维记录权值,第二维记录位置。对于第一维线段树上的一个节点维护的权值区间 $[L, R]$ ,它所指向的那颗线段树中记录的是每个位置包含了几个值在 $[L, R]$ 范围内的数。 之所以这题不能像上文一样用线段树套平衡树(外层记录位置,内层维护权值)的原因是:对于套在外面那一层的线段树是很难进行区间操作的,所以这题就需要换一个思路,把位置信息放在内层的线段树中。 PS:以下代码用到了一个小技巧,就是线段树标记永久化,相比与正常线段树的pushdown,我们在路过该节点的时候把修改对答案的影响加上,这样能优化不少常数,否则这题你很难卡过去 #include using namespace std; #define mem(a,b) memset(a,b,sizeof(a)) typedef long long LL; typedef pair PII; #define X first #define Y second inline int read() { int x=0,f=1;char c=getchar(); while(!isdigit(c)){if(c=='-')f=-1;c=getchar();} while(isdigit(c)){x=x*10+c-'0';c=getchar();} return x*f; } const int maxn=50010,maxnode=20000005; int T,tp,n,tot,lc[maxnode],rc[maxnode],ql,qr,v,rt[maxn<<3];; LL addv[maxnode],sumv[maxnode]; void add(int& o,int L,int R) { if(!o)o=++tot; if(ql<=L && R<=qr){sumv[o]+=(R-L+1);addv[o]++;return;} int mid=L+R>>1; if(ql<=mid)add(lc[o],L,mid); if(qr>mid)add(rc[o],mid+1,R); sumv[o]=sumv[lc[o]]+sumv[rc[o]]+addv[o]*(R-L+1); return; } LL query(int o,int L,int R,LL Add) { if(!o) { int l=max(L,ql),r= min(R,qr); return Add*(r-l+1); } if(ql<=L && R<=qr) return sumv[o]+Add*(R-L+1); int mid=L+R>>1; LL ans=0; if(ql<=mid)ans+=query(lc[o],L,mid,Add+addv[o]); if(qr>mid)ans+=query(rc[o],mid+1,R,Add+addv[o]); return ans; } void update() { int o=1,L=1,R=n<<1|1; while(L>1,lo=o<<1,ro=lo|1; if(v<=mid)R=mid,o=lo; else L=mid+1,o=ro; } return add(rt[o],1,n); } int query() { int o=1,L=1,R=n<<1|1; while(L>1,lo=o<<1,ro=lo|1; LL res=query(rt[ro],1,n,0); if(v<=res)L=mid+1,o=ro; else R=mid,o=lo,v-=res; } return L; } int main() { n=read();T=read(); while(T--) { tp=read();ql=read();qr=read();v=read(); if(tp==1)v+=n+1,update(); else if(tp==2)printf("%d\n",query()-n-1); } return 0; } ===== 树状数组套可持久化线段树 ===== 话不多说,上题 ==== 题目 ==== 给定一个含有n个数的序列$a[1],a[2],a[3]……a[n]$,程序必须回答这样的询问:对于给定的$i,j,k$,在$a[i],a[i+1],a[i+2]……a[j]$中第$k$小的数是多少$(1≤k≤j-i+1)$,并且,你可以改变一些$a[i]$的值,改变后,程序还能针对改变后的$a$继续回答上面的问题。 ==== 题解 ==== 题目询问的是区间第 $k$ 小 如果不考虑区间,求第 $k$ 小的话,用线段维护权值数量即可。求第 $k$ 大只需在线段树上走,如果左区间权值和 $L_v \geq k$ ,就往左走,否则往右走,直到一个叶子节点,即为答案。 现在考虑区间,我们可以将线段树可持久化,就可以支持作差处理,如果要求一段区间 $[l,r]$ 的第 $k$ 小,我们只需在 $r$ 处的线段树和 $l - 1$ 处线段树作差后求出第 $k$ 大。 如果还要支持单点修改,我们线段树的建立就不能单纯的在区间的基础上。如果仍然如此建树,则每次修改区间都要修改该点之后的线段树,数目是 $O(n)$ 的。这个时候我们可以在树状数组的每个点上建树。如此,每个点有关的线段树只有 $O(logn)$ 总复杂度降到 $O(nlog^2n)$ #include #include #include #include #define REP(i,n) for (int i = 1; i <= (n); i++) #define lbt(x) (x & -x) using namespace std; const int maxn = 10005,maxm = 10000005,INF = 1000000000; inline int read(){ int out = 0,flag = 1; char c = getchar(); while (c < 48 || c > 57) {if (c == '-') flag = -1; c = getchar();} while (c >= 48 && c <= 57) {out = (out << 3) + (out << 1) + c - '0'; c = getchar();} return out * flag; } int rt[maxn],A[maxn],B[2 * maxn],n,m,tot = 1,siz,N; int sum[maxm],ls[maxm],rs[maxm],a[2][20]; struct Que{int opt,l,r,k;}Q[maxn]; int getn(int x){ int l = 1,r = tot,mid; while (l <= r){ mid = l + r >> 1; if (B[mid] < x) l = mid + 1; else r = mid - 1; } return l; } void update(int& u,int l,int r,int pos,int v){ if (!u) u = ++siz; sum[u] += v; if (l == r) return; int mid = l + r >> 1; if (mid >= pos) update(ls[u],l,mid,pos,v); else update(rs[u],mid + 1,r,pos,v); } int query(int l,int r,int k){ if (l == r) return l; int mid = l + r >> 1,t = 0; for (int i = 1; i <= a[0][0]; i++) t += sum[ls[a[0][i]]]; for (int i = 1; i <= a[1][0]; i++) t -= sum[ls[a[1][i]]]; if (t >= k){ for (int i = 1; i <= a[0][0]; i++) a[0][i] = ls[a[0][i]]; for (int i = 1; i <= a[1][0]; i++) a[1][i] = ls[a[1][i]]; return query(l,mid,k); }else { for (int i = 1; i <= a[0][0]; i++) a[0][i] = rs[a[0][i]]; for (int i = 1; i <= a[1][0]; i++) a[1][i] = rs[a[1][i]]; return query(mid + 1,r,k - t); } } void add(int u,int x,int v){while (u <= n) update(rt[u],1,tot,x,v),u += lbt(u);} int solve(int l,int r,int k){ a[0][0] = a[1][0] = 0; for (int i = r; i; i -= lbt(i)) a[0][++a[0][0]] = rt[i]; for (int i = l - 1; i; i -= lbt(i)) a[1][++a[1][0]] = rt[i]; return query(1,tot,k); } int main(){ n = read(); m = read(); char c; REP(i,n) A[i] = B[++N] = read(); REP(i,m){ c = getchar(); while (c != 'Q' && c != 'C') c = getchar(); if (c == 'Q') Q[i].opt = 0,Q[i].l = read(),Q[i].r = read(),Q[i].k = read(); else Q[i].opt = 1,Q[i].l = read(),Q[i].k = B[++N] = read(); } sort(B + 1,B + 1 + N); for (int i = 2; i <= N; i++) if (B[i] != B[tot]) B[++tot] = B[i]; REP(i,n) A[i] = getn(A[i]),add(i,A[i],1); REP(i,m){ if (!Q[i].opt) printf("%d\n",B[solve(Q[i].l,Q[i].r,Q[i].k)]); else{ Q[i].k = getn(Q[i].k); add(Q[i].l,A[Q[i].l],-1); A[Q[i].l] = Q[i].k; add(Q[i].l,A[Q[i].l],1); } } return 0; } 以上就是树状数组套可持久化线段树的应用,通常用于一个二维空间的整体查询单点修改的操作。 再贴一道板题 [[https://www.luogu.com.cn/problem/P3759|[TJOI2017]不勤劳的图书管理员]]