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})\) 查询的数据结构,就可以以更优的时间复杂度解决。
正好,我们还真能找到。
做序列分块,每个块内维护答案即可。
维护你可以考虑很多种实现,有链表、并查集等等。
这里我是从同级大佬那里学来的做法,对于每个极长段,在其左端点和右端点记录一下,这样就可以写出下面代码里的 insert
和 undo
了。
然后这样维护有缺陷,就是删去一个数没法搞。
实际很好解决,外层的莫队改为回滚莫队即可。
那么这道题就解决了,据说还有双倍经验。
#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;
}