莫队(学习笔记)
推荐一篇莫队算法的博客!!!
莫队其实是一种将所有询问离线的算法,它巧妙地利用了每个询问之间的关系,从而优化时间复杂度.另外,莫队基于分块的思想.
基础莫队
题目描述
小Z有N只袜子,从1到N编号,每只袜子有一个颜色\(C_i\),有M次询问,每次询问[L,R],回答区间[L,R]内,小Z有多大的概率抽到两只颜色相同的袜子.
然而数据中有L=R的情况,请特判这种情况,输出0/1。
输出包含M行,对于每个询问在一行中输出分数A/B表示从该询问的区间[L,R]中随机抽出两只袜子颜色相同的概率。若该概率为0则输出0/1,否则输出的A/B必须为最简分数。
这里谈一下概率的求法:
假设有a,b,c......n中颜色,则
(分子分母的算法类比n只球队,两两进行一场比赛的总场数)
分子为:
\(a*(a-1)/2+b*(b-1)/2+c*(c-1)/2+......\)
分母为:
\((R-L+1)*(R-L)/2\)
化简得:
分子:\(a^2+b^2+c^2+...-(a+b+c+...)\)
=\(a^2+b^2+c^2+...-(R-L+1)\)
分母:\((R-L+1)*(R-L)\)
int n,m,t;
int color[50005],place[50005];
long long ans,sum[50005];
struct mo{
int l,r,id;
long long a,b;
}qu[50005];
bool cmp1(mo x,mo y){
return place[x.l]==place[y.l]?x.r<y.r:x.l<y.l;
}
bool cmp2(mo x,mo y){
return x.id<y.id;
}
long long SUM(long long x){return x*x;}
void turn(int x,int y){
ans-=SUM(sum[color[x]]);
sum[color[x]]+=y;
ans+=SUM(sum[color[x]]);
}
//这里ans记录的是上面提到的累加和,不是整个概率分数
//以y=-1为例:
//对于颜色color[x],它在ans中由sum[color[x]]的平方
//变为sum[color[x]-1]的平方,为了方便处理
//直接先减掉原来的sum[color[x]]的平方
//然后sum[color[x]]-1变为当前的贡献
//再把其平方值加入ans中
long long GCD(long long x,long long y){
if(y==0)return x;
return GCD(y,x%y);
}
int main(){
n=read();m=read();
t=sqrt(n);//分块
for(int i=1;i<=n;i++){
place[i]=i/t+1;
}//place数组记录第i只袜子所属的块
for(int i=1;i<=n;i++){
color[i]=read();
}//color数组记录第i只袜子的颜色
for(int i=1;i<=m;i++){
qu[i].l=read();
qu[i].r=read();
qu[i].id=i;
}
//把m个询问一起用结构体记录下来,转为离线
//别忘了要把每个询问编号,以便于最后按照顺序输出
sort(qu+1,qu+m+1,cmp1);
//把每个询问按照左端点从小到大排序
//莫队的核心开始了!!!
int l=1,r=0;
for(int i=1;i<=m;i++){
while(l<qu[i].l){turn(l,-1);l++;}
//l指针小于当前询问区间的左端点,
//就要把l位置上的颜色减1,指针位置向右挪.
while(l>qu[i].l){turn(l-1,1);l--;}
while(r<qu[i].r){turn(r+1,1);r++;}
while(r>qu[i].r){turn(r,-1);r--;}
//想象一下l,r两个指针跳来跳去的名场面
if(qu[i].l==qu[i].r){
qu[i].a=0;qu[i].b=1;
continue;
}//特判l=r的情况
qu[i].a=ans-(qu[i].r-qu[i].l+1);
qu[i].b=1LL*(qu[i].r-qu[i].l+1)*(qu[i].r-qu[i].l);
long long gcd=GCD(qu[i].a,qu[i].b);
qu[i].a/=gcd;qu[i].b/=gcd;
}
sort(qu+1,qu+m+1,cmp2);
//恢复原来询问的顺序输出结果
for(int i=1;i<=m;i++){
printf("%lld/%lld\n",qu[i].a,qu[i].b);
}
return 0;
}
带修莫队
多次区间询问,询问区间\([l,r]\)中不同颜色的种类数。可以单点修改颜色。
带修莫队在普通莫队的基础上多了第三个指针t,记录时间,这个时间是指修改的时间(次数)
借助我们记录的修改时间,每一次询问不仅要让l,r两个指针跳来跳去(跳到正确的区间内),还要保证当前已经修改到此次询问发生的时间(询问和修改是交错发生的,所以我们要查询此次问题的结果,首先要保证当前修改的时间/次数指针t跳到了当前询问应在的查询次数时)
int n,m,unit,num,t,l=1,r,Time,Ans;
int color[50005],now[50005],place[50005];
int sum[1000005],ans[50005];
struct query{
int l,r,tim,id;
}qu[50005];
//一个结构体记录问题(询问区间),记录信息有:
//左端点,右端点,询问时间(带修莫队的特色),编号
struct change{
int pos,New,Old;
}ch[50005];
//这个结构体是为修改操作量身打造的
//pos位置编号,New修改后的颜色,Old修改前的颜色
bool cmp(query a,query b){
return place[a.l]==place[b.l]?(place[a.r]==place[b.r]?a.tim<b.tim:a.r<b.r):a.l<b.l;
}按照三个关键字l>r>tim排序('>'在这里指优先于)
void turn(int col,int d){
sum[col]+=d;
if(d>0)Ans+=sum[col]==1;
//等价于if(d>0&&sum[col]==1)Ans+=1;
//如果d>0(即d=1),sum[col]+1之后才等于1,
//说明颜色col之前对答案还未产生贡献,此时Ans+1
if(d<0)Ans-=sum[col]==0;//同理
}
void going(int x,int col){
if(l<=x&&x<=r){turn(col,1);turn(color[x],-1);}
//如果这一次修改的位置在询问区间内
//就把修改后的颜色加1,修改前(被修改)的颜色减1
color[x]=col;
//更新x位置上的颜色
}
int main(){
n=read();m=read();
unit=pow(n,0.666666);
//本题中这样分块最好,这个是需要数学能力的
for(int i=1;i<=n;i++){
color[i]=read();
now[i]=color[i];
place[i]=i/unit+1;
}
//新增了一个now数组来记录当前的颜色
for(int i=1;i<=m;i++){
char op;int a,b;
cin>>op;a=read();b=read();
if(op=='Q'){qu[++num]=(query){a,b,Time,num};}
//询问操作:num询问编号,在第Time次修改之后的询问
if(op=='R'){ch[++Time]=(change){a,b,now[a]};now[a]=b;}
//修改操作:Time修改次数,a位置修改为颜色b
//now[a]修改为当前的颜色b
}
sort(qu+1,qu+num+1,cmp);
//对这num个询问排序,开始莫队
for(int i=1;i<=num;i++){
while(t<qu[i].tim){going(ch[t+1].pos,ch[t+1].New);t++;}
//如果当前的修改次数t在 此次询问前已修改次数tim 之前
while(t>qu[i].tim){going(ch[t].pos,ch[t].Old);t--;}
while(l<qu[i].l){turn(color[l],-1);l++;}
while(l>qu[i].l){turn(color[l-1],1);l--;}
while(r<qu[i].r){turn(color[r+1],1);r++;}
while(r>qu[i].r){turn(color[r],-1);r--;}
ans[qu[i].id]=Ans;
}
for(int i=1;i<=num;i++){
printf("%d\n",ans[i]);
}
return 0;
}
副本(尚未开启):树上带修莫队(太蒻了!!!)