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})\) 查询的数据结构,就可以以更优的时间复杂度解决。
正好,我们还真能找到。
做序列分块,每个块内维护答案即可。
维护你可以考虑很多种实现,有链表、并查集等等。
这里我是从同级大佬那里学来的做法,对于每个极长段,在其左端点和右端点记录一下,这样就可以写出下面代码里的 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;
}

浙公网安备 33010602011771号