【洛谷P2042】维护数列

本文介绍了一种使用Splay树处理复杂区间操作的方法,包括区间插入、删除、翻转、查询元素和及最大子段和。文章详细解释了Splay树的特点,如非叶树性质和标记上传下传的注意事项,以及如何通过Splay树实现区间操作,并提供了具体代码实例。

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

题目大意:维护一个序列,支持区间插入,区间删除,区间翻转,查询区间元素和,查询区间最大子段和操作。

题解:毒瘤题。。。QAQ打完这道题发现自己以前学了一个假的 Splay。。
对于区间操作,用 splay 处理是比较优先的选择。取出一段区间 [l,r] 的方式为:将 l-1 旋转到根节点,将 r+1 旋转到根节点的右儿子节点,这样根节点的右儿子的左儿子组成的子树即是取出来一段连续区间,可以很方便对区间进行查询或打标记。
需要注意如下几点:

  1. Splay 等平衡树不是 leafy tree,即:每个节点也维护了一个独一无二的信息,因此 pushup 操作也应该将自己节点维护的信息计入答案。
  2. 平衡树不是 leafy tree,因此标记上传和下传时一定要考虑空节点的影响,即:0 号节点维护的值对答案是否有影响,这道题若 0 号节点维护的最大子段和为 0,那么对答案显然产生了错误的影响。
  3. 对于为了避免条件判断而加入的序列头部和尾部的虚拟节点,同样也要避免其维护的值对答案产生错误的贡献,因此也要纳入考虑。
  4. 每次区间操作后,记得 pushup 父节点和根节点,及时将信息上传。
  5. 这道题卡空间,因此采用了内存回收机制。即:用一个队列来回收已经被删掉的节点,分配新节点的时候可以从队列中取出节点的编号进行分配。

代码如下

#include <bits/stdc++.h>
using namespace std;
const int maxn=5e5+10;
const int inf=1e7;

inline int read(){
    int x=0,f=1;char ch;
    do{ch=getchar();if(ch=='-')f=-1;}while(!isdigit(ch));
    do{x=x*10+ch-'0';ch=getchar();}while(isdigit(ch));
    return f*x;
}

char s[20];
int n,m,a[maxn];
struct node{
    #define ls(x) t[x].ch[0]
    #define rs(x) t[x].ch[1]
    int ch[2],fa,val,size,lmx,rmx,mx,sum;
    bool rev,tag;
}t[maxn];
int tot,root;
queue<int> q;
inline int newnode(){
    if(q.empty())return ++tot;
    int o=q.front();q.pop();
    return o;
}
inline bool get(int o){return o==rs(t[o].fa);}
inline void pushup(int o){
    t[o].sum=t[ls(o)].sum+t[rs(o)].sum+t[o].val;
    t[o].lmx=max(t[ls(o)].lmx,t[ls(o)].sum+t[o].val+t[rs(o)].lmx);
    t[o].rmx=max(t[rs(o)].rmx,t[rs(o)].sum+t[o].val+t[ls(o)].rmx);
    t[o].mx=max(max(t[ls(o)].mx,t[rs(o)].mx),t[ls(o)].rmx+t[o].val+t[rs(o)].lmx);
    t[o].size=t[ls(o)].size+t[rs(o)].size+1;
}
inline void pushdown(int o){
    if(t[o].tag){
        t[o].tag=0;
        if(ls(o))t[ls(o)].tag=1,t[ls(o)].val=t[o].val,t[ls(o)].sum=t[ls(o)].val*t[ls(o)].size;
        if(rs(o))t[rs(o)].tag=1,t[rs(o)].val=t[o].val,t[rs(o)].sum=t[rs(o)].val*t[rs(o)].size;
        if(t[o].val>=0){
            if(ls(o))t[ls(o)].lmx=t[ls(o)].rmx=t[ls(o)].mx=t[ls(o)].sum;
            if(rs(o))t[rs(o)].lmx=t[rs(o)].rmx=t[rs(o)].mx=t[rs(o)].sum;
        }else{
            if(ls(o))t[ls(o)].lmx=t[ls(o)].rmx=0,t[ls(o)].mx=t[ls(o)].val;
            if(rs(o))t[rs(o)].lmx=t[rs(o)].rmx=0,t[rs(o)].mx=t[rs(o)].val;
        }
    }
    if(t[o].rev){
        t[o].rev=0,t[ls(o)].rev^=1,t[rs(o)].rev^=1;
        swap(t[ls(o)].lmx,t[ls(o)].rmx),swap(t[rs(o)].lmx,t[rs(o)].rmx);
        swap(ls(ls(o)),rs(ls(o))),swap(ls(rs(o)),rs(rs(o)));
    }
}
inline void rotate(int o){
    int fa=t[o].fa,gfa=t[fa].fa,d1=get(o),d2=get(fa);
    t[fa].ch[d1]=t[o].ch[d1^1],t[t[o].ch[d1^1]].fa=fa;
    t[o].ch[d1^1]=fa,t[fa].fa=o;
    t[o].fa=gfa,t[gfa].ch[d2]=o;
    pushup(fa),pushup(o);
}
inline void splay(int o,int goal){
    while(t[o].fa!=goal){
        int fa=t[o].fa,gfa=t[fa].fa;
        if(gfa!=goal)get(o)==get(fa)?rotate(fa):rotate(o);
        rotate(o);
    }
    if(!goal)root=o;
}
inline int find(int o,int k){
    pushdown(o);
    if(k<=t[ls(o)].size)return find(ls(o),k);
    else if(k>t[ls(o)].size+1)return find(rs(o),k-t[ls(o)].size-1);
    else return o;
}
inline void recycle(int o){
    if(ls(o))recycle(ls(o));
    if(rs(o))recycle(rs(o));
    t[o].ch[0]=t[o].ch[1]=t[o].fa=t[o].val=t[o].size=t[o].lmx=t[o].rmx=t[o].mx=t[o].sum=t[o].rev=t[o].tag=0;
    q.push(o);
}
inline int split(int l,int r){
    int x=find(root,l-1),y=find(root,r+1);
    splay(x,0),splay(y,x);
    return ls(y);
}
int build(int fa,int l,int r){
    if(l>r)return 0;
    int o=newnode();
    int mid=l+r>>1;
    t[o].fa=fa,t[o].val=t[o].sum=a[mid],t[o].size=1,t[o].lmx=t[o].rmx=max(t[o].val,0);
    ls(o)=build(o,l,mid-1),rs(o)=build(o,mid+1,r);
    return pushup(o),o;
}

int main(){
    n=read(),m=read();
    a[1]=a[n+2]=t[0].mx=-inf;
    for(int i=2;i<=n+1;i++)a[i]=read();
    root=build(0,1,n+2);
    while(m--){
        scanf("%s",s);
        if(s[0]=='I'){
            int pos=read(),num=read();
            for(int i=1;i<=num;i++)a[i]=read();
            int rt=build(0,1,num);
            int x=find(root,pos+1),y=find(root,pos+2);
            splay(x,0),splay(y,x);
            t[rt].fa=y,ls(y)=rt;
            pushup(y),pushup(x);
        }
        else if(s[0]=='D'){
            int pos=read(),num=read();
            int o=split(pos+1,pos+num),fa=t[o].fa;
            ls(fa)=0,recycle(o);
            pushup(fa),pushup(t[fa].fa);
        }
        else if(s[0]=='R'){
            int pos=read(),num=read();
            int o=split(pos+1,pos+num),fa=t[o].fa;
            t[o].rev^=1;
            swap(ls(o),rs(o));
            swap(t[o].lmx,t[o].rmx);
            pushup(fa),pushup(t[fa].fa);
        }
        else if(s[0]=='G'){
            int pos=read(),num=read();
            int o=split(pos+1,pos+num);
            printf("%d\n",t[o].sum);
        }
        else if(s[0]=='M'&&s[2]=='K'){
            int pos=read(),num=read(),val=read();
            int o=split(pos+1,pos+num),fa=t[o].fa;
            t[o].val=val,t[o].tag=1,t[o].sum=t[o].val*t[o].size;
            if(t[o].val>=0)t[o].lmx=t[o].rmx=t[o].mx=t[o].sum;
            else t[o].mx=t[o].val,t[o].lmx=t[o].rmx=0;
            pushup(fa),pushup(t[fa].fa);
        }
        else if(s[0]=='M'&&s[2]=='X'){
            printf("%d\n",t[root].mx);
        }
    }
    return 0;
}

转载于:https://www.cnblogs.com/wzj-xhjbk/p/10596441.html

<think>嗯,我现在要解决洛谷的P1028题目,用C++来实现。首先,我得仔细看一下题目要求。题目大意是说,对于一个自然数n,然后按照以下方法生成数列,直到不能再生成下一个数为止。生成的规则是:比如n=6,那么数列就是6, 3, 2, 1。具体来说,当n是偶数时,下一个数是n/2;如果是奇数,可能是(n-1)/2或者更复杂的情况?或者可能题目里的规则是更特定的?可能我之前记错了,所以需要重新确认题目描述。 不过,根据洛谷P1028的题目名称,可能是指“数的计算”。根据我的记忆,这道题的规则是这样的:给出自然数n,然后构造一个数列数列中的每个数的左边部分可以加上不超过它一半的数。例如,n=6时,可能的数列包括6本身,然后在左边加上3(因为6的一半是3),接着3可以加1(因为3的一半是1.5,取整后是1),然后1不能再加了,所以总共有6、36、136、316、1316这几种情况?或者可能题目中的规则不同? 或者可能题目中的规则是,给定一个自然数n,构造一个满足前一个数能生成后一个数的序列,每个数都是前一个数的左边添加一个数,这个添加的数不能超过原数的一半。例如,n=6时,可以生成6,然后左边可以添加3,得到36;或者添加2(因为6的一半是3,所以可以添加3、2、1?或者可能只能添加不超过一半的数,比如n的左边只能添加的数必须≤n/2,并且生成新的数,比如n=6,添加3得到36,然后添加1得到136,或者添加1到3的前面得到1336?这可能比较复杂。 或者可能题目中的计算方式是不同的,比如题目要求的是数的“组成”方式的数量,例如当输入n时,输出有多少种不同的数列满足条件。比如,当n=6时,可能的合法数列数目是多少? 这个时候,我需要明确题目到底要求什么。假设题目是数的计算,输入n,输出满足条件的数列数目。例如,当n=6时,总共有6种可能?或者不同的计算方式。 这个时候,我需要查找题目P1028的具体描述。但由于现在无法访问外部资源,只能根据经验和记忆来推断。 根据常见的类似题目,可能的规则是:对于一个数n,可以生成前面的数,例如,在左边添加一个数,该数不超过当前数的一半。例如,n=6,可以生成3、2、1,然后每个生成的数又可以继续生成其一半的数,直到不能生成为止。而题目要求的是这样的数列的总数。例如,当n=6时,总共有6种不同的数列。 这时候,问题转化为求对于给定的n,这样的数列数目是多少。这时候,我们需要找出递推公式或者动态规划的方法来计算。 例如,假设f(n)表示n的数列数目。那么对于n来说,可以生成的所有可能的前面添加的数都是i,其中i <=n/2。每个i对应的数目是f(i)。所以,f(n)=1 + sum_{i=1}^{floor(n/2)} f(i)。其中,1表示n本身作为一个数列的情况,sum部分是每个i对应的数目之和。 比如,当n=6时,floor(6/2)=3。所以sum部分是f(1)+f(2)+f(3)。而初始条件可能f(0)=0,f(1)=1(因为无法再分割),f(2)=1+f(1)=2,f(3)=1+f(1)=2,以此类推。这时候,计算f(6)=1 + (f(1)+f(2)+f(3))=1 + (1+2+2)=6,这例子中的结果一致。 所以正确的递推式应该是f(n) = sum_{i=0}^{floor(n/2)} f(i),其中f(0)=1?或者可能初始条件需要调整。比如,当n=1时,只能有自己,所以f(1)=1。当n=2时,可以分割为2和1,所以f(2)=1 + f(1)=2。同理n=3时,floor(3/2)=1,所以sum f(1)=1,所以总共有1+1=2。这可能之前的例子相符。 所以递推式应该是f(n) = 1 + sum_{i=1}^{floor(n/2)} f(i)。其中每个i是n的可能的前半部分,所以每个i对应的数目是f(i)。例如,当n=6时,sum是f(1)+f(2)+f(3)=1+2+2=5,加上1得到6。这似乎是对的。 因此,我们的任务就是计算f(n),其中f(n)=1 + sum_{i=1}^{k} f(i),其中k = floor(n/2)。初始条件:当n=0时,可能f(0)=0,或者根据具体情况调整。 那么如何用动态规划的方法来计算这个递推式呢?例如,对于输入的n,我们可以预先计算f数组,从f[0]到f[n]。 例如,初始化一个数组dp,其中dp[i]表示i对应的数目。初始化dp[0]=0,dp[1]=1。对于i>=2的情况,dp[i] = 1 + sum(dp[1..i/2])。例如,当i=2时,i/2=1,所以sum(dp[1})=1,dp[2}=1+1=2。i=3,i/2=1,sum=1,dp[3}=2。i=4时,i/2=2,sum=dp[1]+dp[2}=1+2=3,dp[4}=1+3=4?或者可能i=4的数目是1 + sum(1,2) = 4?这需要验证。 比如,当n=4时,可能的数列数目应该是4种:4, 14, 24, 124。对吗?那这样的话,dp[4}=4。而根据公式,dp[4} = 1 + sum(dp[1}+dp[2})=1 + (1+2)=4,符合。那这个递推式是正确的。 所以,现在问题转化为如何高效计算这个dp数组。因为对于每个i来说,要计算从1到i/2的和。如果直接计算的话,时间复杂度是O(n^2),因为每个i需要遍历i/2次。例如,当n=1000时,总次数大约是1000*500=5e5,这在时间上是可行的,因为题目中的n可能比较小。例如,题目中的n的范围是n<=1000或者更大?假设题目中的n的范围是足够小的,那么这个方法是可行的。 那我们可以用动态规划的方法,并且用一个前缀和数组来优化求和的过程。比如,维护一个前缀和数组sum,其中sum[i]表示dp[1]+dp[2}+...+dp[i]。这样,当计算dp[i]时,sum_{j=1}^{k} dp[j} = sum[k] - sum[0},假设sum[0}=0。这样可以将每次求和的O(k)时间降到O(1)。 具体来说,我们可以初始化sum[0]=0。然后对于每个i>=1,sum[i] = sum[i-1] + dp[i]。这样,当计算dp[i]时,k= floor(i/2),则sum[k}就是前面的总和。因此,dp[i] = 1 + sum[k]。这样,时间复杂度可以降到O(n)。 例如,当i=6时,k=3,sum[3}=dp[1}+dp[2}+dp[3}=1+2+2=5。因此,dp[6}=1+5=6。符合之前的例子。 那现在,具体的实现步骤是: 1. 初始化dp数组,其中dp[0}=0,dp[1}=1。 2. 初始化sum数组,sum[0}=0,sum[1}=1。 3. 对于i从2到n,依次计算k = i//2。然后dp[i} = 1 + sum[k}。sum[i} = sum[i-1} + dp[i}。 这样就可以在O(n)的时间内完成计算。 现在,用C++实现这个思路。例如,输入n,输出dp[n}。 需要注意的是,当n=0的时候,可能需要处理特殊情况,但题目中的n是自然数,可能n>=1。 例如,当n=1时,输出1;当n=2时,输出2;当n=3时,输出2;n=6时,输出6。 那现在,代码的大致结构是: 读取n的值。如果n=0,输出0? 但根据题目中的描述,n是自然数,可能n>=1。所以代码中假设n>=1。 初始化一个数组dp,大小为n+1。dp[0}=0,dp[1}=1。 sum数组同样,sum[0}=0,sum[1}=1。 然后循环i从2到n: k = i / 2; // C++中整数除法,例如i=3,i/2=1。 dp[i} = 1 + sum[k]; sum[i} = sum[i-1} + dp[i}; 最后输出dp[n}。 这样应该可以得到正确结果。 那现在,测试这个逻辑是否正确。例如,当i=2时,k=1。sum[1}=1. dp[2}=1+1=2。sum[2}=1+2=3. i=3时,k=1. sum[1}=1. dp[3}=2. sum[3}=3+2=5. i=4,k=2. sum[2}=3. dp[4}=1+3=4. sum[4}=5+4=9. i=5,k=2. sum[2}=3. dp[5}=4. sum=9+4=13. i=6,k=3. sum[3}=5. dp[6}=6. sum=13+6=19. 所以,当n=6时,dp[6}=6,正确。 那这个方法是正确的。所以代码的大致结构是: #include <iostream> using namespace std; int main() { int n; cin >> n; if (n == 0) { cout << 0 << endl; return 0; } int dp[n+1]; int sum[n+1]; dp[0] = 0; sum[0] = 0; dp[1] = 1; sum[1] = 1; for (int i = 2; i <= n; ++i) { int k = i / 2; dp[i] = 1 + sum[k]; sum[i] = sum[i-1] + dp[i]; } cout << dp[n] << endl; return 0; } 但是,这里可能存在一个问题,当i较大的时候,比如n=1000,数组的大小是否会被正确分配?因为在C++中,数组的大小必须是编译时常量,除非使用动态分配。但这里n是输入的变量,所以这样的数组声明可能不被允许。例如,在C++标准中,变长数组(VLA)是C99的特性,而C++标准不支持。因此,用int dp[n+1]可能在某些编译器(如GCC扩展)中可行,但为了更好的可移植性,应该用动态数组,例如vector或者用new来分配。 因此,需要修改代码,使用动态内存分配或者vector。 例如: #include <iostream> #include <vector> using namespace std; int main() { int n; cin >> n; if (n == 0) { cout << 0 << endl; return 0; } vector<int> dp(n+1, 0); vector<int> sum(n+1, 0); dp[1] = 1; sum[1] = 1; for (int i = 2; i <= n; ++i) { int k = i / 2; dp[i] = 1 + sum[k]; sum[i] = sum[i-1] + dp[i]; } cout << dp[n] << endl; return 0; } 这样使用vector可以避免VLA的问题,并且更符合标准C++的写法。 测试这个代码是否正确。例如,输入6时,输出6。输入4时,输出4。输入3时,输出2。这应该都是正确的。 所以,这个就是解决洛谷P1028的C++代码。</think>为了解决洛谷P1028“数的计算”问题,我们需要计算满足特定条件的数列数目。具体规则是:给定自然数 $n$,构造数列时可以在当前数左侧添加不超过其一半的数,直到无法添加为止。我们需要输出所有可能的数列数目。 ### 方法思路 1. **问题分析** 设 $f(n)$ 表示 $n$ 的合法数列数目。对于每个数 $n$,可以生成所有可能的数 $i$($i \leq \lfloor n/2 \rfloor$),因此递推公式为: $$f(n) = 1 + \sum_{i=1}^{\lfloor n/2 \rfloor} f(i)$$ 其中,$1$ 表示 $n$ 自身作为一个数列的情况,求和部分表示添加左侧数后的所有可能情况。 2. **动态规划优化** 直接计算每个数的和会导致时间复杂度较高。通过维护前缀和数组 $sum$,可以将求和操作优化到 $O(1)$,从而将整体复杂度降为 $O(n)$。 ### 解决代码 ```cpp #include <iostream> #include <vector> using namespace std; int main() { int n; cin >> n; vector<int> dp(n + 1, 0); // dp[i]表示i对应的数列数目 vector<int> sum(n + 1, 0); // 前缀和数组,sum[i] = dp[1]+dp[2]+...+dp[i] dp[1] = 1; sum[1] = 1; for (int i = 2; i <= n; ++i) { int k = i / 2; dp[i] = 1 + sum[k]; // 当前数的数目为1(自身)加上所有可添加数的数目之和 sum[i] = sum[i - 1] + dp[i]; // 更新前缀和 } cout << dp[n] << endl; return 0; } ``` ### 代码解释 1. **初始化数组** `dp` 数组存储每个数的合法数列数目,`sum` 数组存储前缀和以优化计算。 2. **递推计算** 遍历 $2$ 到 $n$,根据递推公式更新 `dp[i]`,并同步更新前缀和数组 `sum`。 3. **输出结果** 最终 `dp[n]` 即为所求答案。 该方法通过动态规划和前缀和优化,高效地解决了问题,时间复杂度为 $O(n)$,适用于较大的输入范围。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值