量产数据结构
概念:给一个序列,用树维护信息。
- 偏序类
- 树分治类
- 小Z的袜子类
一. 偏序类
1.偏序的概念
设 A 是一个非空集,P 是 A 上的一个关系,
若关系P是自反的、反对称的、和传递的,则称P是集合A上的偏序关系。
即P适合下列条件:
- (1)对任意的 a∈A , (a,a)∈P ;
- (2)若(a,b)∈P 且(b,a)∈P ,则 a=b ;
- (3)若(a,b)∈P , (b,c)∈P ,则(a,c)∈P ,
-
则称 P 是 A 上的一个偏序关系。
带偏序关系的 集合 A 称为 偏序集或半序集 。
若P是A上的一个偏序关系,我们用 a≤b 来表示(a,b)∈P。
举如下例子说明偏序关系:
1、实数集上的小于等于关系是一个偏序关系。
2、设 S 是集合,P(S)是 S 的所有子集构成的集合,
定义 P(S)中两个元素 A≤B 当且仅当 A 是 B 的子集,
即 A 包含于 B,则P(S)在这个关系下成为偏序集。
3、设 N 是正整数集,定义 m ≤ n 当且仅当m能整除n,不难验证这是一个偏序关系。
注意它不同于N上的自然序关系。 偏序是在集合 P 上的二元关系(≤),
它是自反的、反对称的、和传递的,就是说,对于所有 P 中的 a, b 和 c,有着:
a ≤ a (自反性); a ≤ b 且 b ≤ a 则 a = b (反对称性); a ≤ b 且 b ≤ c 则 a ≤ c (传递性)。
【定义简单化】
所谓偏序就是当你知道元素A,B,C时,元素A<B,且A<C,
但是B和C之间的关系却无法比较的现象。在ACM中,
此类的题目通常会告诉我们n个数组,每个元素的各属性对应于不同数组的同一位置的值 。
询问通常是回答比这元素小的有几个,或者是LIS这样的dp问题。
( LIS:最长上升(递增)子序列。)
通常有以下方法:排序,数据结构(树状数组,线段树,平衡树),cdq分治,分块。
2.偏序类---量产数据结构
即每次对满足多维(某维)的一个限制的所有数进行操作。
多维(某维)的限制:每个点 i 有 ai,bi,ci...不同的值。
操作具体:每次对每个值满足的某区间进行一次修改操作。
例如,对满足 l1<=ai<=r1 , l2<=bi<=r2 ...的 i 进行一次修改操作。
3.具体实现和维护
(1) Range Tree
范围树:狭义上的树套树.
- 能在O( logn^d )的复杂度内进行一次d维偏序的空间查询
- 能在O( logn^d )的复杂度内进行一次d维偏序的单点修改
- 空间为O( nlogn^(d-1) ),可以优化到O( n(logn/loglogn)^(d-1) )
如果要维护d维,出于方便,设每维的值大小是v的一个偏序。
*高维树状数组*
本质就是树状数组的嵌套。时间复杂度O( logv^d ),空间复杂度O( v^d )。
可以预先高维离散化来优化。时间复杂度O( logv^d ),空间复杂度O( nlogv^d )。
//普通树状数组写法 inline void add(int x,int y){ //元素修改(增加) for(int i=x;i<=n;i+=lowbit(i)) t[i]+=y; }
//二维树状数组+偏序写法 inline void add(int x,int y,int z){ //空间修改(增加) for(int i=x;i<=n;i+=lowbit(i)) for(int j=y;j<=n;j+=lowbit(j)) t[i][j]+=z; }
其实高维树状数组一般只有二维。
二维离散化含义及用法:先访问一遍,选择离散化(二维),用类似hash筛选有用的点。
*树状数组套树*
优势:好写,常数。
劣势:只能维护支持减法的信息,如果不能满足减法则基本上不能使用。
1)套平衡树:时间复杂度O( logn^2 ),空间复杂度O( nlogn )。
优势:空间。
劣势:平衡树没有简单的可以在多个平衡树上二分的方法,
区间kth这种询问会多一个log(其实可以不多log的)。
2)套线段树:时间复杂度O( lognlogv ),空间复杂度O( nlognlogv )。
优势:可以简单地在多个线段树上二分。 劣势:空间。
*线段树套树*
套平衡树:时间复杂度O( logn^2 ),空间复杂度O( nlogn )。
套Trie:时间复杂度O( lognlogv ),空间复杂度O( nlognlogv )。
优点:可以维护不支持减法的信息。
缺点:相对难写,慢,空间大。
线段树原理 查询一个区间,通过分治把它分成了若干个区间,
让区间维护自己的信息。只需要支持信息能高调合并。
线段树模板函数 insert是插入函数,erase是删除函数。
find是查询区间和,rank是查找小于x的数的个数。
普通线段树(modify函数)
void Modify(int x,int y,int pos,long long val){ int mid=x+y>>1; if(x==y){ this->val+=val; return ; } if(pos<=mid) ls->Modify(x,mid,pos,val); else rs->Modify(mid+1,y,pos,val); this->val=GCD(ls->val,rs->val); }
线段树套平衡树(要写之前的线段树模板,运用wblt)
先构造出线段树,每个线段树除了记录左边界和右边界之外,
还储存了一棵平衡树(当然实际上只需要储存根节点),对应着这一个区间的所有数。
每次修改区间L~R时,需要将所有包含L~R的线段树节点的平衡树都修正。
操作其实没有什么难点,和普通线段树一样分块处理即可,效率为O(log2(n))。
但是查询区间第k大不能像普通线段树一样,必须用二分枚举答案mid,
然后查询mid的排名,如果排名<=k就增大mid,否则减小mid。
假设二分跨度为t,则效率为O(log2(n)∗log2(t))。
ps:树套树常数较大,请谨慎使用。
模板:以BZOJ3196为例,这里使用的是线段树套Treap(二叉搜索树)。
#include<cstdio> #include<cstdlib> #include<algorithm> using namespace std; const int maxn=50000,maxt=1700000,MAXINT=((1<<30)-1)*2+1; int n,te,a[maxn+5]; //============================================================ struct Node{ Node *son[2]; int val,fix,si,w; int cmp(int k) {if (k==val) return -1;if (k<val) return 0; else return 1;} void Pushup() {si=son[0]->si+w+son[1]->si;} }; typedef Node* P_node; Node tem[maxt+5]; P_node null=tem,len=null; P_node newNode(int k){ len++;len->son[0]=len->son[1]=null; len->si=len->w=1;len->val=k;len->fix=rand(); return len; } void Rotate(P_node &p,int d){ P_node t=p->son[d^1];p->son[d^1]=t->son[d];t->son[d]=p; p->Pushup();t->Pushup();p=t; } void Insert(P_node &p,int k){ if (p==null) {p=newNode(k);return;} int d=p->cmp(k); if (d==-1) p->w++; else{ Insert(p->son[d],k); if (p->son[d]->fix>p->fix) Rotate(p,d^1); } p->Pushup(); } void Delete(P_node &p,int k){ if (p==null) return; int d=p->cmp(k); if (d==-1){ if (p->w>1) p->w--; else if (p->son[0]==null) p=p->son[1]; else if (p->son[1]==null) p=p->son[0]; else{ int d;if (p->son[0]->fix>p->son[1]->fix) d=0; else d=1; Rotate(p,d);if (p==null) return;Delete(p->son[d],k); } if (p==null) return; } else Delete(p->son[d],k); p->Pushup(); } int getrank(P_node p,int k){ //对于不存在的k,排名是k后继的排名 if (p==null) return 1; int d=p->cmp(k); if (d==-1) return p->son[0]->si+1; else if (d==0) return getrank(p->son[0],k); else return getrank(p->son[1],k)+p->son[0]->si+p->w; } int getpre(P_node p,int k){ if (p==null) return -MAXINT; int d=p->cmp(k); if (d==1) return max(getpre(p->son[1],k),p->val); else return getpre(p->son[0],k); } int getsuf(P_node p,int k){ if (p==null) return MAXINT; int d=p->cmp(k); if (d==0) return min(getsuf(p->son[0],k),p->val); else return getsuf(p->son[1],k); } //以上为Treap //============================================================ struct SegmentTree{ int l[4*maxn+5],r[4*maxn+5];P_node ro[4*maxn+5]; //只保留根指针 void Build(int p,int L,int R){ l[p]=L;r[p]=R;ro[p]=null; if (L==R) return; int mid=L+(R-L>>1); Build(p<<1,L,mid);Build(p<<1|1,mid+1,R); } void Seg_Insert(int p,int L,int k){ if (L<l[p]||r[p]<L) return; if (l[p]<=L&&L<=r[p]) Insert(ro[p],k); //这里不是L<=l[p]&&r[p]<=L,因为所有包含L的节点都要插入k if (l[p]==r[p]) return; Seg_Insert(p<<1,L,k);Seg_Insert(p<<1|1,L,k); } void Seg_Delete(int p,int L,int k){ if (L<l[p]||r[p]<L) return; if (l[p]<=L&&L<=r[p]) Delete(ro[p],k); //同理 if (l[p]==r[p]) return; Seg_Delete(p<<1,L,k);Seg_Delete(p<<1|1,L,k); } int Seg_rank(int p,int L,int R,int k) //这个函数返回真正的答案-1,防止重复 { if (R<l[p]||r[p]<L) return 0; if (L<=l[p]&&r[p]<=R) return getrank(ro[p],k)-1; //这里是普通线段树查询,所以是L<=l[p]&&r[p]<=R return Seg_rank(p<<1,L,R,k)+Seg_rank(p<<1|1,L,R,k); } int Seg_kth(int l,int r,int k){ int L=0,R=1e8; while (L<=R) //二分 { int mid=L+(R-L>>1),rk=Seg_rank(1,l,r,mid)+1; if (rk<=k) L=mid+1; else R=mid-1; } return R; } int Seg_pre(int p,int L,int R,int k){ if (R<l[p]||r[p]<L) return -MAXINT; if (L<=l[p]&&r[p]<=R) return getpre(ro[p],k); return max(Seg_pre(p<<1,L,R,k),Seg_pre(p<<1|1,L,R,k)); } int Seg_suf(int p,int L,int R,int k){ if (R<l[p]||r[p]<L) return MAXINT; if (L<=l[p]&&r[p]<=R) return getsuf(ro[p],k); return min(Seg_suf(p<<1,L,R,k),Seg_suf(p<<1|1,L,R,k)); } }; SegmentTree tr; //以上为线段树 //============================================================ bool Eoln(char ch) {return ch==10||ch==13||ch==EOF;} int readi(int &x){ int tot=0,f=1;char ch=getchar(),lst=' '; while ('9'<ch||ch<'0') {if (ch==EOF) return EOF;lst=ch;ch=getchar();} if (lst=='-') f=-f; while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=getchar(); x=tot*f; return Eoln(ch); } void LNR(P_node ro){ if (ro==null) return; LNR(ro->son[0]);for (int i=1;i<=ro->w;i++) printf("%d ",ro->val);LNR(ro->son[1]); } int main(){ freopen("STBST.in","r",stdin); freopen("STBST.out","w",stdout); readi(n);readi(te);tr.Build(1,1,n); for (int i=1;i<=n;i++) readi(a[i]),tr.Seg_Insert(1,i,a[i]); while (te--){ int td,x,y,z;readi(td);readi(x);readi(y); switch (td){ case 1:readi(z);printf("%d ",tr.Seg_rank(1,x,y,z)+1);break; case 2:readi(z);printf("%d ",tr.Seg_kth(x,y,z));break; case 3:tr.Seg_Delete(1,x,a[x]);tr.Seg_Insert(1,x,y);a[x]=y;break; case 4:readi(z);printf("%d ",tr.Seg_pre(1,x,y,z));break; case 5:readi(z);printf("%d ",tr.Seg_suf(1,x,y,z));break; } } return 0; }
*平衡树套树*
套平衡树:时间复杂度O( logn^2 ),空间复杂度O( nlogn )。
套Trie:时间复杂度O( lognlogv ),空间复杂度O( nlognlogv )。
优点:可以在线支持第一维插入的问题。
缺点:难写,更慢,常数更大。
*树套OVT*
套一个排序后的vector,OVT == Ordered Vector Tree。
优点:好写,空间。缺点:复杂度。
【例题】Luogu3380 二逼平衡树
题意
您需要写一种数据结构(可参考题目标题)来维护一个有序数列,
其中需要提供以下操作:1.查询k在区间内的排名。
2.查询区间内排名为k的值。 3.修改某一位值上的数值。
4.查询k在区间内的前驱。5.查询k在区间内的后继。n,m<=5e4
Solution1
题目翻译:发现本质就是个带单点修改的区间kth,区间rank。
可以用线段树套平衡树维护,时间O( logn^3 )。
(kth:区间第k大的数;可以用rank函数求出。)
-->维护不支持差分,不支持修改。
-->二分答案:check函数 查找判断区间中mid的数值是否小于等于k。
Solution2
发现区间kth可以在多个Trie上一起二分来维护。
( 0/1trie + 线段树 = 权值线段树 )
可以用线段树套Trie维护,时间O( logn^2 )。
-->找两个前缀来表示区间,在这两个前缀的trie上二分。
Solution3
发现区间kth可以支持减法,可以用树状数组套Trie维护。
时间O( logn^2 ),很好写,常数也小。
Solution4(特殊数据结构&&只针对一个问题)
通过读论文可以发现可以用动态划分树维护。
时间O( (logn/loglogn)^2 )。
——时间划过风的轨迹,那个少年,还在等你。