P7261 题解
zhangbo1000 · · 题解
蒟蒻依旧想写丰富的开头,可是没有能力。
update 2024.3.21 重新提交以去除旧博客分类。
update 2024.1.20
-
去除了错误的算法描述,重构了对根号分治的分析。
-
微调了代码。
update 2024.1.1 :
- 微调了讲解和代码,因为此题块长取
2000 虽然实际运行快但复杂度不正确,可以被 hack。 - 修改了复杂度说明,因为查询区间大小的上界应为
2 倍块长。
题目传送门
加强版及双倍经验
MnZn 做完题发现自己的做法(根号分治)与其他人(至少已有题解)不同,于是有了这篇。
对于这道题,先考虑暴力,发现有两种做法:(这两种暴力的顺序只是为了接下来的讨论。)
-
开桶暴力统计
[l,r] 区间内每种数出现次数,再暴力扫统计答案。 -
给每个数维护出现次数前缀和,这样就能
\Theta(1) 统计一个数在区间内的出现次数,再暴力扫一遍所有数,统计答案。
可以看出,前一种慢主要是因为区间太长,后一种慢主要是因为统计的数太多。
那么我们可以根号分治平衡复杂度,在区间较短时直接用第一种暴力,区间长时则可能成为答案的在整个数列内出现次数较多,因此需要维护出现次数前缀和的数也就少了,可以第
-
当
r-l+1\le 2b 时,第一种暴力的最坏复杂度为\Theta(b) 。 -
当
r-l+1>2b 时,则出现次数超过\frac{r-l+1}{2} 的数至少出现了b 次,这样的数最多只有\frac{n}{b} 个,第二种暴力最坏\Theta(\frac{n}{b}) 。
因此总复杂度为
从数学角度来说,有基本不等式可知
但实际实现中,我们发现这个算法的空间复杂度为
代码:(码风炸裂)
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<map>
using namespace std;
#define M 100001
#define N 300001
#define B 1000
int s[N/B+1][N],p[N/B];
int q[10001],a[N];
bool jc[10001];
int c[10001];
inline void read(int& x){
static char c;
while((c=getchar())<'0');
x=c^'0';
while((c=getchar())>='0')x=(x<<3)+(x<<1)+(c^'0');
}
int main(){
int n,m,uke;
read(n);
read(uke);
int top=0;
for(int i=1;i<=n;i++){
read(a[i]);
c[a[i]]++;
}
for(int i=1;i<=uke;i++){
if(c[i]>=B)p[++top]=i;
}
for(int i=1;i<=top;i++){
for(int j=1;j<=n;j++){
s[i][j]=s[i][j-1]+(a[j]==p[i]?1:0);
}
}
int l,r;
read(m);
for(int ttt=0;ttt<m;ttt++){
bool flag=0;
read(l);
read(r);
if(r-l+1<=(B*2)){
for(int i=l;i<=r;i++){
q[a[i]]++;
}
for(int i=l;i<=r;i++){
if((q[a[i]]<<1)>(r-l+1))printf("yes %d\n",a[i]),flag=1;
q[a[i]]=0;
}
if(!flag)printf("no\n");
}
else{
for(int i=1;i<=top;i++){
if(((s[i][r]-s[i][l-1])<<1)>(r-l+1)){
printf("yes %d\n",p[i]);
flag=1;
break;
}
}
if(!flag)printf("no\n");
}
}
return 0;
}