题目
题目大意
给你一个数列,有很多个询问,询问一段区间内,某个数乘它的出现次数的最大值,也就是带权众数。
思考历程
第一次看到这道题,立马想到了树套树之类的二位数据结构,发现不行。(就算可以也很难打……)
然后我就想到了莫队!
其实这题的莫队是很显然的。我们用莫队的方法来搞,用一个数据结构来维护目前的答案。
所以我就打出来了时间复杂度为的做法。
还挺好打的。
交上去之后,我发现,诶,怎么运行这么久?难道是被卡了?
后来看到分数之后……WA,0分。
什么鬼?
然后我就惊奇地发现这题要开long long
……
开了之后,诶,40分?
怎么还是这么低的分数(某YMQ用这种方法AC了这题)?
检查程序,发现自己莫队的排序相当于没有排序。
我们知道朴素的莫队做法就是将左端点所在的块为第一关键字,以右端点为第二关键字来排的。
比较函数一般设为:be[x.l]<be[y.l] || be[x.l]==be[y.l] && x.r<y.r
然后我把||
打成了&&
。
感觉自己心态爆炸……这样子和没有排序有什么区别?
改过来之后,60分,嗯,题解分数。
但我不甘心,因为还有两个点是WA!
经过调试之后,我发现是数组越界了,再改改就80分了。
可是还没到100分,因为这不是正解。但YMQ已经卡到100分了,我才不信这个邪。
所以呢,我又卡了一下:
首先将普通的线段树转化为zkw线段树(因为只有单点修改和整体查询,所以还是比较简单的),然后用宏来打了max
函数,因为自带的那个是挺慢的。
然后就卡过了!比YMQ快!看看YMQ的程序,嘿嘿,还开了O3……
YMQ日常吸臭氧……
正解
目前我知道的正解有两种,时间复杂度都是,少了一个
第一种是XZB大佬首创的莫队加桶维护的方法:
莫队的部分是一模一样的,重点是桶。
我们知道莫队排序的时候,按照左端点所在的块为第一关键字,以右端点为第二关键字。
显然,当左端点所在块一样的时候,右端点是递增的。
我们在处理的时候,用桶记录一下当前块后面的信息。
然后,对于在块内的那一部分区间,我们就暴力计算。计算了之后合并两边的信息,求出这一问的答案。
现在有一个问题,怎么合并呢?
这个问题困扰了我一段时间。然后,我发现,反正这个时间复杂度就这样了,所以暴力一点没有关系。
我们先不要理在块中的部分,先统计好块后面的信息,记录这时的带权众数。
然后将块内的数加进桶中,做完之后这时的带权众数就是答案。
最后,我们将桶还原,就是将原来属于这个块内的全部剔除,带权众数还是之前的那个带权众数。
这个是我自己脑补出来的想法,可能其他人还有更加优秀的方法吧。
一直怎么做下去,如果一个块处理完了,就将桶清空,然后继续处理下一个块。
时间复杂度是显然的。
然后还有一种做法叫作分块,我还没有打过,只是思想理解了而已。
我们可以用和上面有点类似的思想:将一段区间内的东西用桶存起来,存下此时的答案,然后再试着将区间外的东西暴力加进去,得出目前的答案,最后还原。
我们可以分成个块,对于每个块,我们将这个块之前的所有东西放在一个桶中。(就像是前缀和)
然后,我们对于每两个块之间的部分,预处理出它们的带权众数。(用一个的一个数组来存就好,预处理的时候直接枚举从哪个块开始,然后往后面扫,时间)
询问一个区间的时候,这个区间被拆成一个大块和两个散块。对于大块,我们已经预处理除它的带权众数,并且我们通过相减的方式得出一个新的桶。对于散块,我们将里面的元素暴力加进桶中,得出最终的带权众数。
然后你会惊奇地发现,如果真的是这么做,那肯定TLE。
为什么?其实耗费时间的就在一个地方:我们将两个桶相减得出一个新的桶,实际上没有必要。
设后面的桶为,前面的桶为,那么你可以新建一个桶。一开始是全零的。
然后,在后面加数的时候,“新桶”相当于是。在加某个数的时候,我们只需要修改,用来统计答案。做完了之后,将这些数从中一一减去,然后就清零了(如果直接清零是会TLE的)。
这个时间复杂和上面的一样,不过感觉上面的好打一些。
代码
的水法
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define N 252144
int n,m,col;
int a[N+1],p[N+1],b[N+1];//a表示原数组,b表示离散化后的数组
inline bool cmpp(const int x,const int y){
return a[x]<a[y];
}
int K;
int be[N+1];//表示所在的块
struct Oper{
int l,r;
int num;
} o[N+1];
inline bool cmp(const Oper &x,const Oper &y){
return be[x.l]<be[y.l] || be[x.l]==be[y.l] && x.r<y.r;//比较函数千万不要打错了……我当时就是在这里GG的
}
long long t[N*2+1];
int M;
inline void add(int,int);
long long ans[N+1];
int main(){
scanf("%d%d",&n,&m);
K=sqrt(n);
for (int i=1;i<=n;++i)
scanf("%d",&a[i]),p[i]=i;
//以下是离散化
sort(p+1,p+n+1,cmpp);
for (int i=1,bef=0;i<=n;++i){
if (a[p[i]]!=bef)
bef=a[p[i]],col++;
b[p[i]]=col;
}
for (M=1;M<col;M<<=1);
for (int i=1;i<=m;++i)
scanf("%d%d",&o[i].l,&o[i].r),o[i].num=i;
for (int i=1;i*K<=n;++i)
for (int j=0;j<K && i*K+j<=n;++j)
be[i*K+j]=i;
//以下是莫队
sort(o+1,o+m+1,cmp);
int l=1,r=0;
for (int i=1;i<=m;++i){
for (;r<o[i].r;r++)
add(r+1,1);
for (;l>o[i].l;l--)
add(l-1,1);
for (;r>o[i].r;r--)
add(r,-1);
for (;l<o[i].l;l++)
add(l,-1);
ans[o[i].num]=t[1];
}
for (int i=1;i<=m;++i)
printf("%lld
",ans[i]);
return 0;
}
#define my_max(x,y) (((x)>(y))?(x):(y))
inline void add(int x,int c){//zkw线段树中的
int k=b[x]+M;
t[k]+=c*a[x];
for (k>>=1;k;k>>=1)
t[k]=my_max(t[k<<1],t[k<<1|1]);
}
的莫队加桶做法
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define N 252144
int n,m,col;
int a[N+1],p[N+1],b[N+1];
inline bool cmpp(const int x,const int y){
return a[x]<a[y];
}
int K;
int be[N+1],rig[N+1];
struct Oper{
int l,r;
int num;
} o[N+1];
inline bool cmp(const Oper &x,const Oper &y){
return be[x.l]<be[y.l] || be[x.l]==be[y.l] && x.r<y.r;
}
long long buc[N+1],rmx;
long long ans[N+1];
#define MAX(a,b) (((a)>(b))?(a):(b))
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d%d",&n,&m);
K=sqrt(n);
for (int i=1;i<=n;++i)
scanf("%d",&a[i]),p[i]=i;
sort(p+1,p+n+1,cmpp);
for (int i=1,bef=0;i<=n;++i){
if (a[p[i]]!=bef)
bef=a[p[i]],col++;
b[p[i]]=col;
}
for (int i=1;i<=m;++i)
scanf("%d%d",&o[i].l,&o[i].r),o[i].num=i;
for (int i=0;1+i*K<=n;++i)
for (int j=1;j<=K && i*K+j<=n;++j)
be[i*K+j]=i+1;
be[0]=0;
for (int i=1;i<=n;++i)
rig[be[i]]=i;
sort(o+1,o+m+1,cmp);
for (int i=1,r=0;i<=m;++i){
if (be[o[i-1].l]!=be[o[i].l]){
memset(buc,0,sizeof buc);
rmx=0;
r=rig[be[o[i].l]]+1;
for (;r>o[i].r;--r)
buc[b[r-1]]-=a[r-1];//防止左端点和右端点在同一个块中的情况
}
for (;r<=o[i].r;++r)
buc[b[r]]+=a[r],rmx=MAX(rmx,buc[b[r]]);//右端点往外延伸
long long tmx=rmx;//由于等一下要还原,所以这个值要暂时记录
for (int l=rig[be[o[i].l]];l>=o[i].l;--l)
buc[b[l]]+=a[l],tmx=MAX(tmx,buc[b[l]]);//将块内的暴力记录在桶中
ans[o[i].num]=tmx;
for (int l=rig[be[o[i].l]];l>=o[i].l;--l)//还原
buc[b[l]]-=a[l];
}
for (int i=1;i<=m;++i)
printf("%lld
",ans[i]);
return 0;
}
其实这题的数据很水……我一开始的正解程序并没有判断左右端点在同一个块中的特殊情况,可我还是AC了。
总结
如果见到一些用做法不好维护的东西,那就试一下分块和莫队。
从这题当中,我们也得到了一个用来优化莫队的思想,就是将块后和块中的分别考虑,有时会有非常好的成果。
最后就是,卡常技巧很重要,说不定你可以用次解来AC这道题。不要像YMQ一样天天吸臭氧。