← 返回题解列表

P1067 象群回响簿 官方题解

【部分分:测试点 1∼41\sim 41∼4】

  • 一个显然的思路是对每个询问重新扫描区间 [l,r][l,r],统计其中每个数字的出现次数。随后枚举出现过的不同数字 xx,检查其出现次数是否恰好为 xx
  • 使用数组或离散化后的计数表即可完成。总时间复杂度为 O(nm)\mathcal{O}(nm),空间复杂度为 O(n)\mathcal{O}(n)。当询问区间很长且询问数量很多时,重复统计成为瓶颈。

【部分分:测试点 5∼85\sim 85∼8】

  • 特殊性质 A 保证 ai20a_i\leq 20。对每个 1x201\leq x\leq 20,预处理数字 xx 的出现次数前缀和 px,ip_{x,i}
  • 对询问 [l,r][l,r],枚举 x=1,2,,20x=1,2,\ldots,20,检查 px,rpx,l1=xp_{x,r}-p_{x,l-1}=x。时间复杂度为 O(20(n+m))\mathcal{O}(20(n+m)),空间复杂度为 O(20n)\mathcal{O}(20n)

【部分分:测试点 9∼129\sim 129∼12】

  • 特殊性质 B 保证每个询问的长度不超过 100100。仍然使用第一档的直接统计,但只扫描询问区间中的至多 100100 个位置。
  • 处理一个询问后,只清空本次出现过的数字,避免每次清空完整值域。总时间复杂度为 O(100m)\mathcal{O}(100m),空间复杂度为 O(n)\mathcal{O}(n)

【正解】

  • 前几档的做法仍然会为不同询问重复统计公共部分。考虑到相邻询问往往有很长的公共区间,我们可以使用莫队调整当前区间。按照左端点所在块排序,同一块内再按照右端点排序。使用奇偶块反向排序右端点可以进一步减少指针移动。

  • cxc_x 表示数字 xx 在当前区间中的出现次数,令 AnsAns 表示满足 cx=xc_x=x 的不同数字数量。因为区间长度不超过 nn,所有 x>nx>n 都不可能成为回响数字,可以直接忽略。

  • 加入一个值 xx 前,若 cx=xc_x=x,先令 AnsAns1Ans\gets Ans-1。随后令 cxcx+1c_x\gets c_x+1,若新的 cx=xc_x=x,再令 AnsAns+1Ans\gets Ans+1。删除一个值时按照完全相同的顺序先撤销旧贡献、修改计数、再加入新贡献。

  • 每次指针移动只进行常数次操作,因此块长取 Θ(n)\Theta(\sqrt n) 时,单组数据的时间复杂度为 O((n+m)n)\mathcal{O}((n+m)\sqrt n),空间复杂度为 O(n+m)\mathcal{O}(n+m)。多组数据之间只需清空出现过的计数与询问答案。

  • 还可以按照询问右端点离线处理。将数字 xx 在前缀 [1,r][1,r] 中的出现位置依次记为 px,1,px,2,,px,cxp_{x,1},p_{x,2},\ldots,p_{x,c_x},并令 px,0=0p_{x,0}=0。若 cxxc_x\geq x,那么区间 [l,r][l,r] 中数字 xx 恰好出现 xx 次,当且仅当

px,cxx<lpx,cxx+1.p_{x,c_x-x}<l\leq p_{x,c_x-x+1}.
  • 因此,在右端点 rr 固定时,数字 xx 会对左端点区间
[px,cxx+1, px,cxx+1][p_{x,c_x-x}+1,\ p_{x,c_x-x+1}]

中的每一个位置贡献 11。所有数字的贡献相加,就是询问 [l,r][l,r] 的答案。

  • 扫描到位置 rr 时,只有 x=arx=a_r 的出现次数发生变化。若加入 rr 以前 xx 已经产生贡献,就先把旧的左端点区间减去 11,随后记录新的出现位置,再把新的左端点区间加上 11。由于区间长度不超过 nnx>nx>n 时可以直接忽略。
  • 使用树状数组维护左端点上的差分。对区间 [L,R][L,R] 加上 vv,只需在 LL 处加上 vv,在 R+1R+1 处减去 vv。将询问按照右端点归类,扫描到 rr 后,查询位置 ll 的前缀和即可得到 [l,r][l,r] 的答案。
  • 每次加入元素至多产生两次区间修改,每个询问只进行一次单点查询。单组数据的时间复杂度为 O((n+m)logn)\mathcal{O}((n+m)\log n),空间复杂度为 O(n+m)\mathcal{O}(n+m)

【参考代码】

#include <cstdio>#include <vector> #define MAXN 100010 using namespace std; int c,T,n,m,Tree[MAXN]; void Add(int Pos,int Val){    for(int i=Pos;i<=n+1;i+=i&-i) Tree[i]+=Val;} int Query(int Pos){    int Ret=0;    for(int i=Pos;i;i-=i&-i) Ret+=Tree[i];    return Ret;} void Modify(int l,int r,int Val){    if(l>r) return;    Add(l,Val);    Add(r+1,-Val);} void Solve(){    scanf("%d%d",&n,&m);    vector <int> Line(n+1),Ans(m+1);    vector <vector<int> > Pos(n+1);    vector <vector<pair<int,int> > > Ask(n+1);    for(int i=1;i<=n+1;i++) Tree[i]=0;    for(int i=1;i<=n;i++) scanf("%d",&Line[i]);    for(int i=1,l,r;i<=m;i++){        scanf("%d%d",&l,&r);        Ask[r].push_back(make_pair(l,i));    }    for(int r=1;r<=n;r++){        int x=Line[r];        if(x<=n){            int Count=Pos[x].size();            if(Count>=x){                int l=(Count==x?1:Pos[x][Count-x-1]+1);                int rr=Pos[x][Count-x];                Modify(l,rr,-1);            }            Pos[x].push_back(r);            Count++;            if(Count>=x){                int l=(Count==x?1:Pos[x][Count-x-1]+1);                int rr=Pos[x][Count-x];                Modify(l,rr,1);            }        }        for(int i=0;i<(int)Ask[r].size();i++){            Ans[Ask[r][i].second]=Query(Ask[r][i].first);        }    }    for(int i=1;i<=m;i++) printf("%d\n",Ans[i]);} int main(){    scanf("%d%d",&c,&T);    while(T-->0) Solve();    return 0;}