【暖*墟】 #洛谷省选网课# 7.30量产数据结构

量产数据结构

概念:给一个序列,用树维护信息。

  • 偏序类
  • 树分治类
  • 小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\n",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\n",tr.Seg_rank(1,x,y,z)+1);break;
            case 2:readi(z);printf("%d\n",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\n",tr.Seg_pre(1,x,y,z));break;
            case 5:readi(z);printf("%d\n",tr.Seg_suf(1,x,y,z));break;
        }
    }
    return 0;
}
View Code

*平衡树套树*

套平衡树:时间复杂度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 )。

 

 

 

 

 

 

 

 

 

 

                                                 ——时间划过风的轨迹,那个少年,还在等你。

posted @ 2018-07-30 16:09  花神&缘浅flora  阅读(349)  评论(0编辑  收藏  举报