peiwenjun's blog 没有知识的荒原

P6578 [Ynoi2019] 魔法少女网站 题解

题目描述

给定长为 \(n\) 的序列, \(m\) 次操作:

  • 1 x y :将 \(a_x\) 修改为 \(y\)
  • 2 l r x :求 \(\sum\limits_{l\le l'\le r'\le r}[\max\limits_{l'\le i\le r'} a_i\le x]\)

数据范围

  • \(1\le n,m\le 3\cdot 10^5\)
  • \(1\le l\le r\le n,1\le a_i,x,y\le n\)

时间限制 \(\texttt{3.5s}\) ,空间限制 \(\texttt{64MB}\)

分析

询问的答案为对 \([l,r]\) 中所有 \(\le x\) 的数构成的连续段, \(\frac{len(len+1)}2\) 之和。

先考虑没有修改时怎么做。

将询问按 \(x\) 升序排序,这样总共有 \(n\) 次单点插入操作,每次操作可能引起连续段合并。

序列分块,记块长为 \(B_1\)

维护 pre,suf,sum 数组分别表示 \(i\) 为段尾时段头的位置(不跨块)、 \(i\) 为段头时段尾的位置(不跨块)、第 \(i\) 块内连续段的贡献和

\(i\) 不为段尾时,令 pre[i]=0suf 同理。

显然合并代价 \(\mathcal O(1)\) ,回答询问时,维护当前答案 ans 和最后一个连续段长度 cur ,散块暴力扫,整块将 cur 和第一个连续段合并,然后用最后一个连续段更新 cur

预处理 \(\mathcal O(n)\) ,单组询问 \(\mathcal O(B_1+\frac n{B_1})\)

ll ans=0,cur=0;
auto work=[&](int j)
{
    a[j]<=x?ans+=++cur:cur=0;
};
if(u==v) for(int j=l;j<=r;j++) work(j);
else
{
    for(int j=l;j<=ed[u];j++) work(j);
    for(int j=u+1;j<=v-1;j++)
    {
        ans+=cur*max(suf[st[j]]-st[j]+1,0)+sum[j];
        if(!pre[ed[j]]) cur=0;
        else if(pre[ed[j]]==st[j]) cur+=B1;
        else cur=ed[j]-pre[ed[j]]+1;
    }
    for(int j=st[v];j<=r;j++) work(j);
}

接下来考虑修改操作。

询问依然按照 \(x\) 升序排序,将修改后的数组看成新建一个版本,我们需要一个指针滚动版本编号。

操作分块,每次处理 \(B_2\) 个操作,处理完记得更新 \(a\) 数组。

我们可以用 \(\mathcal O(n+B_2^2)\) 的时空复杂度维护所有版本。

具体的,将 \(t\le B_2\) 个修改的位置挑出来,从上一版本 memcpy 得到当前版本然后执行修改操作,其余 \(n-t\) 个位置的值始终不变。

再考虑如何回答询问。

合并连续段(插入)很好维护,但是修改会引入分裂连续段(删除)的操作,删除操作维护起来非常困难。

类比回滚莫队,我们希望将删除操作转化为插入 + 撤销。

非修改位置只会被插入一次,用 vector 记录插入时机。

网上很多题解这一步建议用邻接表,但实际上邻接表的 cache miss 非常大,实际效率反而不如 vector

这里需要用 \(B_2\)vector 合计存储 \(\mathcal O(n)\) 个数,注意 clear 函数会删除元素但不会释放空间,清空应该写成 vector<int>().swap(vec[i]); ,否则 \(\frac m{B_1}\) 轮操作会导致总空间非常大,出现一些莫名其妙的 \(\texttt{MLE}\)

回答询问前扫描所有修改位置,如果当前版本的 \(a_i\) 不超过 \(x\) ,插入位置 \(i\) ,回答完再撤销所有修改位置的贡献。

时间复杂度 \(\mathcal O(\frac m{B_2}\cdot(n+B_2^2)+m(B_1+\frac n{B_1}))=\mathcal O(m\sqrt n)\)

卡常 Tips :

  • fread 快读,本题 \(1.5\cdot 10^6\) 的读入量,相比普通快读可以节约 \(0.3\sim 0.5\texttt{s}\) 的时间。
  • 块长 \(B_1\)\(500\) 左右, \(B_2\)\(1500\) 左右最优。
  • 序列分块的部分需要精细实现,用三个数组( pre,suf,sum )实现比用 \(\frac n{B_1}\) 个结构体快的多。
  • 撤销部分需要精细实现,笔者最开始记录每个修改的位置和修改前的值,但其中有大量冗余信息,最后也不出意料的 \(\texttt{TLE}\) 了。实际上,记录被修改位置 \(x\) ,合并后的左右端点 \(l,r\) ,修改前 \(x\) 所在连续段的贡献 sum[bel[x]] 就可以还原出所有信息。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int B1=480,B2=1500,maxn=3e5+5;
int m,n,flg,top;/// flg=0/1 表示不需要/需要被撤销
int a[maxn],x[maxn],y[maxn],id[maxn],buc[maxn];
int bel[maxn],st[maxn/B1+5],ed[maxn/B1+5];
int b[B2+5][B2+5];
int pre[maxn],suf[maxn];///对连续段结尾, pre 存储开头;对连续段开头, suf 存储结尾
ll res[B2+5],sum[maxn/B1+5];/// sum[i] 表示第 i 块的贡献和
vector<int> vec[B2+5];
struct quer
{
    int l,r,x,t,id;
}e[B2+5];
struct oper
{
    int l,r,x;
    ll s;
}sta[B2+5];
inline ll calc(const int &x)
{
    return x*(x+1ll)/2;
}
inline void modify(const int &x)
{
    ///栈存储 int 类型修改,将 sum 的高低位分别存储
    int l=x,r=x,u=bel[x];
    ll tmp=sum[u];
    if(x!=st[u]&&pre[x-1]) sum[u]-=calc(x-pre[x-1]),l=pre[x-1],pre[x-1]=0;
    if(x!=ed[u]&&suf[x+1]) sum[u]-=calc(suf[x+1]-x),r=suf[x+1],suf[x+1]=0;
    sum[u]+=calc(r-l+1),pre[r]=l,suf[l]=r;
    if(flg) sta[++top]={l,r,x,tmp};
}
int read()
{
    int q=0;char ch=getchar();
    while(!isdigit(ch)) ch=getchar();
    while(isdigit(ch)) q=q*10+ch-'0',ch=getchar();
    return q;
}
void roll()
{
    while(top)
    {
        int l=sta[top].l,r=sta[top].r,x=sta[top].x;
        sum[bel[x]]=sta[top--].s,pre[x]=suf[x]=0;
        if(l!=x) pre[x-1]=l,suf[l]=x-1;
        if(r!=x) suf[x+1]=r,pre[r]=x+1;
    }
}
void solve(int m)
{
    vector<int> h;
    int k=0,t=0;
    for(int i=1;i<=m;i++)
    {
        int op=read();
        if(op==1) x[++t]=read(),y[t]=read(),h.push_back(x[t]);
        else e[++k].l=read(),e[k].r=read(),e[k].x=read(),e[k].t=t,e[k].id=k;
    }
    ///修改位置分配唯一编号,其余位置编号 -1, b[i] 存储第 i 次修改后的值
    sort(h.begin(),h.end());
    h.erase(unique(h.begin(),h.end()),h.end());
    memset(id,-1,sizeof(id));
    for(int i=0;i<h.size();i++) id[h[i]]=i,b[0][i]=a[h[i]];
    for(int i=1;i<=t;i++) memcpy(b[i],b[i-1],4*h.size()),b[i][id[x[i]]]=y[i];
    ///询问按 x 升序排序, vec[i] 存储回答第 i 个询问前 0->1 的非修改位置
    sort(e+1,e+k+1,[](quer a,quer b){return a.x<b.x;});
    for(int i=1;i<=k;i++) for(int j=e[i-1].x+1;j<=e[i].x;j++) buc[j]=i;
    fill(buc+e[k].x+1,buc+n+1,0);
    for(int i=1;i<=n;i++) if(!~id[i]&&buc[a[i]]) vec[buc[a[i]]].push_back(i);
    memset(pre,0,sizeof(pre));
    memset(suf,0,sizeof(suf));
    memset(sum,0,sizeof(sum));
    for(int i=1;i<=k;i++)
    {
        int l=e[i].l,r=e[i].r,x=e[i].x,t=e[i].t,u=bel[l],v=bel[r];
        for(auto j:vec[i]) modify(j);///非修改位置
        flg=1;
        for(int i=0;i<h.size();i++) if(b[t][i]<=x) modify(h[i]);///修改位置,后面会撤销
        ll ans=0,cur=0;
        auto work=[&](int i)
        {
            if((~id[i]?b[t][id[i]]:a[i])<=x) ans+=++cur;
            else cur=0;
        };
        if(u==v) for(int j=l;j<=r;j++) work(j);
        else
        {
            for(int j=l;j<=ed[u];j++) work(j);
            for(int j=u+1;j<=v-1;j++)
            {
                ans+=cur*max(suf[st[j]]-st[j]+1,0)+sum[j];
                if(!pre[ed[j]]) cur=0;
                else if(pre[ed[j]]==st[j]) cur+=B1;
                else cur=ed[j]-pre[ed[j]]+1;
            }
            for(int j=st[v];j<=r;j++) work(j);
        }
        res[e[i].id]=ans,flg=0,roll();
    }
    ///清空需要精细实现
    for(int i=1;i<=k;i++) printf("%lld\n",res[i]),vector<int>().swap(vec[i]);
    for(int i=0;i<h.size();i++) a[h[i]]=b[t][i];
}
int main()
{
    n=read(),m=read();
    for(int i=1;i<=n;i++) a[i]=read(),bel[i]=(i-1)/B1+1;
    for(int i=1;i<=bel[n];i++) st[i]=(i-1)*B1+1,ed[i]=min(i*B1,n);
    for(int i=1;i<=m/B2;i++) solve(B2);
    solve(m%B2);
    return 0;
}

posted on 2026-02-11 00:26  peiwenjun  阅读(8)  评论(0)    收藏  举报

导航