問題の間隔でスプレイの応用

(私は無知、不完全な場合も私に知らせてください)

二分探索木の話から、1

まず、バイナリ検索ツリーは、以下の特性を満たすバイナリツリーです

  1. 各バイナリツリーノードは、重み値有する\(V \)を

  2. それはサブツリーを放置した場合、各ノードのバイナリ検索ツリーについては、その左部分木の各ノードの重みは、ノードの重みは、それ自体の値よりも小さいです

  3. それは右の部分木である場合、二分探索ツリー内の各ノードに対して、このノードの右側のサブツリーの各ノードの重みは、自重の値よりも大きいです

この図は、二分探索木(OI画家のおかげEternalAlexander)であります

図5は、左の部分木の接合部の重量である1,2,3,4重量、5未満である
6,7の重量と右サブツリー、5はより大きかった
この美しいを満たす他のノードについてプロパティ

誰も二分探索木の理解を深めるために、我々は、トピックを見て:

反復配列要素のないメンテナンス期間は、以下の2つの操作をサポートしています
。1 \(挿入(X)\)を挿入要素\(X \)
2 $のGet(X)$出力\(X \)の小さな数字を

アクション1については、我々は見つける必要がある\(X \) 適切な挿入位置を、我々が今したいとします(X \)\するために挿入された(私は\)\(自身を含む)のサブツリーのルートがあり、そこになります2例:

1. \(X <ツリー[I] .Valueの\) 説明\(X \)位置\を(私は\)サブツリー左
2. (ツリー[I] .Valueの\ \ X)>、説明を\(X \)位置\(私は\)右部分木

ルートノードから空のノードを再帰的に挿入することができるまで、そのようなアクセスが挿入されています

まあ、今はそれを行うにはどのように操作1、操作2を解決する必要がありますか?

各ノードのために、我々は維持\(サイズ\)で表される、\を(私は\) を含むサブツリーのルートである\(I \)サイズ)が

私たちが見ているということになりましたと\(私は\)をルートとするサブツリーの\(K \)よりサブツリーツリーを知るのは簡単、小さな値を(私は\)\小さな設定[I] \(サイズ[ツリー.leftson] \)ヶ月なので、

  1. もし\(K <=サイズ[ツリー[I] .leftson] \) 再帰検索、左部分木に
  2. もし\(K =サイズ[ツリー[I] .leftson]さんが+1 \) その後、\(私は\)私たちが見ている接合ノード
  3. そうでない場合には、再帰的に右のサブツリー検索に、この時間\(k個\)が引くべきです\(サイズ[ツリー[i]は .leftson] +1 \)

受け入れることは非常に困難になってきた、それは削除操作に来るときしかし、通常の二分探索木の問題は非常に限られた解決することができ、木は悪い状況チェーンに退化も発生する可能性があり(増加数を挿入する想像)単一の操作の最悪の複雑さが達することが\(O(n)は、\)

いわゆる平衡ツリーは、実際には、ツリーの元の形式はによって、アンバランスのバランスを取ることを試みることができる(\ \回転)、このような優れた到達するツリーの深さなどの操作\を(\ N-ログ)、および\(スプレイ\)は、バランスのとれたツリーに属し1で

また、学生はスプレイの重量はdalaoの説明を見て下さいません〜

2.基本的な間隔スプレイの性質

まず、二分探索木は、私たちアナロジーと重みの前に\(スプレイ\)文字、ツリーのように構成することができます

  1. ツリー内の各ノードに対して、それが前方ノードよりも元の配列におけるその左サブツリーの各ノードの位置次に、サブツリーを残している場合

  2. ツリー内の各ノードに対して、それが右の部分木である場合、リスト上のこのノードよりも元の配列におけるその右側のサブツリーの各ノードの、位置

例えば、我々は\({1,5,4,2,3} \)を確立間隔\(スプレイ\)

1. 1

こうして2の右の子に挿入5,5の比で2位以降2. 5、

3.ツリーのバランスを維持するために、我々は5になります\(スプレイ\)ルートへ

4. 5の後に4,4-位置を挿入し

、前記同様。4 \(スプレイ\)をルートに

最后这棵树可能是这样的(为了好看我就先提前\(splay\)了)

现在我们会发现一个重要的性质:对这棵splay进行中序遍历后得到的恰好是原序列

那么,如果我们把元素在原序列中的位置当做权值,区间splay其实就是一棵每个点的权值都可能动态变化的权值splay,至于为什么权值会变化,是因为这个数在序列中的位置可能发生了变化(比如翻转)

区间splay中,每个结点在储存它本身的信息外,还储存着它子树这段连续区间的信息,比如说上图中标号为1的结点可能除了存自己的value,size以外还存着1,5的value之和,4结点可能存着所有结点value的总和,通过这种信息合并可以像线段树一般大大缩短查询的复杂度

你可以这样认为:区间splay中每个结点代表的不只是它本身,还可以代表它的整颗子树

下面开始讲解一些具体操作:

Kth

区间splay中查找目前序列中第\(k\)位置上的数和二叉搜索树查找第\(k\)小是一摸一样的,代码如下:

int Kth(int x)
{
    int u=root;
    while(1)
    {
        if(t[u].tag)pushdown(u);//这东西之后解释
        if(x<=t[t[u].ch[0]].siz)u=t[u].ch[0];//ch[0],ch[1]分别是左儿子和右儿子,t[x].siz表示以x为根的子树的大小
        else if(t[t[u].ch[0]].siz+1==x)return u;
        else x-=(t[t[u].ch[0]].siz+1),u=t[u].ch[1];
    }
}

Pushup

将子树信息向上合并的函数,具体代码如下(以维护子树和 sum为例)

inline void pushup(int x)
{
    int l=t[x].ch[0],r=t[x].ch[1];
    t[x].sum=t[l].sum+t[r].sum+t[x].v;//一个结点子树之和sum=左子树sum+右子树sum+本身的value
    t[x].siz=t[l].siz+t[r].siz+1;
}

Split

就像LCT中的Access操作,区间splay的核心操作split就是提取一段区间\([l,r]\),具体操作就是先将\(Kth(l-1)\)旋转到根,再把\(Kth(r+1)\)旋转到根的右儿子,那么由于区间splay的中序遍历为原序列,现在\(Kth(r+1)\)的左子树就正好是\([l,r]\)这段区间
代码如下:

int split(int l,int r)
{
    int x=Kth(l-1),y=Kth(r+1);
    splay(x,0),splay(y,x);
    return t[y].ch[0];
}//这样返回的就是代表一段区间[l,r]的结点编号

举个例子,假设还是刚才的\({1,5,4,2,3}\)这段序列,我们现在想要区间\([3,4]\)的和
1 把Kth(3-1)即5旋转到根

2 把Kth(4+1)即3旋转到根的右儿子

可以在上图中很明显的看出区间\([3,4]\)\(4,2\)这两个数就是现在3的左子树,既不少也不多,由于我们在每个结点存了子树和,所以3的左儿子的子树和\(sum\)就是我们要求的区间和

Insert

假设我们要将数k插入到第x个数后面,那么先\(split(Kth(x),Kth(x+1))\)后将x作为现在\(Kth(x+1)\)的左儿子就可以啦

Delete

\(Insert\)操作一样,只不过是将\(Kth(x+2)\)的左儿子删去

Pushdown

现在考虑如何区间修改?
首先,如果我们要将区间\([l,r]\)每个数加上\(k\),那么肯定先要\(split(l,r)\),将这段区间提取出来,之后怎么办?

打懒惰标记?那不是线段树吗?

答案是可以的,由于我们统计答案是自上而下的,所以可以在所有复合操作所共有的\(Kth\)操作中边访问下传标记,这样就能保证解的正确性

代码:(以区间统一赋值为k的标记为例)

inline void pushdown(int x)
{
    int l=t[x].ch[0],r=t[x].ch[1];
    if(t[x].tag)//是否有区间统一赋值的标记
    {
        t[x].tag=0;
        if(l)t[l].tag=1,t[l].v=t[x].v,t[l].sum=t[x].v*t[l].siz;//sum是维护的子树和
        if(r)t[r].tag=1,t[r].v=t[x].v,t[r].sum=t[x].v*t[r].siz;
    }
}

其实你会发现区间splay对于区间加,区间乘,区间赋值,区间Rmq问题的处理和线段树是一模一样的,大多数线段树都可以用区间splay代替,而区间splay能处理的问题会更多

区间splay的\(rotate,splay\)和权值splay并没有什么区别,这里不再赘述

下面看一道例题:

您需要写一种数据结构,来维护一个数列,其中需要提供以下操作:翻转一个区间,例如原序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1

首先构建区间splay,不难发现翻转一段区间\([l,r]\)就是将代表这个区间的结点的子树中每一对左右子树都交换

区间操作肯定会想到打标记啦,只要用一个tag表示区间翻转的懒惰标记就可以了,下传标记就是左右子树的标记^=1,然后交换左右子树就好了

细节:这样如果修改区间\([1,n]\)会出问题,所以加入两个哨兵结点\(1,n+2\)防止越界,那么我们的序列下标都+1就好了

这里是丑陋的代码~

const int maxn=100000+10;
struct node
{
    int ff,val;//父亲,权值
    int ch[2];//左右儿子
    int siz;//子树大小
    int tag;//翻转的懒惰标记
}t[maxn];
int tot,n,root,m;
inline void pushup(int x)//信息向上合并
{
    t[x].siz=t[t[x].ch[0]].siz+t[t[x].ch[1]].siz+1;
}
inline void pushdown(int x)//下传标记
{
    t[t[x].ch[0]].tag^=1,t[t[x].ch[1]].tag^=1;
    swap(t[x].ch[0],t[x].ch[1]);
    t[x].tag=0;
}
inline void rotate(int x)
{
    int y=t[x].ff,z=t[y].ff;
    int k=(t[y].ch[1]==x);
    t[z].ch[t[z].ch[1]==y]=x,t[x].ff=z;
    t[y].ch[k]=t[x].ch[k^1],t[t[x].ch[k^1]].ff=y;
    t[x].ch[k^1]=y,t[y].ff=x;
    pushup(y),pushup(x);
}
void splay(int x,int goal)
{
    while(t[x].ff!=goal)
    {
        int y=t[x].ff,z=t[y].ff;
        if(z!=goal)
            (t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
        rotate(x);
    }
    if(goal==0)root=x;
}
void insert(int x)//插入第x个数
{
    int u=root,ff=0;
    while(u)ff=u,u=t[u].ch[x>t[u].val];
    u=++tot;
    if(ff)t[ff].ch[x>t[ff].val]=x;
    t[u].val=x,t[u].siz=1,t[u].ff=ff;
    t[u].tag=0,t[u].ch[0]=t[u].ch[1]=0;
    splay(u,0);
}
int Kth(int x)//查询序列中第x个数在splay中的位置
{
    int u=root;
    while(1)
    {
        if(t[u].tag)pushdown(u);
        if(x<=t[t[u].ch[0]].siz)u=t[u].ch[0];
        else if(t[t[u].ch[0]].siz+1==x)return u;
        else x-=(t[t[u].ch[0]].siz+1),u=t[u].ch[1];
    }
}
void solve(int l,int r)
{
    l=Kth(l),r=Kth(r+2);//相当于split(l,r),由于有哨兵结点所以下标+1
    splay(l,0),splay(r,l);
    t[t[r].ch[0]].tag^=1;
}
void print(int x)
{
    if(!x)return;
    if(t[x].tag)pushdown(x);
    print(t[x].ch[0]);
    if(t[x].val>1&&t[x].val<n+2)printf("%d ",t[x].val-1);
    print(t[x].ch[1]);
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n+2;++i)insert(i);
    int l,r;
    for(int i=1;i<=m;++i)scanf("%d%d",&l,&r),solve(l,r);
    print(root);
}

3.一些习题

  1. P3391 【模板】文艺平衡树(Splay) 模板题QwQ
  2. SPOJ4487 GSS6带插入区间最大子段和,同线段树一样 \(pushup\) 区间信息即可
  3. P2042 [NOI2005]维护数列 操作比较多,且涉及到 \(splay\) 的线性构造

おすすめ

転載: www.cnblogs.com/study-ysj/p/splay.html