LG5386 Cnoi2019 数字游戏 题解

Cnoi2019 数字游戏

题意

给定一个长度为 \(n\) 的排列,你需要回答 \(m\) 个询问 \((l,r,x,y)\),表示询问 \([l,r]\) 这个区间内有多少个子区间的值域是 \([x,y]\) 的子集。

Sub1:\(n,m\le 3\times 10^4\)

样例:1 2 5 4 3 做询问 1 5 1 4 的答案是 6

如下区间会被统计进答案:

\( [1,1]\\ [1,2]\\ [2,2]\\ [4,4]\\ [4,5]\\ [5,5]\\ \)

对了,这题时限 \(7\) 秒。

做法

下面认为 \(n,m\) 同阶,一律以 \(n\) 表示。

  • \(\mathcal{O}(n^3)\)

每次暴力枚举,st表查询区间最值即可。

  • \(\mathcal{O}(n^2)\)

考虑到我们每一次只需要找到满足条件的极长区间即可。

(对于极长的定义:没有更长的就认为是极长,比如样例,极长区间是 \([1,2]\)\([4,5]\)。)

这个东西是好找的,暴力遍历找然后记录下长度就可以直接算答案了。

据说有小常数选手就这么过了?

  • \(\mathcal{O}(n\sqrt{n}\log n)\)

我们对于 \(x,y\) 两维做莫队,新建一个数组 \(b_i\) 表示原排列里的数 \(a_i\) 是否在当前莫队限制的值域内。

此时我们的问题就是找到 \(b_i\) 里面在 \([l,r]\) 内的所有极长 \(1\) 连续段(下称“极长段”)并统计答案,线段树能够完成这个任务,而且其实出乎意料地好写。

那么这道题就得到了解决。

进一步

Sub2:\(n,m\le 2\times 10^5\)


做法

  • \(\mathcal{O}(n\sqrt{n}\log\sqrt{n})\)

这个做法我是从同级大佬那里搞到的,我也没想到能过。

我们分块,对每个块维护线段树就能够降低修改的时间复杂度。

每次询问暴力扫所有块,由于询问只有 \(m\) 次,不会影响总的时间复杂度。

然后你会发现这个东西实际上只是砍掉了一个 \(2\) 的常数……

这个东西由于原题的时限有整整 \(7\) 秒,常数写小点可以 \(6\) 秒多点稳过。

下面这份代码需要看点脸,评测机波动一下就能过去。

如果想要更稳妥的写法,建议zkw线段树。

#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=200010,B=500;
struct node
{
    int llen,rlen;
    ll sum;
    int all_one;
    //线段树的维护自行推导,不难。
    node operator+(node oth)const noexcept
    {
        node ret={llen,oth.rlen,sum+oth.sum+rlen*oth.llen,all_one&oth.all_one};
        ret.llen+=all_one*oth.llen;
        ret.rlen+=oth.all_one*rlen;
        return ret;
    }
};
class Segment_Tree
{
public:
    node sgtr[1000000/B];
    void Change(int k,int l,int r,int x,int c)
    {
        if(l==x&&x==r)
        {
            if(c)sgtr[k]={1,1,1,1};
            else sgtr[k]={0,0,0,0};
            return;
        }
        int mid=(l+r)>>1;
        if(x>mid)Change(k*2+1,mid+1,r,x,c);else Change(k*2,l,mid,x,c);
        sgtr[k]=sgtr[k*2]+sgtr[k*2+1];
        return;
    }
    node Query(int k,int l,int r,int x,int y)
    {
        if(x<=l&&r<=y)return sgtr[k];
        int mid=(l+r)>>1;
        if(y<=mid)return Query(k*2,l,mid,x,y);
        if(x>mid)return Query(k*2+1,mid+1,r,x,y);
        return Query(k*2,l,mid,x,mid)+Query(k*2+1,mid+1,r,mid+1,y);
    }
}sgtr[B];
int a[N],r[N];
int n,q;
int block_len,block_num;
int bl[N],st[N],ed[N],id[N];
inline void Build_Block()
{
    //分块。
    block_len=sqrt(n);
    block_num=(n-1)/block_len+1;
    for(int i=1;i<=n;i++)bl[i]=(i-1)/block_len+1;
    for(int i=1;i<=block_num;i++)st[i]=(i-1)*block_len+1,ed[i]=i*block_len;
    for(int i=1;i<=n;i++)id[i]=i-st[bl[i]]+1;
    ed[block_num]=n;
    return;
}
struct query
{
    int id,l,r,x,y;
    int operator<(query oth){if(bl[x]==bl[oth.x])return (y<oth.y)^(bl[x]&1);return x<oth.x;}
}Q[N];
ll final_ans[N];
int X,Y;
inline void add(int x)
{
    sgtr[bl[x]].Change(1,1,ed[bl[x]]-st[bl[x]]+1,id[x],1);
    return;
}
inline void del(int x)
{
    sgtr[bl[x]].Change(1,1,ed[bl[x]]-st[bl[x]]+1,id[x],0);
    return;
}
inline ll Query(int l,int r)
{
    //询问区间 $[l,r]$ 在当前 $[X,Y]$ 限制下的答案。
    if(bl[l]==bl[r])return sgtr[bl[l]].Query(1,1,id[ed[bl[l]]],id[l],id[r]).sum;
    node ret=sgtr[bl[l]].Query(1,1,id[ed[bl[l]]],id[l],id[ed[bl[l]]]);
    for(int i=bl[l]+1;i<bl[r];i++)ret=ret+sgtr[i].Query(1,1,id[ed[bl[i]]],1,id[ed[bl[i]]]);
    ret=ret+sgtr[bl[r]].Query(1,1,id[ed[bl[r]]],1,id[r]);
    return ret.sum;
}
int main()
{
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)scanf("%d",a+i),r[a[i]]=i;
    for(int i=1;i<=q;i++)scanf("%d%d%d%d",&Q[i].l,&Q[i].r,&Q[i].x,&Q[i].y),Q[i].id=i;
    Build_Block();
    sort(Q+1,Q+q+1);
    int lst=0;
    X=1,Y=0;
    for(int i=1;i<=q;i++)
    {
        int l=Q[i].l,r=Q[i].r,x=Q[i].x,y=Q[i].y;
        while(X>x)X--,add(::r[X]);
        while(Y<y)Y++,add(::r[Y]);
        while(X<x)del(::r[X]),X++;
        while(Y>y)del(::r[Y]),Y--;
        final_ans[Q[i].id]=Query(l,r);
    }
    for(int i=1;i<=q;i++)printf("%lld\n",final_ans[i]);
    return 0;
}
  • \(\mathcal{O}(n\sqrt{n})\)

实际上上面那个卡常很不优美。

我们考虑莫队的时候实际上有 \(\mathcal{O}(n\sqrt{n})\) 次修改和 \(\mathcal{O}(n)\) 次询问。

因此如果我们找到一个 \(\mathcal{O}(1)\) 修改,\(\mathcal{O}(\sqrt{n})\) 查询的数据结构,就可以以更优的时间复杂度解决。

正好,我们还真能找到。

做序列分块,每个块内维护答案即可。

维护你可以考虑很多种实现,有链表、并查集等等。

这里我是从同级大佬那里学来的做法,对于每个极长段,在其左端点和右端点记录一下,这样就可以写出下面代码里的 insertundo 了。

然后这样维护有缺陷,就是删去一个数没法搞。

实际很好解决,外层的莫队改为回滚莫队即可。

那么这道题就解决了,据说还有双倍经验。

#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=200010;
int a[N],r[N];
int n,q;
int block_len,block_num;
int bl[N],st[N],ed[N];
inline void Build_Block()
{
    block_len=sqrt(n);
    block_num=(n-1)/block_len+1;
    for(int i=1;i<=n;i++)bl[i]=(i-1)/block_len+1;
    for(int i=1;i<=block_num;i++)st[i]=(i-1)*block_len+1,ed[i]=i*block_len;
    ed[block_num]=n;
    return;
}
//本题最恶心的也就是维护极长段了。
//维护方法不止一种,但都很恶心。
//一种实现:在每个极长段的最左/右边维护该段长度。
ll llen[N],rlen[N];
#define mp make_pair
#define fr first
#define sc second
stack<pair<int,pair<int,int>>>stk;
//使用栈进行操作撤销,用于回滚
ll ans[N];//每个块的答案
inline void insert(int x)
{
    //插入新点
    int ls=0,rs=0,id=bl[x];
    if(llen[x+1]&&bl[x+1]==bl[x]&&rlen[x-1]&&bl[x-1]==bl[x])
    {
        //左右同时相连
        int lp=x-1,rp=x+1;
        ans[id]-=(rlen[lp]*(rlen[lp]+1)/2)+(llen[rp]*(llen[rp]+1)/2);
        ls=lp-rlen[lp]+1,rs=rp+llen[rp]-1;
        llen[ls]+=llen[rp]+1;
        rlen[rs]+=rlen[lp]+1;
        llen[rp]=rlen[lp]=0;
        ans[id]+=(llen[ls]*(rlen[rs]+1)/2);
    }
    else if(llen[x+1]&&bl[x+1]==bl[x])
    {
        //右端相连
        int rp=x+1;
        ans[id]+=llen[rp]+1;
        rs=rp+llen[rp]-1;
        llen[x]=llen[rp]+1;
        rlen[rs]++;
        llen[rp]=0;
    }
    else if(rlen[x-1]&&bl[x-1]==bl[x])
    {
        //左端相连
        int lp=x-1;
        ans[id]+=rlen[lp]+1;
        ls=lp-rlen[lp]+1;
        rlen[x]=rlen[lp]+1;
        llen[ls]++;
        rlen[lp]=0;
    }
    else llen[x]=rlen[x]=1,ans[id]++;//完全不相连
    stk.push(mp(x,mp(ls,rs)));
    return;
}
inline void undo()
{
    //撤销操作
    int x=stk.top().fr,ls=stk.top().sc.fr,rs=stk.top().sc.sc;
    int id=bl[x];
    stk.pop();
    if(ls&&rs)
    {
        //从中间断开
        ans[id]-=llen[ls]*(rlen[rs]+1)/2;
        llen[ls]=x-ls;
        rlen[x-1]=x-ls;
        llen[x+1]=rs-x;
        rlen[rs]=rs-x;
        ans[id]+=(llen[ls]*(llen[ls]+1)/2)+(rlen[rs]*(rlen[rs]+1)/2);
    }
    else if(ls)
    {
        //去掉最右端
        ans[id]-=llen[ls];
        llen[ls]--,rlen[x-1]=rlen[x]-1,rlen[x]=0;
    }
    else if(rs)
    {
        //去掉最左端
        ans[id]-=rlen[rs];
        rlen[rs]--,llen[x+1]=llen[x]-1,llen[x]=0;
    }
    else llen[x]=rlen[x]=0,ans[id]--;//去掉独块
    return;
}
int X,Y;
inline ll query(int l,int r)
{
    ll ret=0,len=0;
    if(bl[r]-bl[l]<=1)
    {
        for(int i=l;i<=r;i++)
        {
            if(X<=a[i]&&a[i]<=Y)len++;
            else ret+=len*(len+1)/2,len=0;
        }
        return ret+len*(len+1)/2;
    }
    for(int i=l;i<=ed[bl[l]];i++)if(X<=a[i]&&a[i]<=Y)len++;else ret+=len*(len+1)/2,len=0;
    for(int i=bl[l]+1;i<bl[r];i++)
    {
        if(llen[st[i]]==block_len&&rlen[ed[i]]==block_len)len+=llen[st[i]];//整块都是,直接累加
        else
        {
            len+=llen[st[i]];
            ret+=len*(len+1)/2;
            ret+=ans[i]-llen[st[i]]*(llen[st[i]]+1)/2-rlen[ed[i]]*(rlen[ed[i]]+1)/2;//把开头和结尾要拼起来的部分去掉
            len=rlen[ed[i]];
        }
    }
    for(int i=st[bl[r]];i<=r;i++)if(X<=a[i]&&a[i]<=Y)len++;else ret+=len*(len+1)/2,len=0;
    return ret+len*(len+1)/2;
}
inline void clear()
{
    memset(llen,0,sizeof llen);
    memset(rlen,0,sizeof rlen);
    memset(ans,0,sizeof ans);
    while(!stk.empty())stk.pop();
    return;
}
struct query
{
    int id,l,r,x,y;
    bool operator<(query oth){if(bl[x]==bl[oth.x])return y<oth.y;return x<oth.x;}
}Q[N];
ll final_ans[N];
int main()
{
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)scanf("%d",a+i),r[a[i]]=i;
    for(int i=1;i<=q;i++)scanf("%d%d%d%d",&Q[i].l,&Q[i].r,&Q[i].x,&Q[i].y),Q[i].id=i;
    Build_Block();
    sort(Q+1,Q+q+1);
    int lst=0;
    for(int i=1;i<=q;i++)
    {
        int l=Q[i].l,r=Q[i].r,x=Q[i].x,y=Q[i].y;
        if(bl[x]!=lst)
        {
            clear();
            lst=bl[x];
            X=ed[lst]+1;
            Y=ed[lst];
        }
        if(bl[x]==bl[y])
        {
            for(int i=x;i<=y;i++)insert(::r[i]);
            swap(x,X),swap(y,Y),final_ans[Q[i].id]=query(l,r),swap(x,X),swap(y,Y);
            for(int i=x;i<=y;i++)undo();
            continue;
        }
        while(Y<y)Y++,insert(::r[Y]);
        while(X>x)X--,insert(::r[X]);
        final_ans[Q[i].id]=query(l,r);
        while(X<ed[lst]+1)undo(),X++;
    }
    for(int i=1;i<=q;i++)printf("%lld\n",final_ans[i]);
    return 0;
}
posted @ 2025-01-04 08:26  小山云盘  阅读(33)  评论(0)    收藏  举报