[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 @ 2024-03-21 14:56  lty_ylzsx  阅读(43)  评论(5编辑  收藏  举报