题目链接:codeforces 220B
题意分析:有n个数,m次询问,一开始给你这n个数ai,每一次询问的时候给你一个区间,求这个区间里面满足“数值为x,出现了x次”的条件的数有几个!(1 ≤ n, m ≤ 100000) 从题述上看,这题的难点应该是区间的频繁访问,于是可以想到线段树或者树状数组
解题思路:
即便是线段树或者树状数组这种高端数据结构,有时候也是需要一些辅助算法的,在这道题里面,辅助算法(思想)就是离线思想!
离线思想:其实很简单的一种思想,就是回应查询的时候不是一次查询一次回复,而是多次查询一次回复。那么这种思想究竟对我们解题有什么帮助呢?
我们来考虑用树状数组怎么解决这道题,首先,找到的数x,一定满足 x ≤ n。假设我们要查询一个区间[ l , r ],那么是不是如果我们事先知道了区间上点 i 到 r 这个区间有多少个数,就会方便很多?好像是会方便很多,但是好像会重复计算,那么我们就考虑这个点 i 在区间内是否是满足要求的数 x 。好像有点头绪了~
首先定义一个数组x,还有bit数组,是x的树状数组,另外我们需要一个vector(动态数组),vec[i]记录数字 i 在什么位置出现过:
从头开始遍历数组a,如果ai ≤ n则将其push到vec[ai]中,此时我们来检测一下vec[ai]的大小:
1)如果size ≥ ai,说明出现了一个满足题意的区间了,我们需要标记!于是,执行操作:x[ vec[ ai ][ size-ai ] ] = 1,表示从vec[ai][size-ai]那里到当前的位置 i,ai这个数满足条件。
2)如果size ≥ ai+1,说明我们之前已经将vec[ai][size-ai-1]这个位置标记了,这个点到当前位置 i 明显是有ai+1个ai的,我们需要订正它——x[ vec[ ai ][ size-ai ] ] = -1。(之所以是-1,是因为在[ vec[ai][size-ai-1] , i ]这个区间中,ai不是满足条件的数x了!)
3)如果size ≥ ai+2,我们又要把刚刚订正的地方修正回来——x[ vec[ ai ][ size-ai ] ] = 0。
因为,我们每次考虑问题都是以当前的 位置 i 为右界,那么为了节省时间,我们就应该对查询从左到右处理,在一切开始之前对查询按右边界进行排序。这就是所谓的离线处理。
每一次处理完一个数 i 之后,看看它是不是查询的右边界,是的话就用树状数组求和一下。
然后更新信息部分:
int tmp=a[i],len=vec[tmp].size()+1;
vec[tmp].push_back(i);
if(len>=tmp)
{
add(vec[tmp][len-tmp],1);
if(len>tmp)
add(vec[tmp][len-tmp-1],-2);
if(len>tmp+1)
add(vec[tmp][len-tmp-2],1);
}
最后,按查询给出的顺序输出查询结果就行了!
AC代码:
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
int n,m,a[100005],bit[100005],ans[100005];
vector<int> vec[100005];
struct node{
int l,r,index;
}q[100005];
bool cmp(node x,node y){
return x.r<y.r;
}
int lowbit(int num){
return num&(-num);
}
int sum(int index)
{
int res=0;
for(int i=index;i>0;i-=lowbit(i))
res+=bit[i];
return res;
}
void add(int index,int delta){
for(int i=index;i<=n;i+=lowbit(i))
bit[i]+=delta;
}
int main()
{
while(~scanf("%d %d",&n,&m))
{
memset(bit,0,sizeof(bit));
memset(vec,0,sizeof(vec));
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=0;i<m;i++)
{
scanf("%d %d",&q[i].l,&q[i].r);
q[i].index=i;
}
sort(q,q+m,cmp);
int j=0;
for(int i=1;i<=n;i++)
{
if(a[i]<=n)
{
int tmp=a[i],len=vec[tmp].size()+1;
vec[tmp].push_back(i);
if(len>=tmp)
{
add(vec[tmp][len-tmp],1);
if(len>tmp)
add(vec[tmp][len-tmp-1],-2);
if(len>tmp+1)
add(vec[tmp][len-tmp-2],1);
}
}
while(j<m&&q[j].r==i)
{
ans[q[j].index]=sum(q[j].r)-sum(q[j].l-1);
j++;
}
}
for(int i=0;i<m;i++)
printf("%d\n",ans[i]);
}
return 0;
}
总结:
1、离线思想,灵活运用可以提高算法复杂度;
2、树状数组是辅助工具,用来快速求和的!