[AHOI 2013]作业(分块&莫队)

作业 [做题笔记]

题目描述

给定一个 \(n\) 和序列 \(S_1,S_2,S_3,\dots S_n\),给出 \(T\) 次询问,每次 \(l,r,a,b\),对于 \(i∈[l,r]\),求当 \(S_i∈[a,b]\) 时,\(i\) 的个数和 \(S_i\) 的个数。

\(n \leq 10^5 ,T \leq 10^6\)

做题历程 (废话可忽略)

首先记录一下机房语录:

当我询问机房dalao这题纯用分块可不可过:

@CuFeO4:这题用莫队套树状数组。

@xrlong:这题用莫队套分块。

\(for(int\ i=1;i<=114514;i++)\) \(\{\)

$\ \ \ \ $@CuFeO4:树状数组好写!

$\ \ \ \ $@xrlong:分块快!

\(\}\)

@xrlong:你树状数组容易被卡

@CuFeO4:我不管反正树状数组好写,而且这题没卡我

@xrlong:加强版数据卡树状数组

\(\dots \ \dots\)

但是我看着这个\(nb\)的题目,我想挑战一下用纯分块做法——

假做法1 时空复杂都可过,但是第二问想假了

我推了半节晚三,推出一个很真的假做法:

  • 对于第一问,规定 \(sumless[i][j]\) 为前\(i\)块比\(j\)小的数的个数,预处理 \(O(n \times tot)\),这个预处理很显然:

\[sumless[i][j]=sumless[i][j-1]+cnt[i][j] \]

要求 \([l,r]\) 中大于等于 \(a\),小于等于 \(b\),的数的个数,设 \(vl,vr\) 为左右端点所在块,那么大块中的结果:

\([vl+1,vr-1]\) 中小于 \(a\) 的:

\[A=sumless[vr-1][a-1]-sumless[vl][a-1] \]

\([vl+1,vr-1]\) 中小于等于 \(b\) 的:

\[B=sumless[vr-1][b]-sumless[vl][b] \]

则大块中结果:

\[res=B-A \]

然后再跑散块即可。显然第一问没有问题。

  • 第二问我如法炮制,规定 \(numless[i][j]\) 为前\(i\)块比\(j\)小的不一样数的种数,求法同上,但是显然假了。因为一个符合条件的数很可能在 \([1,vl]\)\([vl,vr]\) 中同时出现,而用上文直接相减显然会假。

假代码(记录我的愚蠢)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 100010
#define M 320
#define read read()
#define pt puts("")
inline int read
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-'0',c=getchar();
return f*x;
}
void write(int x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
return;
}
int n,T;
int s[N];
int len,tot,v[N],L[M],R[M];
int sumless[M][N];
int numless[M][N];
int cnt[M][N];
bool have[M][N];
void init()
{
len=sqrt(n);
tot=(n-1)/len+1;
for(int i=1;i<=tot;i++){
L[i]=R[i-1]+1;
R[i]=L[i]+len-1;
}
R[tot]=n;
for(int i=1;i<=tot;i++){
for(int j=L[i];j<=R[i];j++){
v[j]=i;
cnt[i][s[j]]++;
have[i][s[j]]=1;
}
for(int j=1;j<=n;j++){
cnt[i][j]+=cnt[i-1][j];
have[i][j]|=have[i-1][j];
}
}
for(int i=1;i<=tot;i++){
for(int j=1;j<=n;j++){
sumless[i][j]=sumless[i][j-1]+cnt[i][j];
numless[i][j]=numless[i][j-1]+have[i][j];
}
}
}
bool vis[N];
void query(int l,int r,int a,int b)
{
int sum=0,num=0;
int vl=v[l],vr=v[r];
if(vr-vl<=1){
for(int i=l;i<=r;i++){
if(s[i]>=a&&s[i]<=b){
sum++;
if(!vis[s[i]])
num++,vis[s[i]]=1;
}
}
for(int i=l;i<=r;i++) vis[s[i]]=0;
write(sum);putchar(' ');write(num);pt;
return;
}
int A,B;
A=sumless[vr-1][a-1]-sumless[vl][a-1];
B=sumless[vr-1][b]-sumless[vl][b];
sum=B-A;
A=numless[vr-1][a-1]-numless[vl][a-1];
B=numless[vr-1][b]-numless[vl][b];
num=B-A;
for(int i=l;i<=R[vl];i++){
if(s[i]>=a&&s[i]<=b){
sum++;
if(!(cnt[vr-1][s[i]]-cnt[vl][s[i]])&& !vis[s[i]])
num++,vis[s[i]]=1;
}
}
for(int i=r;i>=L[vr];i--){
if(s[i]>=a&&s[i]<=b){
sum++;
if(!(cnt[vr-1][s[i]]-cnt[vl][s[i]])&& !vis[s[i]])
num++,vis[s[i]]=1;
}
}
for(int i=l;i<=R[vl];i++) vis[s[i]]=0;
for(int i=r;i>=L[vr];i--) vis[s[i]]=0;
write(sum);putchar(' ');write(num);pt;
return;
}
signed main()
{
n=read,T=read;
for(int i=1;i<=n;i++) s[i]=read;
init();
int l,r,a,b;
while(T-->0)
{
l=read,r=read,a=read,b=read;
query(l,r,a,b);
}
return 0;
}

假做法2 牺牲空间,但由于看错 \(T\) 的范围,狂 \(T\) 不止

第一问同上,第二问我考虑用 \(num[i][j][k]\) 来表示第 \(i\) 块到第 \(j\) 块小于等于 \(k\) 的数的种数,空间 \(O(n \times tot^2)\),为了卡过 \(10^8\) ,算出块长可为 \(3000\) ,我本以为 \(T\)\(n\) 同一个数量级,时间 \(O(10^9)\) 可以卡过 \(10000ms\) 时限,然后 \(TLE\) ,我才发现

\[n \leq 10^5 ,T \leq 10^6 \]

for(int i=1;i<=tot;i++){
for(int j=1;j<=n;j++)
sumless[i][j]=sumless[i][j-1]+cnt[i][j];
for(int j=i;j<=tot;j++){
for(int k=1;k<=n;k++){
numless[i][j][k]=numless[i][j][k-1];
if(cnt[j][k]-cnt[i-1][k]) numless[i][j][k]++;
}
}
}

又挂了,于是我果断—— 学习莫队

正解(莫队&分块)

初学莫队,不得不赞叹,莫队太屌了,我们不再对原序列分块,我们对所有询问排序并分块,类似于一个区间从左往右扫,挨个解决每个询问

核心就这几行:

for(int i=1;i<=m;i++){
while(ql<q[i].l) del(s[ql++]);
while(ql>q[i].l) add(s[--ql]);
while(qr<q[i].r) add(s[++qr]);
while(qr>q[i].r) del(s[qr--]);
//到底是 ++ql,还是ql++,可以推一下原因
ans1[q[i].id]=query1(q[i].a,q[i].b);
ans2[q[i].id]=query2(q[i].a,q[i].b);
}

我们对值域分块,设:

  • \(cnt[x]\) 表示当前区间内 \(x\) 出现次数
  • \(spx1[i]\) 表示第 \(i\) 块中数的个数
  • \(spx2[i]\) 表示第 \(i\) 块中不同数的种数

修改即:

void add(int x)
{
if(!cnt[x]) spx2[v[x]]++;
spx1[v[x]]++;
cnt[x]++;
}
void del(int x)
{
cnt[x]--;
spx1[v[x]]--;
if(!cnt[x]) spx2[v[x]]--;
}

当我们跑到查询区间 \([q_i.l,q_i.r]\) 时,直接分块思想查询 \([q_i.a,q_i.b]\) 即可。

int query1(int a,int b)
{
int res=0;
int va=v[a],vb=v[b];
if(vb-va<=1){
for(int i=a;i<=b;i++) res+=cnt[i];
return res;
}
for(int i=va+1;i<=vb-1;i++)
res+=spx1[i];
for(int i=a;i<=R[va];i++) res+=cnt[i];
for(int i=b;i>=L[vb];i--) res+=cnt[i];
return res;
}
int query2(int a,int b)
{
int res=0;
int va=v[a],vb=v[b];
if(vb-va<=1){
for(int i=a;i<=b;i++) res+=(cnt[i]>0);
return res;
}
for(int i=va+1;i<=vb-1;i++)
res+=spx2[i];
for(int i=a;i<=R[va];i++) res+=(cnt[i]>0);
for(int i=b;i>=L[vb];i--) res+=(cnt[i]>0);
return res;
}

\(AC\ \ code\)

#include<bits/stdc++.h>
using namespace std;
#define N 100010
#define T 350
#define M 1000010
#define read read()
#define pt puts("")
inline int read
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-'0',c=getchar();
return f*x;
}
void write(int x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
return;
}
int n,m;
int s[N];
int len,tot,v[N],L[T],R[T];
int spx1[T],spx2[T];
int ans1[M],ans2[M];
int cnt[N];
struct Q{
int l,r,a,b,id;
}q[M];
bool cmp(Q a,Q b)
{
if(v[a.l]==v[b.l]) return a.r<b.r;
return a.l<b.l;
}
void init()
{
len=sqrt(n);
tot=(n-1)/len+1;
for(int i=1;i<=tot;i++){
L[i]=R[i-1]+1;
R[i]=L[i]+len-1;
}
R[tot]=n;
for(int i=1;i<=tot;i++)
for(int j=L[i];j<=R[i];j++)
v[j]=i;
}
void add(int x)
{
if(!cnt[x]) spx2[v[x]]++;
spx1[v[x]]++;
cnt[x]++;
}
void del(int x)
{
cnt[x]--;
spx1[v[x]]--;
if(!cnt[x]) spx2[v[x]]--;
}
int query1(int a,int b)
{
int res=0;
int va=v[a],vb=v[b];
if(vb-va<=1){
for(int i=a;i<=b;i++) res+=cnt[i];
return res;
}
for(int i=va+1;i<=vb-1;i++)
res+=spx1[i];
for(int i=a;i<=R[va];i++) res+=cnt[i];
for(int i=b;i>=L[vb];i--) res+=cnt[i];
return res;
}
int query2(int a,int b)
{
int res=0;
int va=v[a],vb=v[b];
if(vb-va<=1){
for(int i=a;i<=b;i++) res+=(cnt[i]>0);
return res;
}
for(int i=va+1;i<=vb-1;i++)
res+=spx2[i];
for(int i=a;i<=R[va];i++) res+=(cnt[i]>0);
for(int i=b;i>=L[vb];i--) res+=(cnt[i]>0);
return res;
}
signed main()
{
n=read,m=read;
for(int i=1;i<=n;i++) s[i]=read;
init();
for(int i=1;i<=m;i++){
q[i].id=i;q[i].l=read;q[i].r=read;q[i].a=read;q[i].b=read;
}
sort(q+1,q+m+1,cmp);
int ql=1,qr=0;
for(int i=1;i<=m;i++){
while(ql<q[i].l) del(s[ql++]);
while(ql>q[i].l) add(s[--ql]);
while(qr<q[i].r) add(s[++qr]);
while(qr>q[i].r) del(s[qr--]);
ans1[q[i].id]=query1(q[i].a,q[i].b);
ans2[q[i].id]=query2(q[i].a,q[i].b);
}
for(int i=1;i<=m;i++){
write(ans1[i]);
putchar(' ');
write(ans2[i]);
pt;
}
return 0;
}
posted @   lty_ylzsx  阅读(46)  评论(5编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示