Splay 模板【NOI2005】 bzoj1500 维修数列

本文详细介绍了Splay树的数据结构及其应用场景,包括插入、删除、翻转等操作,并通过NOI2005维修数列题目展示了Splay树如何高效实现区间修改、查询等操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

老师说写博客有助于学习和理解代码,所以从今天起,做一个写博客的人,面朝代码,春暖花开。


Splay是一种可以对一个数列进行区间修改,区间反转,查询最值,查询总和等操作的数据结构(我觉得Splay的结构类似于treap,功能类似于线段树),而且能够快速实现区间的分裂与合并。


Splay与Treap的最大区别是旋转方式,Treap将树按照堆得形式维护,只需要单旋。而Splay需要双旋,来保证时间复杂度为均摊O(log n)。

Spaly最主要的操作是伸展操作,即将某一个节点旋转到根节点处,主要依靠双旋来完成。

用Splay进行区间的分裂与合并可以通过伸展操作来构造。


————————————————Splay————————————————


NOI2005维修数列是一道Splay裸题,题目要求就是写一个程序要求维护一个数列,支持插入、删除、翻转、修改、查询区间和,查询最大子段这些操作。


插入和删除操作只需要把原序列分裂,插入或删除一段,再合并起来就可以。

翻转和修改只需要把需要操作的区间分离出来,打标记再合并。

查询区间和和最大字段,可以像线段树一样在每个Splay节点处维护区间最大和,左侧最大和,右侧最大和,区间总和。

最后把要查询的区间分裂出来查询就可以了。


注意事项:

1、如果合并函数返回一个指针的话,传参就不要打引用;

2、如果区间内全是负数,应该找最大的负数输出,所以maintain的时候要注意;

3、要时刻想着是否应该判断空节点(如果建立虚节点不必考虑此问题),因为分裂出来的区间或者将要用来合成的区间很有可能是NULL,防止RE;

4、使用指针和引用的时候要小心,尤其是当真正修改原指针的时候。

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<string>
#include<cstring>
#define N 510002
#define INF 2147483647
#define Max(x,y) (x>y?x:y)
using namespace std;
int n,m,pos,tot,key;
int a[N];
char s[20];
struct splay
{
    splay *ch[2],*fa;
    int sz,val,ls,rs,ms,sum;
    int flip,lz;
    splay(int x)//构造函数
    {
        ch[0]=ch[1]=fa=NULL;
        sz=1;
        val=sum=ls=rs=ms=x;
        flip=0;lz=INF;
    }
    int cmp(int k)
    {
        int z=1;
        if(ch[0]) z+=ch[0]->sz;
        if(k<z) return 0;
        if(k>z) return 1;
        return -1;
    }
    void push_down()//下传标记
    {
        if(flip==1)
        {
            flip=0;
            swap(ch[0],ch[1]);
            swap(ls,rs);
            if(ch[0]) ch[0]->flip^=1;
            if(ch[1]) ch[1]->flip^=1;
        }
        if(lz!=INF)
        {
            val=lz;
            sum=lz*sz;
            ls=rs=ms=lz>0?lz*sz:lz;
            if(ch[0]) ch[0]->lz=lz;
            if(ch[1]) ch[1]->lz=lz;
            lz=INF;
        }
    }
    void maintain()//维护节点信息
    {
        push_down();
        sz=1;sum=ms=ls=rs=val;
        if(ch[0]) {ch[0]->push_down();sz+=ch[0]->sz;sum+=ch[0]->sum;}
        if(ch[1]) {ch[1]->push_down();sz+=ch[1]->sz;sum+=ch[1]->sum;}
             if(!ch[0] && !ch[1]) return;
        else if(!ch[0] && ch[1])
        {
            ms=Max(Max(ms,val+ch[1]->ls),ch[1]->ms);
            ls=Max(ls,val+ch[1]->ls);
            rs=Max(ch[1]->rs,ch[1]->sum+val);
        }
        else if(ch[0] && !ch[1])
        {
            ms=Max(Max(ms,val+ch[0]->rs),ch[0]->ms);
            ls=Max(ch[0]->ls,ch[0]->sum+val);
            rs=Max(rs,ch[0]->rs+val);
        }
        else
        {
            ms=Max(Max(ms+Max(ch[0]->rs,0)+Max(ch[1]->ls,0),ch[0]->ms),ch[1]->ms);
            ls=Max(ch[0]->ls,ch[0]->sum+val+Max(ch[1]->ls,0));
            rs=Max(ch[1]->rs,ch[1]->sum+val+Max(ch[0]->rs,0));
        }
    }
    friend void turn(splay *&c,int d)//旋转
    {
        splay *y=c->ch[d^1];
        c->ch[d^1]=y->ch[d];
        if(y->ch[d]) y->ch[d]->fa=c;
        y->ch[d]=c; y->fa=c->fa;
        c->fa=y; c->maintain();
        c=y;     c->maintain();
    }
    friend void splaying(splay *&c,int k)//伸展操作
    {
        c->push_down();
        int d=c->cmp(k);
        if(d==1) k-=c->sz-c->ch[1]->sz;
        if(d!=-1)
        {
            splay *y=c->ch[d];
            y->push_down();
            int d2=y->cmp(k);
            if(d2==1) k-=y->sz-y->ch[1]->sz;
            if(d2!=-1)
            {
                splaying(y->ch[d2],k);
                if(d==d2) turn(c,d^1);
                else      turn(c->ch[d],d);
            }
            turn(c,d^1);
        }
    }
    friend void build_it(splay *&c,int l,int r)//将数组转成splay
    {
        if(l>r) return;
        int mid=(l+r)/2;
        c=new splay(a[mid]);
        build_it(c->ch[0],l,mid-1);
        build_it(c->ch[1],mid+1,r);
        if(c->ch[0]) c->ch[0]->fa=c;
        if(c->ch[1]) c->ch[1]->fa=c;
        c->maintain();
    }
    friend void dele(splay *&c)//删除操作
    {
        if(c->ch[0]) dele(c->ch[0]);
        if(c->ch[1]) dele(c->ch[1]);
        delete c;
    }
    friend splay* Merge(splay *x,splay *y)//合并
    {
        if(!x) return y;
        if(!y) return x;
        splaying(x,x->sz);
        x->ch[1]=y;
        if(y) y->fa=x;
        x->maintain();
        return x;
    }
    friend void Split(splay *V,int k,splay *&x,splay *&y)//分裂
    {
        if(k<=0)     {x=NULL;y=V;return;}
        if(k>=V->sz) {x=V;y=NULL;return;}
        x=V;
        splaying(x,k);
        y=x->ch[1]; y->fa=NULL;
        x->ch[1]=NULL;
        x->maintain();
        return;
    }
}*V,*x,*y;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    build_it(V,1,n);
    while(m--)
    {
        scanf("%s",s);
             if(s[2]=='S')
        {
            scanf("%d%d",&pos,&tot);
            for(int i=1;i<=tot;i++) scanf("%d",&a[i]);
            build_it(x,1,tot);
            Split(V,pos,V,y);
            V=Merge(Merge(V,x),y);
        }
        else if(s[2]=='L')
        {
            scanf("%d%d",&pos,&tot);
            Split(V,pos-1,V,x);
            Split(x,tot,x,y);
            dele(x);
            V=Merge(V,y);
        }
        else if(s[2]=='K')
        {
            scanf("%d%d%d",&pos,&tot,&key);
            Split(V,pos-1,V,x);
            Split(x,tot,x,y);
            if(x) x->lz=key;
            V=Merge(Merge(V,x),y);
        }
        else if(s[2]=='V')
        {
            scanf("%d%d",&pos,&tot);
            Split(V,pos-1,V,x);
            Split(x,tot,x,y);
            if(x) x->flip^=1;
            V=Merge(Merge(V,x),y);
        }
        else if(s[2]=='T')
        {
            scanf("%d%d",&pos,&tot);
            Split(V,pos-1,V,x);
            Split(x,tot,x,y);
            int ans=x?x->sum:0;
            printf("%d\n",ans);
            V=Merge(Merge(V,x),y);
        }
        else
        {
            V->maintain();
            printf("%d\n",V->ms);
        }
    }
    return 0;
}




<think>嗯,用户想了解如何用分块方法解决洛谷P2042这个维护数列的问题。首先,我需要回忆一下分块算法的基本概念。分块通常是将数据分成多个块,每个块单独处理,平衡查询和修改的时间复杂度。不过,题目是NOI2005的,可能比较复杂,因为维护数列通常涉及插入、删除、区间修改、翻转、求和、最大子段和等操作。分块处理这些操作可能比较麻烦,尤其是动态的插入和删除,会导致块的大小变化,影响效率。 首先,我得确认分块是否适合这个问题。一般来说,分块的时间复杂度是O(n√n),对于大数可能还行,但像这种NOI题目可能数据量较大,分块可能不够高效,不过用户明确问分块方法,所以需要找到如何结构设计。 维护数列的典型操作包括:在某个位置插入一段数列,删除某个区间,区间覆盖,区间翻转,区间求和,求最大子段和。分块的话,每个块需要维护这些操作的信息。比如每个块需要记录翻转标记、覆盖标记,以及各种区间和、最大子段和等。 接下来,分块的结构设计。每个块可以存储一个数组,同时维护块内的总和、最大前缀和、最大后缀和、最大子段和,以及翻转和覆盖的懒标记。插入和删除操作需要考虑块的分裂和合并,这可能比较复杂,因为频繁的插入删除会导致块的大小不均,影响效率。所以可能需要设置块的大小阈值,比如当块的大小超过2√n时分裂,小于√n/2时合并相邻块。 处理翻转操作时,每个块维护一个翻转标记,当需要翻转区间时,对覆盖的完整块标记翻转,并处理部分块。同样,覆盖操作需要懒标记,当块被完全覆盖时,直接更新块内的值,并记录覆盖值。 最大子段和的计算需要每个块维护内部的最大子段和、最大前缀和、最大后缀和,这样在查询时,可以将多个块的信息合并起来得到整体的最大子段和。这可能比较复杂,需要考虑块之间的前后缀如何组合。 插入和删除操作需要找到对应的位置所在的块,如果插入导致块过大,就分裂成两个块。删除时如果块变得过小,可能需要合并相邻块。这些操作需要维护块之间的链表结构,方便快速插入和删除块。 举个例子,当要在位置pos插入一段数列,首先找到pos所在的块,如果块的大小加上插入元素后超过阈值,就分裂该块为两个,然后将插入的元素作为新块插入到链表中。这样插入的时间复杂度是O(√n),因为分裂和插入块的时间与块的大小有关。 对于翻转和覆盖操作,应用懒标记是关键,这样可以延迟实际的操作,直到需要访问块的具体内容时才进行实际的处理。例如,当需要访问一个带有翻转标记的块时,先执行翻转操作,并清除标记。 不过分块在处理这些动态操作时,尤其是频繁的插入和删除,可能效率不如平衡树结构(如Splay或Treap),但分块实现起来可能相对简单一些。需要权衡实现复杂度和效率。 总结一下,分块实现的关键点包括: 1. 分块的结构设计,每个块维护必要的信息和懒标记。 2. 插入和删除时的块分裂与合并策略。 3. 懒标记的处理,如翻转和覆盖,何时下传。 4. 查询操作时如何合并各个块的信息,如最大子段和。 可能遇到的问题包括块的大小管理不当导致性能下降,懒标记处理错误导致数据不一致,以及在合并块信息时逻辑错误。需要详细设计每个操作的步骤,并处理各种边界情况,比如操作区间跨多个块,或者在块的开头或结尾的部分操作。 最后,参考用户提供的引用,尤其是引用[1]提到递归图的理解,可能分块的递归处理不太相关,但引用[2]提到洛谷题目,可能需要参考类似题解中的分块实现方法。不过用户提供的引用可能没有直接相关的内容,所以需要依靠已有的分块知识来构建解决方案。</think>### 分块方法实现洛谷 P2042 维护数列问题 #### 分块结构设计 1. **块内信息**: - 数据存储:每个块存储$size \leq 2\sqrt{n}$个元素 - 维护以下字段: ```text sum: 区间和 lmax: 最大前缀和 rmax: 最大后缀和 tmax: 最大子段和 rev: 翻转标记(0/1) cov: 覆盖标记(特殊值表示未覆盖) ``` 2. **分块逻辑**: - 块间通过链表或数组连接 - 插入/删除时动态调整块大小,触发分裂或合并(例如块大小超过$2\sqrt{n}$时分裂,相邻块总大小低于$\sqrt{n}$时合并) #### 核心操作实现 **1. 区间翻转(REVERSE)**: - 对完整覆盖的块:翻转标记`rev ^= 1`,交换`lmax`与`rmax` - 对部分覆盖的块:下传标记后暴力翻转元素 时间复杂度:$O(\sqrt{n})$ **2. 区间覆盖(MAKE-SAME)**: - 对完整覆盖的块:更新`cov`标记,直接计算`sum`,`lmax`,`rmax`,`tmax` - 对部分覆盖的块:下传标记后暴力修改 时间复杂度:$O(\sqrt{n})$ **3. 最大子段和(MAX-SUM)**: - 合并相邻块的`rmax`和`lmax`,遍历所有块维护全局最大值 时间复杂度:$O(\sqrt{n})$ **4. 插入(INSERT)**: - 定位插入位置所在块,若插入后块大小超过阈值则分裂 时间复杂度:$O(\sqrt{n} + k/\sqrt{n})$ **5. 删除(DELETE)**: - 移除目标区间涉及的所有块,合并相邻小块 时间复杂度:$O(\sqrt{n} + k/\sqrt{n})$ #### 代码框架示例(关键部分) ```python class Block: def __init__(self): self.data = [] self.sum = 0 self.lmax = self.rmax = self.tmax = -inf self.rev = 0 self.cov = None def push_down(self): # 下传覆盖/翻转标记到数据层 if self.cov is not None: self.data = [self.cov] * len(self.data) if self.rev: self.data.reverse() self.rev = 0 self.cov = None def update_info(self): # 重新计算sum/lmax/rmax/tmax pass class BlockList: def __init__(self): self.blocks = [] def split(self, idx): # 分裂过大的块 pass def merge(self): # 合并过小的相邻块 pass ``` #### 复杂度分析 | 操作类型 | 时间复杂度 | |----------------|---------------------| | 插入/删除 | $O(\sqrt{n} + k)$ | | 翻转/覆盖/求和 | $O(\sqrt{n})$ | | 最大子段和 | $O(\sqrt{n})$ | #### 注意事项 1. **标记下传策略**:在访问块内数据前必须下传所有标记 2. **块大小平衡**:通过动态分裂/合并保证$size \in [\sqrt{n}/2, 2\sqrt{n}]$ 3. **边界处理**:特别注意区间跨多个块时的部分覆盖情况
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值