继分块后的第三种高级数据结构,,,
(学了分块后好像就是对莫队有了很高很高的兴趣,,估计是学分块学傻了吧 0.0)
还是先听了听大佬的课,用了一个小时自己消化了一下,才知道莫队的思想:就是在分块的基础上加上排序,可以大大降低复杂度,降至O(n1.5),还有一个最好认识的标志离线询问(那个分块9要是不在线就是个裸莫队啊 ̄へ ̄)
有关排序的证明,请参考这位大佬的介绍:传送门
莫队算法通常用来解决序列上多次进行区间询问的问题。与分块算法一样,莫队算法常使用在,需要维护的信息无法快速合并的时候(即传统数据结构题无法简单处理)。使用莫队算法时,题目需要满足下列条件:
1、题目允许离线;
2、题目没有修改操作(后面会进行拓展,使得莫队能支持一些简单的修改操作,即带修莫队);
3、不同询问区间的答案可以快速地互相计算得出。
莫队算法的核心是将所有的询问重新排序,按照这个新的顺序依次回答询问,并且回答每一个询问时会以上一个询问为基础。
更具体地,我们举个例子,若现在已知区间[L1 , R1 ] 的答案,则要利用这个值得到区间 [L2 , R2 ] 的答案,需要花费O(|L1 – L2| + |R1 – R2|) 的时间,则我们可以将询问看成是平面上的一个坐标(Li , Ri) ,并定义两个询问间转移的花费,为它们在二维平面上的曼哈顿距离(这个写法是n*sqtr(n),但是蒟蒻不会,如果有兴趣的话:传送门)。但是一般来说都是用分块写的。
设序列长度为 n n n,询问次数为 m m m,块大小为 b l o c k block block,
分类讨论:
①L的移动:若下一个询问与当前询问的L所在的块不同,那么只需要经过最多 2 b l o c k 2block 2block步可以使得L成功到达目标。复杂度为: O ( m ∗ b l o c k ) O(m*block) O(m∗block)
②R的移动: R R R只有在 b e l o n g [ L ] belong[L] belong[L]相同时才会有序,对于每一个块,排序执行了第二关键字R,因为R是单调递增的,所以枚举完一个块,R最多移动n次,总共有 n / b l o c k n/block n/block个块。复杂度为: O ( n ∗ n / b l o c k ) O(n*n/block) O(n∗n/block)
显然最优情况下两者应取等,即 m ∗ b l o c k = n 2 / b l o c k m*block=n^2/block m∗block=n2/block,当 b l o c k = n / s q r t ( m ) block=n/sqrt(m) block=n/sqrt(m)时,取得最优移动次数为 n ∗ s q r t ( m ) n*sqrt(m) n∗sqrt(m),所以莫队最优复杂度为: O ( n ∗ s q r t ( m ) ) O(n*sqrt(m)) O(n∗sqrt(m))
经典莫队(题解注释吧)
第一次刷codeforces上的题,(好激动啊~~~٩(๑>◡<๑)۶)
现放AC记录:
AC代码:
#include
#include
#include
#define LL long long
using namespace std;
const int sea=1<<20;
int n,m,k,belong[sea],block,L=1,R=0,a[sea];
LL ans[sea],flag[sea],res=0;
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1; ch=getchar();}
while(ch<='9'&&ch>='0')s=s*10+ch-'0',ch=getchar();
return s*w;
}
struct ask {int l,r,id;}q[sea];
bool cmp(ask x,ask y)
{
if(belong[x.l]==belong[y.l]) return x.r<y.r;
return belong[x.l]<belong[y.l];
}//重要的排序
void del(int x)//删除
{
flag[a[x]]--;
res-=flag[a[x]^k];
}
void add(int x)//添加
{
res+=flag[a[x]^k];//每添加进去一个数就相当于加上这个异或k
flag[a[x]]++;
}
int main()
{
n=read(); m=read(); k=read();
block=sqrt(n);
for(int i=1;i<=n;i++)
{
a[i]=read();
a[i]=a[i]^a[i-1];//处理前缀和,至于为什么要处理前缀和,,应该都知道吧,就是一段区间i,j的异或就相当于从1异或到i-1的连续异或再异或上从1异或到j的连续异或;
belong[i]=i/block;
}
for(int i=1;i<=m;i++)
{
q[i].l=read(); q[i].r=read();
q[i].id=i;
}
sort(q+1,q+m+1,cmp);//莫队基本用法即核心思想
flag[0]=1;//记录前缀和出现的此数
for(int i=1;i<=m;i++)
{
while(L<q[i].l) del(L-1),L++;
while(L>q[i].l) L--,add(L-1);
while(R<q[i].r) R++,add(R);
while(R>q[i].r) del(R),R--;
ans[q[i].id]=res;//这个比较抽象不是很好理解看下面我画个图帮助理解吧
}
for(int i=1;i<=m;i++)
cout<<ans[i]<<endl;
return 0;
}
所知区间:[ ]
询问区间:( )
L,R:所知区间左,右端点
l,r;询问区间左,右端点
这就比较清晰了吧,,(字太丑啦(⌒_⌒;)想着回去买个写字板什么的,,)
Xors on Segments
(好好的一道经典莫队怎么被DP搞垮了,,,)
但是突然发现,这个题用莫队写好像有点问题,好像没有办法维护删除后的数,看大佬的题解知道要用到字典树(我还不知道什么是字典树)
看完题解后才发现这个方法好暴力啊,但是神奇的是,这个竟然没有WA也没有T,听大佬说正解是莫队+字典树复杂度是(n+m)lognsqrt(n)但是这道题也有(n^2+nm)的算法,两个算法的复杂度差距不是很大。
AC代码:
#include
#include
#include
#define LL long long
using namespace std;
const int sea=1e6+5;
int ans[sea],f[sea],a[sea],g[sea];
int n,m;
struct ask{int l,r;}q[sea];
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1; ch=getchar();}
while(ch<='9'&&ch>='0')s=s*10+ch-'0',ch=getchar();
return s*w;
}
int main()
{
n=read(); m=read();
for(int i=1;i<=sea;i++) f[i]=f[i-1]^i;//初始化f[]数组,表示前缀和
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=m;i++) q[i].l=read(),q[i].r=read();
//好多大佬都用DP直接写了,作为蒟蒻的我看了看字典树+莫队,默默地写了DP,,,
for(int i=1;i<=n;i++)
{
int maxx=0;
for(int j=i;j<=n;j++)//直接暴力DP即可
{
int tt=f[a[i]]^f[a[j]];//a[i]的前缀和^a[j]的前缀和
tt^=min(a[i],a[j]);
maxx=max(tt,maxx);
g[j]=maxx;
}//把整个区间两两之间异或一遍存在g[]中
for(int j=1;j<=m;j++)
if(q[j].l<=i&&i<=q[j].r)//遍历一下看是不是在询问区间中
ans[j]=max(ans[j],g[q[j].r]);//暴力选举最大值
}
//好暴力啊,这个方法,但神奇的是竟然A了!!!
for(int i=1;i<=m;i++)
cout<<ans[i]<<endl;
return 0;
}
小z的袜子
引用XW大佬的话:莫队算法可以解决一类不修改、离线查询问题。
(从这道题,我看出了码代码的时候要认真啊~~~٩(º﹃º٩),死在了一个傻逼操作上,,,)
这个题就是最最最最经典的莫队算法,(好像一连写了好几个经典题目,光知道写裸题了),好像没有什么好说的,就是提醒:敲代码的时候要认真啊
关于题解公式:推荐一个大佬博客(公式推的很详细):传送门
AC代码:
#include
#define LL long long
using namespace std;
const int sea=50010;
struct ask{int l,r,id;} q[sea];
int a[sea],belong[sea];
LL Ans,f[sea],ans1[sea],ans2[sea];
int n,m,L,R;
bool cmp(ask x,ask y)
{
if(belong[x.l]==belong[y.l]) return x.r<y.r;//为什么我会写成x.r
return belong[x.l]<belong[y.l];
}
void ud(int x,int d)
{
Ans-=f[a[x]]*f[a[x]];
f[a[x]]+=d;
Ans+=f[a[x]]*f[a[x]];
}
LL gcd(LL x,LL y)
{
if(y==0) return x;
return gcd(y,x%y);
}
int main()
{
scanf("%d%d",&n,&m);
int block=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]), belong[i]=i/block;
for(int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r), q[i].id=i;
sort(q+1,q+m+1,cmp);
L=1,R=0,Ans=0;
//莫队基础操作
for(int i=1;i<=m;i++)
{
if(q[i].l==q[i].r)
{
ans1[q[i].id]=0;
ans2[q[i].id]=1;
continue;
}
//这个算剪枝吧,也算特判吧
while(R+1<=q[i].r) ud(R+1,1),R++;
while(R>q[i].r) ud(R,-1),R--;
R=q[i].r;
while(L<q[i].l) ud(L,-1),L++;
while(L-1>=q[i].l) ud(L-1,1),L--;
L=q[i].l;
//用莫队写就是比较整齐,对于强迫症的我看着人舒服
LL aa=Ans-q[i].r+q[i].l-1;
LL bb=(LL)(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
//这个使用公式推出来的,可以看看上面我推荐的博客,我手推了一遍,不算难
LL cc=gcd(aa,bb);
aa/=cc,bb/=cc;
ans1[q[i].id]=aa,ans2[q[i].id]=bb;
//对于最简分数的处理
}
for(int i=1;i<=m;i++)
printf("%lld/%lld\n",ans1[i],ans2[i]);
return 0;
}
Fibonacci-ish II
(发现学好英语真的好重要,,,٩(º﹃º٩))
一道正解要莫队+线段树维护,大佬说还是不难的,但作为一个我菜鸡,就推荐一下了(以后一定会补上的!!)。
(原来还以为莫队学完了,还好hsm大佬提醒)
带修莫队是什么呢? 莫队怎么还能修改呢???
(好烂的引入,,,,)
你会发现有一种题啊,就是让你很明显的写个莫队板子,但是就是在其中加一下什么修改,,,特别不舒服,可是在你发现要是可以修改的话比其他算法更简单,今天好像有大佬说可以Splay什么的,或是树状数组套主席树+平衡树,,,,我都记不住名字了,但我觉得还是莫队比较好写,毕竟代码短(不知道为什么就是不喜欢长代码,还超爱压行写,尽管会被大佬怼),但是根据个人喜爱去写就行,(引入是不是有点长了,,,,,)
带修莫队最最最最重要的就是时空转移,尽管这个词很花哨,但是还是要介绍一下,就是在sort(L,R)的时候加上一个指针t,就是用来记录出来时间,排序的话就按照(L,R,T)的顺序排就行,加上time以后就直接暴力修改即可。
的的确确是一个裸题,就是赤裸裸的带修莫队,对于莫队在代码上的具体非操作及解析看代码注释
AC代码:
#include
using namespace std;
const int sea=1000010;
int f[sea*100],ans[sea],belong[sea],a[sea],now[sea],T,R,L=1,Ans,block,n,k,m,Tm;
struct hit{int l,r,id,t;}q[sea];
struct beat{int bl,New,Old;}g[sea];
//beat中的New,Old是来存新的颜色和之前的颜色
bool cmp(hit x,hit y){return belong[x.l]==belong[y.l]?(belong[x.r]==belong[y.r]?x.t<y.t:x.r<y.r):x.l<y.l;}
void ad(int x,int d)//普通莫队的修改
{
f[x]+=d;
if(d>0) Ans+=f[x]==1;//一个有意思的写法先判断再加
if(d<0) Ans-=f[x]==0;
}
void dx(int x,int d)//暴力修改
{
if(x>=L&&x<=R)
ad(d,1),ad(a[x],-1);
a[x]=d;
}
int main()
{
scanf("%d%d",&n,&k);
block=pow(n,0.666666);//这里不知道为什么网上斗都在流传n的2/3次方就可以达到最优
for(int i=1;i<=n;i++) scanf("%d",&a[i]),now[i]=a[i],belong[i]=i/block+1;//now[]是用来存这一块现在的颜色
for(int i=1;i<=k;i++)
{
char c; int x,y;scanf(" %c %d%d",&c,&x,&y);
if(c=='Q') q[++m]=(hit){x,y,m,Tm};
if(c=='R') g[++Tm]=(beat){x,y,now[x]},now[x]=y;
}
sort(q+1,q+m+1,cmp);
for(int i=1;i<=m;i++)
{
while(T<q[i].t) dx(g[T+1].bl,g[T+1].New),T++;
while(T>q[i].t) dx(g[T].bl,g[T].Old),T--;
//带修莫队核心思想
while(R+1<=q[i].r) ad(a[R+1],1),R++;
while(R>q[i].r) ad(a[R],-1),R--;
while(L<q[i].l) ad(a[L],-1),L++;
while(L-1>=q[i].l) ad(a[L-1],1),L--;
ans[q[i].id]=Ans;
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);//离线莫队就代码短
return 0;
}
终于终于终于,写完了这到经典题,花了我一天半天的时间啊,现在就来好好整理一下树上莫队。
树上莫队重点就是树上分块,
贴心的我还是先放一下简化题解:
有 n n n 个点构成棵树,共 m m m 种糖, q q q个询问,每一个点都只发放某种特定的糖,我们用 c i c_i ci 来表示 i 号游览点的糖果。 l l l 到 r r r 的询问,经过每个游览点,都可以品尝到一颗对应种类的糖果。
打分:糖美味指数为 v i v_i vi,新奇指数 w i w_i wi,第 i 次品尝第 j 种糖,愉悦指数 H H H 将会为 v j ∗ w i v_j*w_i vj∗wi。求
∑ i = 1 n v j ∗ w i \sum_{i=1}^n v_j*w_i i=1∑nvj∗wi
有时,一些糖果点所发放的糖果种类可能会更改(也只会是 m m m 种中的一种)。(带修)
求每位游客游玩公园的愉悦指数。(离线)
题解:
(这也算是道很经典的莫队题,很值得一做)
不难看出这是个树上对于序列的操作,也就是树上分块,还可以有其他的一些做法,好像Splay也能做(然而我并不会分块以外的其他神操作,而且Splay代码太长了,,,懒得码,,),那么想到分块的话就很自然的想到了莫队,而且又没有强制在线,那么就树上莫队+带修莫队吧,(觉得自己说了一大堆废话,,,,)直接上干货:
题解=树上莫队+带修莫队
树上莫队=树上分块+莫队思想
树上分块=树上操作+分块思想
树上操作=树上dfs(欧拉)序遍历+lca倍增
AC代码:
#include
#define LL long long
using namespace std;
inline int read()
{
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1; ch=getchar();}
while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar();
return s*w;
}
const int sea=100005;
struct hit{int l,r,t,id;}q[sea];
struct beat{int bl,New,Old;}g[sea];
struct see {int next,ver;}e[sea<<1];
int n,m,Q,tot=0,block,cnq=0,cng=0,kkk=21,dd=0,tim,blonum,top;
int belong[sea],last[sea],deep[sea],fa[17][sea],bin[20],qq[sea],num[sea];
bool vis[sea];
LL f[sea],Ans,v[sea],w[sea],c[sea],now[sea],ans[sea];
//建图
void add(int x,int y){e[++tot].ver=y;e[tot].next=last[x];last[x]=tot;}
//带修莫队的cmp基础操作
bool cmp(hit a,hit b)
{
if(belong[a.l]==belong[b.l]&&belong[a.r]==belong[b.r]) return a.t<b.t;
else if(belong[a.l]==belong[b.l]) return belong[a.r]<belong[b.r];
else return belong[a.l]<belong[b.l];
}
// 对于树上莫队呢,首先就是欧拉序,跑过这个节点后就跑这个节点的子树,再返回去跑其他的点(具体看上面欧拉序的简介)
// 欧拉序和DFS序的区别就是欧拉序有返回,DFS没有返回直接到叶子结点处就完了
// 所以说这道题用DFS显然不可取,根据题意来说就是显然的欧拉序
// LCA(倍增)的 DFS处理(对于树的深度的处理)+树上建块
void dfs(int x)
{
int size=top;
for(int i=1;i<=16;i++)
if(deep[x]>=bin[i]) fa[i][x]=fa[i-1][fa[i-1][x]];
else break;
for(int i=last[x];i;i=e[i].next)
if(e[i].ver!=fa[0][x])
{
int y=e[i].ver;
deep[y]=deep[x]+1; fa[0][y]=x,dfs(y);
//树上建块
if(top-size>=block)
{
blonum++;//分块的块数
while(top!=size)
belong[qq[top--]]=blonum;//树上分块的核心
}
}
qq[++top]=x;
}
// 求LCA
int lca(int x,int y)
{
if(deep[x]<deep[y]) swap(x,y);
int d=deep[x]-deep[y];
for(int i=0;bin[i]<=d;i++)
if(bin[i]&d) x=fa[i][x];
for(int i=16;i>=0;--i)
if(fa[i][x]!=fa[i][y])
x=fa[i][x],y=fa[i][y];
if(x==y) return x;
return fa[0][x];
}
//带修莫队的修改
void ad(int x)
{
if(vis[x]) Ans-=w[num[f[x]]]*v[f[x]],num[f[x]]--;
else num[f[x]]++,Ans+=w[num[f[x]]]*v[f[x]];
vis[x]^=1;//当vis[]=1时vis[]=0,当vis[]=0时,vis[]=0;
}
void change(int x,int y)
{
if(vis[x]){ad(x);f[x]=y;ad(x);}
else f[x]=y;
}
void so(int x,int y)
{
while(x!=y)
{
if(deep[x]>deep[y]) ad(x),x=fa[0][x];
else ad(y),y=fa[0][y];
}
}
int main()
{
bin[0]=1;
for(int i=1;i<20;i++) bin[i]=bin[i-1]<<1;
n=read(); m=read(); Q=read();
block=pow(n,2.0/3)*0.5;
// block=sqrt(n); 这样的话会T两个点,是卡常吗,不是很知道。
for(int i=1;i<=m;i++) v[i]=read();
for(int i=1;i<=n;i++) w[i]=read();
for(int i=1;i<n;i++)
{
int x=read(),y=read();
add(x,y); add(y,x);
}
for(int i=1;i<=n;i++) now[i]=f[i]=read();
dfs(1);
while(top) belong[qq[top--]]=blonum;
for(int i=1;i<=Q;i++)
{
int T=read(),x=read(),y=read();
if(!T) g[++cng]=(beat){x,y,now[x]},now[x]=y;
else
{
if(belong[q[cnq].l]>belong[q[cnq].r]) swap(q[cnq].l,q[cnq].r);
q[++cnq]=(hit){x,y,cng,cnq};
}
}
sort(q+1,q+cnq+1,cmp);
for(int i=1;i<=q[1].t;i++) change(g[i].bl,g[i].New);
so(q[1].l,q[1].r); int
ins=lca(q[1].l,q[1].r);
ad(ins); ans[q[1].id]=Ans; ad(ins);
// 先修改第一组的询问
for(int i=2;i<=cnq;i++)
{
for(int j=q[i-1].t+1;j<=q[i].t;j++) change(g[j].bl,g[j].New);
for(int j=q[i-1].t;j>q[i].t;j--) change(g[j].bl,g[j].Old);
so(q[i-1].l,q[i].l),so(q[i-1].r,q[i].r);
int ins=lca(q[i].l,q[i].r);
ad(ins); ans[q[i].id]=Ans; ad(ins);
}
for(int i=1;i<=cnq;i++) printf("%lld\n",ans[i]);
return 0;
}
很有意思的一道回滚莫队裸题,但是听LDY大佬说这不就是主席树嘛,,,,但是网上有大佬说主席树不是很能写,,,算了,一个菜鸡也只能写写莫队水过了。
回滚莫队,一个很有意思的名字,其实就是在被逼无奈下的暴力优化吧,在普通的莫队是对于所选值的不断删除和添加,但如果你遇到像下面一样的维护最大值的值得时候你就会发现,,,这要是一删除不就挂了嘛,,但是要是用别的做法好像就要码很长很长的代码,就还是想用莫队,那就有个回滚莫队可供选择。
回滚莫队和普通莫队的区别就是,回滚莫队的L值是这个块中的最右端R+1(不能完全说是下一块的头,因为有可能是最后一块),右端点一开始在块尾(因为左右端点同块的我们都暴力解决了,剩下的右端点肯定至少在下一块),然后正常向右,当右端点走到询问区间右端点的时候,把左端点造成的影响还原,然后又滚回到块尾,因此左右端点都没有删除,只偶遇添加。
上题:历史研究
一道我调了很久才发现是编译环境不同的问题,,,(调到快疯了,还是LDY这位大神指点迷津,感谢大神!)
就是那个cmp函数,(我自己A过了我都不知道)害我白白调了一节课。。。
AC 代码:
#include
#define LL long long
using namespace std;
inline int read()
{
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1; ch=getchar();}
while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar();
return s*w;
}
const int sea=1e5 + 10;
struct hit {int l,r,id;}q[sea];
int n,m,tot,block,num;
LL ans[sea],Ans;
int belong[sea],a[sea],b[sea],f[sea];
//bool cmp(hit x,hit y){return belong[x.l]^belong[y.l]?belong[x.l]y.r;}
//在BZOJ上这个优化过不去,但在洛谷上能过去
bool cmp(hit x,hit y){return belong[x.l]==belong[y.l]?x.r<y.r:x.l<y.l;}
LL blf(int l,int r))//暴力查询
{
static int time[sea];//静态局部变量
LL s=0;
for(int i=l;i<=r;i++) time[a[i]]=0;
for(int i=l;i<=r;i++) time[a[i]]++,s=max(s,1ll*time[a[i]]*b[a[i]]);
return s;
}
void add(int x){f[a[x]]++,Ans=max(Ans,1ll*b[a[x]]*f[a[x]]);}
void del(int x){f[a[x]]--;}
int get(int i,int id
{
int RR=min(n,block*id),L=RR+1,R=L-1; Ans=0;
memset(f,0,sizeof(f));
for(;id==belong[q[i].l];i++)
{
//在同一块中直接暴力解决
if(belong[q[i].l]==belong[q[i].r]){ans[q[i].id]=blf(q[i].l,q[i].r);continue;}
// 不在同一块中的处理
while(R<q[i].r) add(++R);
LL ins=Ans;
while(L>q[i].l) add(--L);
ans[q[i].id]=Ans;
while(L<RR+1) del(L++);//每次询问完之后重新统计答案
Ans=ins;
}
return i;
}
int main()
{
n=read(); m=read(); block=sqrt(n);
for(int i=1;i<=n;i++)
{
a[i]=b[i]=read();
belong[i]=(i-1)/block+1;
num=max(num,belong[i]);
}
sort(b+1,b+n+1);
int tot=unique(b+1,b+n+1)-b-1;//去重
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+n+1,a[i])-b;//求有限制的最大值
for(int i=1;i<=m;i++) q[i].l=read(),q[i].r=read(),q[i].id=i;
sort(q+1,q+m+1,cmp);
for(int i=1,id=1;id<=num;id++) i=get(i,id);//枚举所有块
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
总结:
整个莫队算法的学习还算顺吧,其实我觉得莫队就是分块的优化,对询问进行排序,解决一切离线询问的题目,从而降低时间复杂度。