序列终结者[splay平衡树]

序列终结者

传送门

序章:

考试剩下1小时多,还有两题。

要不还是打这道splay吧。

剩下30分钟。过样例。

20分钟。自测出问题。

5分钟。解决了,这波稳了。

0分钟。寄, v v v为负,初值要设负无穷。

旁边打暴力的大佬:Ohhhhh!过了。

0分的我:

序列终结者[splay平衡树]_第1张图片

这篇题解多半出于自嘲巩固。

正片开始:

前置任务:

1.学会splay这种困难较简单的平衡树。

2.学会线段树困难的懒标记。

splay(平衡的二叉查找树的一种)的经典操作就是表示序列,在这种splay中我们要记住:这颗BST(二叉查找树)的每一个节点就是序列中的一个位置,节点保存的信息就是这个位置的信息,树的排序方法是每个节点在序列中的下表的大小关系。

那么我们由排序方法可以知道每一个节点的排名就是这个节点在序列中的位置。

区间旋转

进行典中典的区间旋转时,设旋转区间为 ( l , r ) (l,r) (l,r)

我们先将旋转的区间的前一个位置: l − 1 l-1 l1位置的节点通过splay操作旋转到根。此时,旋转的区间将在根的右子树中(旋转区间的位置肯定比旋转区间前一个位置大)。

再将旋转区间的后一个位置: r + 1 r+1 r+1位置的节点旋转到根的右儿子处,即使他的父亲为根。旋转前根是 l − 1 l-1 l1位置的节点,旋转后 r + 1 r+1 r+1位置的节点的父亲为根,那么 r + 1 r+1 r+1位置的节点肯定再根的右儿子处。

旋转区间 ( l , r ) (l,r) (l,r)的位置都比 l − 1 l-1 l1的位置大,比 r + 1 r+1 r+1的位置小。故其在 l − 1 l-1 l1位置的节点的右子树中, r + 1 r+1 r+1节点的左子树中。

序列终结者[splay平衡树]_第2张图片

现在我们将区间 l l l r r r中的节点左右互换,即每一个节点都将自己的左右儿子互换就可以完成区间旋转操作了。

十分严谨的证明:

设区间中的一个节点在此区间中的排名为 x x x(下用他为代称),即他在序列中的位置为 l + ( x − 1 ) l+(x-1) l+(x1),区间中轴线的位置为 l + r 2 \frac{l+r}{2} 2l+r(不取整)。

翻转操作即翻转前的区间和翻转后的区间关于中轴线对称。

序列终结者[splay平衡树]_第3张图片

到中轴线的距离:

∣ l + ( x − 1 ) − ( l + r ) 2 ∣ = ∣ 2 l + 2 x − 2 2 − l + r 2 ∣ = ∣ l + 2 x − 2 − r 2 ∣ |l+(x-1)-\frac{(l+r)}{2}|=|\frac{2l+2x-2}{2}-\frac{l+r}{2}|=|\frac{l+2x-2-r}{2}| l+(x1)2(l+r)=22l+2x22l+r=2l+2x2r

因为交换为左右儿子交换,所以交换过后此区间内每个比他大的节点都变成了比他小的节点,比他小的节点都变成了比他大的节点。(此处大小关系均为序列中的位置大小)

原序列中比他大的点的个数: r − l + 1 − x r-l+1-x rl+1x

现序列中比他小的点的个数: r − l + 1 − x r-l+1-x rl+1x

现序列中他的位置: l + ( r − l + 1 − x ) = r + 1 − x l+(r-l+1-x)=r+1-x l+(rl+1x)=r+1x

到中轴线的距离:
r + 1 − x − ( l + r ) 2 = ∣ r + 2 − 2 x − l 2 ∣ = ∣ − r + 2 − 2 x − l 2 ∣ = ∣ l + 2 x − 2 − r 2 ∣ r+1-x-\frac{(l+r)}{2}=|\frac{r+2-2x-l}{2}|=|-\frac{r+2-2x-l}{2}|=|\frac{l+2x-2-r}{2}| r+1x2(l+r)=2r+22xl=2r+22xl=2l+2x2r

因为 ∣ l + 2 x − 2 − r 2 ∣ = ∣ l + 2 x − 2 − r 2 ∣ |\frac{l+2x-2-r}{2}|=|\frac{l+2x-2-r}{2}| 2l+2x2r=2l+2x2r

所以我们交换后的序列和交换前的序列关于中轴线对称,即完成了翻转操作。

因为原序列的第一个是没有比他小的位置的,所以我们额外插入一个0作为新序列的第一个位置,目的是使原序列的前面有比他小的位置,但这样我们的原序列的编号在新序列里就要加1。

最后一个位置同理,加入一个最大值的在序列后面。

原序列如图:

序列终结者[splay平衡树]_第4张图片

新序列:

序列终结者[splay平衡树]_第5张图片

所以区间前一个和后一个就改变了。

这里使用了懒标记,如果我们要用到这个节点就将懒标记下传到儿子。

懒标记为1就是要交换,为0就是不用交换。

如果之前要翻转这个节点,但现在又要翻转回去就是不用翻转。

区间修改与查询

我们在每一个节点额外增加3个变量,分别为该点的值,该点子树内的值的最大值,值的懒标记,每次调用该节点前下传懒标记,更新值。

插入

我们插入的时候都是从序列开头遍历到序列的结尾进行初始化插入,那么也就是插入的序列是单调递增的,那么我们每次插入的时候直接向右子树进行插入找到空位就好了。记得加0和最大值。

代码

#include
using namespace std;

#define ll long long

const ll maxn=50005,inf=-2e18;

struct node
{
    ll ch[2],sz,lazy,val,mx=inf,fa;//最大值mx初始化为负无穷。历史影响:100pts->0pts。历史意义:成功取得倒3
    //ch[0]左儿子,ch[1]右儿子。
    bool fzlazy;//翻转懒标记
}tree[maxn];

ll n,m,rt,cnt;//rt为树根编号

void pushdown(ll p)//下传
{
    ll &lc=tree[p].ch[0],&rc=tree[p].ch[1];
    if(tree[p].fzlazy)//翻转
    {
        swap(lc,rc);
        //p翻转p的儿子也要翻转。
        tree[lc].fzlazy^=1;
        tree[rc].fzlazy^=1;
        tree[p].fzlazy=0;
    }
    if(tree[p].lazy)//下传值的懒标记
    {
        if(lc)
        {
            tree[lc].lazy+=tree[p].lazy;
            tree[lc].mx+=tree[p].lazy;
            tree[lc].val+=tree[p].lazy;
        }
        if(rc)
        {
            tree[rc].lazy+=tree[p].lazy;
            tree[rc].mx+=tree[p].lazy;
            tree[rc].val+=tree[p].lazy;
        }
    }
    tree[p].mx=max(tree[rc].mx,tree[lc].mx);//更新最大值
    tree[p].lazy=0;
}
void updata(ll p)//更新sz子树大小和最大值
{
    pushdown(p);
    tree[p].sz=tree[tree[p].ch[0]].sz+tree[tree[p].ch[1]].sz+1;
    tree[p].mx=max(tree[tree[p].ch[0]].mx,max(tree[tree[p].ch[1]].mx,tree[p].val));
}

void rotato(ll x)//旋转
{
    if(tree[x].fa==0||x==0) return ;
    ll y=tree[x].fa;
    ll z=tree[y].fa;
    pushdown(y);//使用y,x前先下传,本次旋转并不会影响z
    pushdown(x);
    bool flg=(tree[y].ch[1]==x);
    tree[y].ch[flg]=tree[x].ch[!flg];
    if(tree[x].ch[!flg]) tree[tree[x].ch[!flg]].fa=y;
    tree[x].ch[!flg]=y;
    tree[x].fa=z;
    tree[y].fa=x;
    if(z) tree[z].ch[(tree[z].ch[1]==y)]=x;
    updata(y);
    updata(x);
}
void splay(ll x,ll goal)//splay操作
{
    if(tree[x].fa==0||x==0) return ;
    for(ll y;(y=tree[x].fa)!=goal;rotato(x))
    {
        ll z;
        if((z=tree[y].fa)!=goal&&z)
        {
            if((tree[z].ch[1]==y)==(tree[y].ch[1]==x)) rotato(y);
            else rotato(x);
        }
    }
    if(goal==0) rt=x;
}

ll getval(ll p,ll x)//和平衡树求排名很像,只不过最后返回的是节点下标
{
    if(!p) return 0;
    pushdown(p);//记得使用前先下传标记
    if(tree[tree[p].ch[0]].sz>=x) return getval(tree[p].ch[0],x);
    if(tree[tree[p].ch[0]].sz+1>=x) {return p;splay(p,0);}//返回下标
    return getval(tree[p].ch[1],x-(tree[tree[p].ch[0]].sz+1));
}

void insert(ll &p,ll x,ll f)
{
    if(!p)
    {
        cnt++;
        p=cnt;
        tree[p].fa=f;
        updata(p);
        splay(p,0);
        return ;
    }
    insert(tree[p].ch[1],x,p);//插入的数单调递增,直接向右插入
    updata(p);
}

int main()
{
    scanf("%lld%lld",&n,&m);
    for(ll i=0;i<=n+1;i++) insert(rt,i,0);
    for(ll i=1;i<=m;i++)
    {
        ll k,l,r,v;
        scanf("%lld%lld%lld",&k,&l,&r);
        if(k==1)//区间增加
        {
            scanf("%lld",&v);
            l=getval(rt,l);//求区间前一个点的下标
            r=getval(rt,r+2);//求区间后一个点的下标
            splay(l,0);//将l转到根
            splay(r,l);//将r的父亲转为l
            tree[tree[r].ch[0]].lazy+=v;//有点类似线段树
            tree[tree[r].ch[0]].mx+=v;//该子树内所以节点的值都要加上v,所以最大值直接加上v
            tree[tree[r].ch[0]].val+=v;
            updata(r);
            updata(l);
        }
        else if(k==2)
        {
            l=getval(rt,l);
            r=getval(rt,r+2);
            splay(l,0);
            splay(r,l);
            tree[tree[r].ch[0]].fzlazy^=1;//翻转懒标记
            //0^1=1,1^1=0
            updata(r);
            updata(l);
        }
        else
        {
            l=getval(rt,l);
            r=getval(rt,r+2);
            splay(l,0);
            splay(r,l);
            printf("%lld\n",tree[tree[r].ch[0]].mx);//直接输出区间最大值
        }
    }
}

后续

deepseaspray大佬说fhq-treap也可以进行区间操作,但我码splay的题解都码4个小时,fhq-treap再码一次有点大可不必了。

教训:暴力出奇迹,n方过百万。 记得检查。

你可能感兴趣的:(算法,c++,数据结构)