浅谈根号算法----分块
___new2zy___ · · 个人记录
浅谈根号算法----分块
------谨以此篇纪念我可怜的分块水准
不定期更新
由于要
尤其是一些非常神奇优美(玄学)的暴力非常实用
所以最近打算学习一下分块和莫队
(也是为了好打暴力)(大雾)
这些东西我打算花
不多说了,直接看好了
分块简介:
分块概述和大致思想:
对于一个长度为
如果我们要更新一段区间
之后我们再统一更新中间的那些整块(打标记),又因为整块最多
那么修改操作的总复杂度即为
又由均值不等式可知,显然当
对区间查询的时候也可以按上述方法处理
由此可见,分块算法是一种通过适当的划分,预处理一些信息并保留,用空间换时间,达到时空平衡的一种算法,一般情况下均摊时间复杂度为
分块算法中利用了均值不等式取得了算法的最优性,因此我们也可以说,分块算法是一种基于根号平衡的算法
注意:根号平衡的算法都自带二倍常数!
这也是为什么分块往往能保证正确性,但是执行的效率很低很低的原因
分块优点及比较
对于很多的数列操作问题,线段树和树状数组等数据结构都是可以实现的,其中典型的,树状数组基于二进制倍增思想,而线段树则基于分治思想。
二者之所以能非常快速、高效的处理信息和执行命令,正是因为它们都将序列中的元素分成大大小小的“段”,花费代价来维护那些成段的信息,从而可以通过这些“段”来快速合并出所有区间的信息
作为一名成熟的
当要求维护一些复杂的信息时(不满足区间可加可减性的信息,比如区间众数),二者处理起来显得非常吃力(代码又臭又长)。
这时,分块就显现出了优势(其实也没有多大)
分块的适用范围更广,能解决的问题更多,相对树状数组和线段树等数据结构而言更加通用和容易实现,没有所维护信息特性的限制,但是通常情况下,分块的效率往往比不上树状数组和线段树,这也是它的缺点所在
事实上,分块更接近于“朴素”,我们也可以称它是一种“优美的暴力”,几乎所有的分块都可以形容为“大块维护,小段朴素”
分块的本质:
分块的本质,其实是一颗度数为
如下图:
其实,第一层没有用,真正操作我们只用到了第二层和第三层
其中第二层有
每次我们维护和查询信息时,只使用了第二层的整块信息和第三层的部分节点信息
更新时,自底而上更新信息
这大概就是分块的本质了
这上面的东西都很好理解吧~~~下面就来热热身好了
===============================================================
新鲜热乎的分块例题:(未完待续)
ex1:P3372 【模板】线段树 1
题目传送门:
https://www.luogu.org/problemnew/show/P3372
诶?这不是线段树题吗?为啥要用分块啊?
看我用线段树、树状数组、Splay、Theap、珂朵莉树秒切了这题!(大雾)
毕竟是在讲分块,我们不妨尝试用分块解决一下这个经典问题
不妨将整个序列
(我太菜了画不出图,请脑补)
我们预处理出
预处理
再设
对于指令“
2.(零散块操作)对开头和结尾两段不足一整块的部分,按照
那么以上这些就是修改操作
对于查询操作“
2.(零散块操作)对开头和结尾两段不足一整块的部分,按照
那么以上这些就是查询操作
综上,我们设计的分块算法采用了“整块修改用
又由于我们分块的长度和分块的个数都是
那么放一下代码
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
#define re register
using namespace std;
typedef long long ll;
const int inf=1e9+7;
inline ll read()
{
ll p=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
return 1ll*f*p;}
const int maxn=100003;
int n,Q,tot,L[maxn],R[maxn],bel[maxn];
ll A[maxn],sum[maxn],tag[maxn];
inline void pre_divide()//分块
{
tot=sqrt(n);
for(re int i=1;i<=tot;i++)
L[i]=(i-1)*sqrt(n)+1,R[i]=i*sqrt(n);
if(R[tot]<n)tot++,L[tot]=R[tot-1]+1,R[tot]=n;
for(re int i=1;i<=tot;i++)
for(re int j=L[i];j<=R[i];j++)
bel[j]=i,sum[i]+=A[j];
}
inline void change(int l,int r,ll k)//给区间[l,r]加上k
{
int p=bel[l],q=bel[r];
if(p==q)//同一块内暴力修改
{
for(re int i=l;i<=r;i++)
A[i]+=k;
sum[p]+=(ll)k*(r-l+1);
return ;
}
for(re int i=p+1;i<=q-1;i++)tag[i]+=k;
//中间块不修改,打上标记
//维护左边残余
for(re int i=l;i<=R[p];i++)A[i]+=k;
sum[p]+=(ll)k*(R[p]-l+1);
//维护右边残余
for(re int i=L[q];i<=r;i++)A[i]+=k;
sum[q]+=(ll)k*(r-L[q]+1);
}
inline ll query(int l,int r)//查询[l,r]的和
{
int p=bel[l],q=bel[r];
ll ans=0;
if(p==q)//同一块内暴力查询
{
for(re int i=l;i<=r;i++)
ans+=A[i];
ans+=tag[p]*(r-l+1);
return ans;
}
for(re int i=p+1;i<=q-1;i++)
ans+=sum[i]+tag[i]*(R[i]-L[i]+1);
//整块的信息
//左边残余
for(re int i=l;i<=R[p];i++)
ans+=A[i];
ans+=tag[p]*(R[p]-l+1);
//右边残余
for(re int i=L[q];i<=r;i++)
ans+=A[i];
ans+=tag[q]*(r-L[q]+1);
return ans;
}
int main()
{
n=read(),Q=read();
for(re int i=1;i<=n;i++)
A[i]=read();
pre_divide();
while(Q--)
{
int flag=read();
if(flag==1)//区间修改
{
int x=read(),y=read();
ll k=read();
change(x,y,k);
}
else if(flag==2)//区间查询
{
int x=read(),y=read();
printf("%lld\n",query(x,y));
}
}
return 0;
}
突然发现。。。貌似分块跑的比线段树快???(大雾)
可能是我太菜了,导致线段树写的很丑,常数很大
其实这也就是最简单的分块了
===============================================================
ex2:P2801 教主的魔法
题目传送门:
https://www.luogu.org/problemnew/show/P2801
第一眼一看。。。貌似是个线段树题
仔细想,线段树好像不能做“询问区间大于等于k的数的个数”,即使能做也很费劲实现
你可能会说:“我会树套树!”
分块看起来很好想对吧?其实实现起来也是很简单的,代码如下:
#include<iostream>
#include<cstdio>
#include<cmath>
#define re register
using namespace std;
inline int read()
{
int p=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
return f*p;}
const int maxn=200003;
int n,Q,tot,L[maxn],R[maxn],bel[maxn],A[maxn],to[maxn],step[maxn];
inline void pre_divide()//分块
{
tot=sqrt(n);
for(re int i=1;i<=tot;i++)
L[i]=(i-1)*sqrt(n)+1,R[i]=i*sqrt(n);
if(R[tot]<n)tot++,L[tot]=R[tot-1]+1,R[tot]=n;
for(re int i=tot;i>=1;i--)//预处理每个点能到达的点和能走的步数
for(re int j=R[i];j>=L[i];j--)
{
bel[j]=i,to[j]=j+A[j];
if(to[j]>R[i])step[j]=1;
else step[j]=step[to[j]]+1,to[j]=to[to[j]];
}
}
inline void change(int x,int k,int p=0)//单点修改
{
A[x]=k,p=bel[x];
for(re int i=R[p];i>=L[p];i--)
{
to[i]=i+A[i];
if(to[i]>R[p])step[i]=1;
else step[i]=step[to[i]]+1,to[i]=to[to[i]];
}
}
inline int query(int x,int ans=0)//暴力向后弹
{
for(;x<=n;x=to[x])ans+=step[x];
return ans;
}
int main()
{
n=read();
for(re int i=1;i<=n;i++)
A[i]=read();
pre_divide();
Q=read();
while(Q--)
{
int flag=read();
if(flag==1)
{
int x=read()+1;
printf("%d\n",query(x));
}
else if(flag==2)
{
int x=read()+1,k=read();
change(x,k);
}
}
return 0;
}
以上就是分块的做法了
但其实,本题的
我太菜了不会写还是用分块解法好了(大雾)
时间复杂度分析:
本题中,分块预处理的时间复杂度为
那么总复杂度为
===============================================================
ex4:P4168 [Violet]蒲公英
题目传送门:
https://www.luogu.org/problemnew/show/P4168
一道神仙毒瘤题,终于写完了
真的是一道挺好的分块模板(水)题,还是可以做一做的
(感觉又占了一道黑题的便宜)(大雾)
这题算得上是一道典型的在线区间众数问题了
题意很简单,给定一个序列,求区间众数,强制在线
可能有人说:我会线段树、树状数组、(此处省去一万字神仙算法)。。。
但是不好意思,这些做法不能解决这个题
因为众数不满足“区间加法”的性质
例如现有区间
这时,如果使用线段树和树状数组等数据结构就很难维护。
发现是一个强制在线无修改区间众数题,不妨考虑分块
本题实际上有两种分块做法,在这里都介绍一下
part1:做法1 暴力分快
一种比做法
我们可以只预处理那些以“块”为单位的众数,即我们设
我们还是考虑每次询问,那么有以下操作
对于查询指令“
2.(零散块操作)对不足一整块的边角块
那么以上这些就是查询操作
现在来分析一下做法
我们不妨设将整个序列分为
显然,我们预处理这些只以块为单位的众数,这些以块为单位组成的区间一共有
考虑每次询问,对于整块显然是
那么综上,整个算法的总复杂度为
根据均值不等式,显然当
此时整个算法的时间复杂度为
那么以上就是第一种做法了
可能。。。数据再大一些就会被卡掉了,毕竟复杂度很高,常数也不小
下面放一下代码
2.(零散块操作)对不足一整块的边角块
那么以上这些就是查询操作
现在来分析一下做法
我们不妨设将整个序列分为
显然,我们预处理整块的众数为
单次回答询问显然是
那么整个算法的总复杂度是
根据均值不等式,显然当
此时整个算法的时间复杂度为
那么第二种方法也就算写完了
感觉。。。大概的思路还是很好理解的吧(可能代码写的有点让人摸不着头脑)
我太菜了,常数写的巨大,滥用STL导致效率不是太好,甚至。。。比做法1还慢
下面放一下(巨丑的)代码好了
既然讲到了分快,就必须提一下更加优美的暴力----莫队
从一种角度来讲,莫队是一种思想化的算法,是一种将询问放在一起考虑,基于分块实现操作,凭借分块的根号算法优势来达到普通暴力无法企及的效率的一种算法。
因此,也可以说莫队本质上是一种优化后的暴力
注意:莫队算法是一种离线算法,通常不能有修改操作
首先,如果要用莫队算法,则必须满足已知
莫队算法的具体步骤:
1.先对原序列分块,一般分为
2.离线操作,把所有询问放在一起排序,以左端点所在块编号为第一关键字,右端点的位置为第二关键字,之后维护
来分析一下莫队算法的时间复杂度:
1.左端点所在块编号确定时,右端点位置单调不降,右端点移动最坏产生的时间复杂度是
2.左端点所在块编号变动时,右端点移动最坏产生的时间复杂度是
3.块内左端点位置每次最多移动
又由于通常
莫队算法的分类:
莫队算法一般分为两类,一是莫队维护区间答案,二是维护区间内的数据结构。当然细分的话还有树上莫队,带修改莫队、二维莫队等等。
莫队算法的小优化:奇偶块排序
莫队算法比较重要的东西是对询问排序
但是排序也是有讲究的
莫队算法一般的排序方式是这样的:
bool cmp(query x,query y)
{
return (x.r/tot)==(y.r/tot)?x.l<y.l:x.r<y.r;
}
而有一种神奇的排序方式叫做奇偶块排序,它长这样:
bool cmp1(query x,query y)
{
return (x.l/tot)^(y.l/tot)?x.l<y.l:(((x.l/tot)&1)?x.r<y.r:x.r>y.r);
}
由于chen_zhe过于毒瘤,导致现在几乎没有几道莫队题用普通排序能通过了
奇偶块排序会快的原理貌似是因为在普通排序之后,指针会出现跳回左边的情况后,而为了处理下一个块又要跳回右边,但经过奇偶块排序之后,指针移到右边后就不用再跳回左边,所以这样能减少很多(一半)操作,理论上能快一倍
但是都不重要,只要记背下来就好了(雾)
到了这里,上面的东西都挺好理解的吧
由于我很菜还不会其他的莫队接下来我们主要来讲一下普通的莫队算法
来看几道例题:(以下都是不带修改的普通莫队)
===============================================================
新鲜热乎的(普通)莫队例题:(未完待续)
ex5:P1972 [SDOI2009]HH的项链
题目传送门:
https://www.luogu.org/problemnew/show/P1972
还记得
题目大意:
给定一个长度为
数据范围:
这题一看就可以主席树来做,但是我们假装不会主席树
emmm....考虑暴力,每次询问暴力扫
不妨来改变一下暴力的方式,可以维护一个区间
仔细一想,会发现我根本就是在骗人
什么嘛,这不就是暴力嘛!和
(没错,我就是在骗你)(大雾)
想到这个过程本质上是与暴力枚举没有区别的,我们考虑优化(其实就是莫队算法)
先对所有询问按上面的方法以左端点所在块为第一关键字,右端点为第二关键字排序
之后就从左往右依次回答每一个询问(暴力跳指针
在回答询问时更新答案也很简单,考虑在指针移动时其实就是加入和删除贝壳的过程。加入贝壳时对于一种颜色
看起来很好理解是不是?其实这就是最简单的莫队算法了
下面放一下代码
(由于数据加强,下面的代码过不去了,有需要的同学可以尝试主席树解决本题)
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂
{
int now=0,f=1;char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxn=500003;
struct query
{
int l,r,id;
}q[maxn];
int n,m,tot,A[maxn],bel[maxn],sum[maxn<<1],Ans[maxn],ans,L,R;
bool cmp1(query x,query y)
{
if(bel[x.l]==bel[y.l])return (bel[x.l]&1)?x.r<y.r:x.r>y.r;
return bel[x.l]<bel[y.l];
}
inline void del(int x)
{
if(!(--sum[A[x]]))ans--;
}
inline void add(int x)
{
if((++sum[A[x]])==1)ans++;
}
int main()
{
n=read(),tot=1221;
for(int i=1;i<=n;i++)
A[i]=read(),bel[i]=(i-1)/tot+1;
m=read();
for(int i=1;i<=m;i++)
q[i].l=read(),q[i].r=read(),q[i].id=i;
sort(q+1,q+1+m,cmp1);
for(int i=1;i<=m;i++)
{
int ql=q[i].l,qr=q[i].r,x=q[i].id;
while(L<ql)del(L++);
while(L>ql)add(--L);
while(R<qr)add(++R);
while(R>qr)del(R--);
Ans[x]=ans;
}
for(int i=1;i<=m;i++)
printf("%d\n",Ans[i]);
return 0;
}
(尽管用了神奇的奇偶块排序和
所以说上面这题的代码仅供参考
==================================================================
ex6:P2709 小B的询问
题目传送门:
https://www.luogu.org/problemnew/show/P2709
题意很简单,对于每一次询问
第一眼看完。。。直接就想到了暴力(雾)
不妨来考虑一下更优的暴力----莫队
显然,用莫队来维护答案是合理的
类似于上面的做法,不妨设维护的区间
考虑在指针移动时其实就是加入和删除某个数字的过程。加入数字时对于一种颜色
下面放一下代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define re register
using namespace std;
typedef long long ll;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂
{
int now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxn=50003;
struct query
{
int l,r,id;
}q[maxn];
int n,m,K,tot,L=1,R,A[maxn],sum[maxn];
ll Ans[maxn],ans;/*
bool cmp(query x,query y)
{
return (x.r/tot)==(y.r/tot)?x.l<y.l:x.r<y.r;
}*/
bool cmp1(query x,query y)
{
return (x.l/tot)^(y.l/tot)?x.l<y.l:(((x.l/tot)&1)?x.r<y.r:x.r>y.r);
}
inline void del(int x)
{
sum[A[x]]--;
ans-=2*sum[A[x]]+1;
}
inline void add(int x)
{
sum[A[x]]++;
ans+=2*sum[A[x]]-1;
}
int main()
{
n=read(),m=read(),K=read(),tot=sqrt(n);
for(re int i=1;i<=n;i++)
A[i]=read();
for(re int i=1;i<=m;i++)
q[i].l=read(),q[i].r=read(),q[i].id=i;
sort(q+1,q+1+m,cmp1);
for(re int i=1;i<=m;i++)
{
int ql=q[i].l,qr=q[i].r;
while(L<ql)del(L++);
while(L>ql)add(--L);
while(R<qr)add(++R);
while(R>qr)del(R--);
Ans[q[i].id]=ans;
}
for(re int i=1;i<=m;i++)
printf("%lld\n",Ans[i]);
return 0;
}
这道题还是要用奇偶块排序(但是尝试了一下,不用也能过)
================================================================
ex7:P1494 [国家集训队]小Z的袜子
题目传送门:
https://www.luogu.org/problemnew/show/P1494
听说是一道莫队入门经典题,于是就来做了
结果发现和
(反正莫队都是一个套路就对了)
来讲一下这道题
一看,立马就能出答案(真的,不是我吹牛)
对于某一次询问
则所有合法的方案为
还可以得到本区间内所有方案数为
那么对于询问区间
展开化简,可以得到
题目中没有修改操作,显然用线段树和树状数组是无用功
不妨使用莫队
同时考虑莫队算法过程中指针
考虑在指针移动时其实就是加入和删除某只袜子的过程。加入袜子时对于一种颜色
(其实和上面的
最后注意约分和
下面放代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define re register
using namespace std;
typedef long long ll;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂
{
int now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxn=50003;
struct query
{
int l,r,id;
}q[maxn];
struct anwser
{
ll up,down;
}Ans[maxn];
int n,m,tot,L,R,A[maxn];
ll sum[maxn],ans;
bool cmp1(query x,query y)
{
return (x.l/tot)^(y.l/tot)?x.l<y.l:(((x.l/tot)&1)?x.r<y.r:x.r>y.r);
}
inline ll gcd(ll a,ll b)
{
if(!b)return a;
return gcd(b,a%b);
}
inline void del(int x)
{
sum[A[x]]--;
if(sum[A[x]]>0)
ans+=(-2)*sum[A[x]];
}
inline void add(int x)
{
sum[A[x]]++;
if(sum[A[x]]>1)
ans+=2*(sum[A[x]]-1);
}
int main()
{
/*
分析:对于某一次询问,答案为
ans=sigma(i:l~r)num[x]*(num[x]-1)/(r-l+1)*(r-l)即
ans=sigma(i:l~r)sum[x]^2-sigma(i:l~r)sum[x]/(r-l+1)*(r-l)
可以发现只需要维护分子
那么用莫队来做
考虑每次挪动加入元素对答案的贡献为2*num[x]+1
那么删除同理,贡献为2*num[x]-1
最后注意gcd约分即可
*/
n=read(),m=read(),tot=sqrt(n);
for(re int i=1;i<=n;i++)
A[i]=read();
for(re int i=1;i<=m;i++)
q[i].l=read(),q[i].r=read(),q[i].id=i;
sort(q+1,q+1+m,cmp1);
for(re int i=1;i<=m;i++)
{
ll ql=q[i].l,qr=q[i].r,x=q[i].id;
if(ql==qr)
{
Ans[x].up=0,Ans[x].down=1;
continue;
}
while(L<ql)del(L++);
while(L>ql)add(--L);
while(R<qr)add(++R);
while(R>qr)del(R--);
if(!ans)
{
Ans[x].up=0,Ans[x].down=1;
continue;
}
ll g=gcd(ans,(qr-ql+1)*(qr-ql));
Ans[x].up=ans/g,Ans[x].down=(qr-ql+1)*(qr-ql)/g;
}
for(re int i=1;i<=m;i++)
printf("%lld/%lld\n",Ans[i].up,Ans[i].down);
return 0;
}
仍然用的是奇偶块排序。。。普通排序没试过。。。
===============================================================
ex8:P4462 [CQOI2018]异或序列
题目传送门:
https://www.luogu.org/problemnew/show/P4462
一道看起来不是太像莫队的题目。。。
但是转念一想发现几乎是个莫队板子题
来看一下怎么做
题目要求询问区间
第一眼一看像个线段树题
但是想到没有修改,最后还是放弃了线段树(会多个
先来考虑暴力做法
预处理前缀异或和,直接暴力枚举
显然这样做的单次复杂度达到了
既然都是暴力,不妨考虑莫队
貌似莫队
其实异或还有一个性质,
基于上面的暴力做法,每次都要枚举一个区间异或和,为什么不把区间异或和存在一个数组中呢?
即我们记
现在重要的事就转变成了如何维护这个
我们使用莫队算法来维护
来考虑莫队算法过程中指针
考虑在指针移动时其实就是加入和删除某个数字的过程。加入数字
注意:由于异或前缀和的计算方式是
那么下面就放一下代码好了
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define re register
using namespace std;
typedef long long ll;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂
{
int now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxn=100003;
struct query
{
int l,r,id;
}q[maxn];
int n,m,K,tot,L,R,A[maxn],sum[maxn],Ans[maxn],ans;
bool cmp1(query x,query y)
{
return (x.l/tot)^(y.l/tot)?x.l<y.l:(((x.l/tot)&1)?x.r<y.r:x.r>y.r);
}
inline void del(int x)//注意先后顺序
{
ans-=sum[A[x]^K],sum[A[x]]--;
}
inline void add(int x)//注意先后顺序
{
ans+=sum[A[x]^K],sum[A[x]]++;
}
int main()
{
n=read(),m=read(),K=read(),tot=sqrt(n);
for(re int i=1;i<=n;i++)
A[i]=A[i-1]^read();
for(re int i=1;i<=m;i++)
q[i].l=read()-1,q[i].r=read(),q[i].id=i;
sort(q+1,q+1+m,cmp1),sum[0]=1;
for(re int i=1;i<=m;i++)
{
int ql=q[i].l,qr=q[i].r,x=q[i].id;
while(L<ql)del(L++);
while(L>ql)add(--L);
while(R<qr)add(++R);
while(R>qr)del(R--);
Ans[x]=ans;
}
for(re int i=1;i<=m;i++)
printf("%d\n",Ans[i]);
return 0;
}
仍然用的是奇偶块排序。。。还是没测试普通排序
===============================================================
ex9:P4137 Rmq Problem / mex
题目传送门:
https://www.luogu.org/problemnew/show/P4137
一道看起来就不像是莫队的题目
但是第一眼看见的时候,感觉线段树貌似可行?
仔细一想,发现区间的
因此还是要向暴力(莫队)方向想
于是。。。又用莫队水了一道紫题(大雾)
本题没有修改操作,很容易想到将所有询问离线,排序后用莫队暴力处理
貌似莫队都是一个套路
但是每个数
来考虑一下
emmmm。。。(还是看不出什么)
既然说了
仔细想一想应该是能看懂这段话的
即,无论
又由于题目中保证
于是乎用莫队就可以做啦!
再来考虑莫队算法过程中指针
考虑在指针移动时其实就是加入和删除某个数字的过程。加入数字
下面放代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define re register
using namespace std;
inline int read()
{
int p=0,f=1;re char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
return f*p;}
const int maxn=200003;
struct query
{
int l,r,id;
}q[maxn];
int n,m,tot,L=1,R=1,ans,A[maxn],sum[200003],Ans[maxn];
bool cmp1(query x,query y)
{
return (x.l/tot)^(y.l/tot)?x.l<y.l:(((x.l/tot)&1)?x.r<y.r:x.r>y.r);
}
inline void del(int x)
{
if(A[x]>n)return ;
if(!(--sum[A[x]]))ans=min(ans,A[x]);
}
inline void add(int x)
{
if(A[x]>n)return ;
sum[A[x]]++;
if(ans==A[x])while(sum[ans])ans++;
}
int main()
{
n=read(),m=read(),tot=(int)sqrt(n);
for(re int i=1;i<=n;i++)
A[i]=read();
for(re int i=1;i<=m;i++)
q[i].l=read(),q[i].r=read(),q[i].id=i;
sort(q+1,q+1+m,cmp1),add(L);
for(re int i=1;i<=m;i++)
{
int ql=q[i].l,qr=q[i].r,x=q[i].id;
while(L<ql)del(L++);
while(L>ql)add(--L);
while(R<qr)add(++R);
while(R>qr)del(R--);
Ans[x]=ans;
}
for(re int i=1;i<=m;i++)
printf("%d\n",Ans[i]);
return 0;
}
还还还是奇偶块排序。。。仍然不敢写普通排序
过掉了之后才发现,但是貌似这题正解是树套树?
(我不管,反正用莫队水过了)(大雾)
于是乎。。。又水了一道树套树神仙题
===============================================================
小结:
做完这么多题之后我们能发现:
莫队算法几乎在所有的离线区间问题面前都是无敌的
几乎只要是能离线,用莫队算法都能取得一个令人满意的分数
(当然像noip、chen_zhe、lin_toto那样毒瘤的出题人不算在出题人队伍里)
通常莫队算法不是满分也是高分(
暴力碾标算!(大雾)
莫队不像线段树和树状数组,那样需要对维护的信息有所顾虑(必须满足区间可加性),和分块一样,莫队也适用于很多问题,比如中位数和区间众数等问题莫队都是可以做的
但是普通的莫队算法也有缺陷,就是不能修改
为了延续暴力的美德,后人提出了可修改的莫队算法,即带修的莫队算法
接下来我们来讲一下带修莫队
进阶:带修莫队----普通莫队的扩展
由于本人过于菜,看了将近一上午才大概看懂了带修莫队的实现方式
(不得不说也是神奇,为了暴力竟然能想出这么神仙的算法)
众所周知,莫队是一种离线算法,通常不能有修改
对付那些没有修改的区间查询题目,普通莫队可以说是杀手锏
但是如果加上了修改,普通莫队就没有了用武之地
这时,我们就要对普通的莫队算法进行改进,带修莫队就这样横空出世了
那么下面就大概讲一下如何实现带修莫队
带修莫队为了解决修改的问题,仍让沿用了普通莫队的思想,将时间也分割开来,在合理的排序下仍然能保证得到最优的解决方式,这种思想使得算法得到优化和改进,这样莫队算法也能适用于更多的问题
类似于普通莫队的思想,还是维护某个区间
由于有了修改操作,需要给我们维护的区间加上一维时间轴,即我们现在维护的是
仍然基于暴力思想,我们可以想到当
那么带修的莫队算法就呼之欲出了
带修莫队实现如下:(非常类似于普通莫队)
1.先对原序列进行分块
2.增加一个变量
带修莫队的排序方式:
仍然类似于普通莫队,带修莫队的排序方式是下面这样的:
bool cmp1(query x,query y)
{
return (x.l/tot)^(y.l/tot)?x.l<y.l:((x.r/tot)^(y.r/tot)?x.r<y.r:x.pre<y.pre);
}
即我们先按照左端点排序,再按照右端点排序,最后按照时间排序
(
貌似在带修莫队里,没有像普通莫队那样神奇的排序方式(奇偶块排序)
(反正都不重要只要背下来就好了)(大雾)
带修莫队的时间复杂度分析
看完上面普通莫队的你应该了解到,莫队算法需要分块
那么,带修莫队时,分块的大小为多少呢?
我们要先分析一下带修莫队的时间复杂度
前方高能预警,请非战斗人员尽快撤离!
不妨设分块的大小为
我们来考虑带修莫队算法中
1.对于
在块内移动时,每一次移动的复杂度为
到下一个块时,每一次移动的复杂度为
综上,因
2.对于
当
当
综上,因
3.对于
当
当
当
综上,因
综上所述,带修莫队的总时间复杂度为
根据均值不等式可知,存在一个
显然此时当
即带修莫队算法的总时间复杂度为
(不得不说时间复杂度证明真的恶心)
光说上面这些可能有些无力,下面结合例题说明
===============================================================
新鲜热乎的(带修)莫队例题:(未完待续)
ex10:P1903 [国家集训队]数颜色 / 维护队列
题目传送门:
https://www.luogu.org/problemnew/show/P1903
据说是一道典型的带修莫队。。。于是就来做一下
(不用带修莫队就要用树套树,我也不会啊QAQ)
这题如果没有修改操作,就和
但是即使多了修改操作,我们也能用神仙的带修莫队算法搞定
如果你已经看懂上面的带修莫队了,那么这题就是一道板子题了
对于每一个询问,记录下在这次询问前进行了几次修改
对于每一个修改,直接记录
每次维护三个指针
先来考虑莫队算法过程中指针
考虑在指针移动时其实就是加入和删除某个数字的过程。加入数字时对于一种颜色
再来考虑莫队算法过程中指针
对于每一个询问,如果发现时间不同步,我们要将没有执行的修改执行,或者将多执行的修改撤销,同时等效成添加或删除数字,继续维护当前的答案
光说可能有些难懂,下面放代码了
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define re register
using namespace std;
typedef long long ll;
inline int read()
{
int p=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
return f*p;}
const int maxn=50003;
struct query
{
int l,r,pre,id;
}q[maxn];
struct change
{
int pos,x;
}C[maxn];
int n,m,tot,sum_query,sum_change,L=1,R,now,ans;
int A[maxn],sum[1000003],Ans[maxn];
bool cmp1(query x,query y)
{
return (x.l/tot)^(y.l/tot)?x.l<y.l:((x.r/tot)^(y.r/tot)?x.r<y.r:x.pre<y.pre);
}
inline void del(int x)
{
if(!(--sum[A[x]]))ans--;
}
inline void add(int x)
{
if(++sum[A[x]]==1)ans++;
}
inline void time_go(int p,int id)
{
if(q[id].l<=C[p].pos&&C[p].pos<=q[id].r)
{
if(!(--sum[A[C[p].pos]]))ans--;
if(++sum[C[p].x]==1)ans++;
}
// A[C[p].pos]=C[p].x;
swap(A[C[p].pos],C[p].x);
//注意:由于以后还要用到第p次修改,所以只能暂时交换值,不能直接赋值
}
int main()
{
n=read(),m=read(),tot=(int)pow(n*1.0,2.0/3.0);
for(re int i=1;i<=n;i++)
A[i]=read();
for(re int i=1;i<=m;i++)
{
char s[5];
scanf("%s",s);
if(s[0]=='Q')
{
sum_query++;
q[sum_query].l=read();
q[sum_query].r=read();
q[sum_query].id=sum_query;
q[sum_query].pre=sum_change;
}
else if(s[0]=='R')
{
sum_change++;
C[sum_change].pos=read();
C[sum_change].x=read();
}
}
sort(q+1,q+1+sum_query,cmp1);
for(re int i=1;i<=sum_query;i++)
{
int ql=q[i].l,qr=q[i].r,x=q[i].id,t=q[i].pre;
while(L<ql)del(L++);
while(L>ql)add(--L);
while(R<qr)add(++R);
while(R>qr)del(R--);
while(now<t)time_go(++now,i);
while(now>t)time_go(now--,i);
Ans[x]=ans;
}
for(re int i=1;i<=sum_query;i++)
printf("%d\n",Ans[i]);
return 0;
}
如果你看懂了上面带修莫队的一系列操作应该能看懂这份代码了
===============================================================
ex11:CF940F Machine Learning
一道强行拼凑起来的题。。。细节写错结果交了
大概读完之后能发现其实是上面的
那就是一道有修改操作的
直接上带修莫队配合???
再来考虑莫队算法过程中指针
对于每一个询问,如果发现时间不同步,我们要将没有执行的修改执行,或者将多执行的修改撤销,同时等效成添加或删除数字,继续维护当前的答案
最后的最后,我们直接暴力循环一遍找答案
至于为什么可以暴力循环来找答案,可以给出一个复杂度证明
不妨设我们要找的答案
本人表述可能有些不太清楚,所以还是看代码好了
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<map>
#define re register
using namespace std;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂
{
int now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxn=200005;
struct query
{
int l,r,pre,id;
}q[maxn];
struct change
{
int pos,x,oldval;
}C[maxn];
int n,m,tot,cnt,sum_query,sum_change,L=1,R=1,now,ans;
int A[maxn],B[maxn],sum[maxn],num[maxn],Ans[maxn];
//sum[x]存的是x的出现次数,num[i]存的是sum值为i的出现次数
map <int,int> M;
bool cmp1(query x,query y)
{
return (x.l/tot)^(y.l/tot)?x.l<y.l:((x.r/tot)^(y.r/tot)?x.r<y.r:x.pre<y.pre);
}
inline void del(int x)
{
num[sum[x]]--,sum[x]--;
if(sum[x]>0)num[sum[x]]++;
}
inline void add(int x)
{
if(sum[x]>0)num[sum[x]]--;
sum[x]++,num[sum[x]]++;
}
inline void time_back(int p,int id)
{
if(q[id].l<=C[p].pos&&C[p].pos<=q[id].r)
del(C[p].x),add(C[p].oldval);
A[C[p].pos]=C[p].oldval;
}
inline void time_go(int p,int id)
{
if(q[id].l<=C[p].pos&&C[p].pos<=q[id].r)
del(C[p].oldval),add(C[p].x);
A[C[p].pos]=C[p].x;
}
int main()
{
n=read(),m=read(),tot=(int)pow(n*1.0,2.0/3.0);
for(re int i=1;i<=n;i++)
{
A[i]=read();
if(!M[A[i]])M[A[i]]=++cnt;
B[i]=A[i]=M[A[i]];
}
for(re int i=1;i<=m;i++)
{
int flag=read(),x=read(),y=read();
if(flag==1)
{
sum_query++;
q[sum_query].l=x;
q[sum_query].r=y;
q[sum_query].id=sum_query;
q[sum_query].pre=sum_change;
}
else if(flag==2)
{
sum_change++;
C[sum_change].pos=x;
if(!M[y])M[y]=++cnt;
C[sum_change].x=y=M[y];
C[sum_change].oldval=B[x],B[x]=y;
}
}
sort(q+1,q+1+sum_query,cmp1),add(A[L]);
for(re int i=1;i<=sum_query;i++)
{
int ql=q[i].l,qr=q[i].r,x=q[i].id,t=q[i].pre;
while(L<ql)del(A[L++]);
while(L>ql)add(A[--L]);
while(R<qr)add(A[++R]);
while(R>qr)del(A[R--]);
while(now<t)time_go(++now,i);
while(now>t)time_back(now--,i);
for(ans=1;num[ans];ans++);
Ans[x]=ans;
}
for(re int i=1;i<=sum_query;i++)
printf("%d\n",Ans[i]);
return 0;
}
于是乎,又用莫队暴力过掉了一道紫题
(怎么感觉又在占题目的便宜)(大雾)
===============================================================
总结:
学习完了带修莫队,我才感受到暴力的美妙
(反正只要是暴力能想到解决方法的几乎都能用莫队来实现)
个人感觉,莫队是一种美妙的暴力算法,基于分块实现使得莫队算法在处理离线问题方面具有无与伦比的效率,这是其他更加高级的算法所不能媲美的
如让我果用两个词语来形容莫队算法,那就是朴素与直观
尽管我们上面提到的很多例题都是可以用树套树(或平衡树等)来解决的,但是由于树套树代码量高,常数巨大,实现困难等种种原因,都使得问题变得更加复杂。相比之下,莫队算法为我们提供了一种解决问题的好途径,使得我们能在变化量很小的情况下,快速而简便的得出问题的解,因此可以说,莫队算法是一种是非常优秀的算法
(这上面都是我自己
或许这就是算法的美妙所在了吧
突然发现。。。
(大部分莫队题都被
学完带修莫队之后,发现其实还有树上莫队和树上带修莫队
这里给出一道树上带修莫队模板题:P4074 [WC2013]糖果公园
学有余力的同学可以去学习一下(我太菜了,目前并不会树上带修莫队QAQ)
由于我太菜了导致并不想再学了(大雾)所以就这两个鸽了吧
行,这篇文章就暂时先写这么多吧,以后可能还会补上一些例题