zoukankan      html  css  js  c++  java
  • 莫队学习笔记

    都0202年了怎么还会考根号数据结构呢?

    莫队是一种用于处理静态区间查询的一类方法,其的时间复杂度为 (O(nsqrt m+m))

    当然,由于其适用面之广,也出现了诸如带修莫队,在线莫队,二次离线莫队,树上莫队,二维莫队 以及套在一起 的变形。

    1.普通莫队

    首先我们要明白,莫队究竟是怎么优化暴力的。

    考虑这样一道题:给定 (n) 个数字,(m) 次询问区间 ([l_i,r_i]) 出现次数最多的数的出现次数。(n,m,a_ileq 10^5)

    这是最经典的区间众数,一般情况只有根号做法。莫队就是其中一种。

    考虑如何用莫队处理这件事:

    首先如果只有一次询问,是不是很好处理:直接用一个桶记录某个数字出现次数即可,然后更新的同时处理最大值。

    那么再考虑如果保证询问 (forall i<jleq m , l_ileq l_j , r_ileq r_j) 怎么处理?

    显然我们可以用类似于双指针的思想,不断加入右指针 (pr) 的值并向右移动直到 (pr=r_i),再不断删除左指针 (pl) 的值并向右移动直到 (pl=l_i)。复杂度 (O(n+m))

    再考虑如果保证询问 (forall i,jleq m , ext{若} l_i< l_j ext{那么} r_ileq r_j) 怎么处理?

    按左端点排个序就好了。复杂度 (O(nlog n))

    但是,几乎不会有题会有这种限制。如果单纯的按左端点排序,右指针移来移去,显然会被卡成 (O(n^2))

    所以现在我们就要一个新的排序方式,这就是莫队的精髓(没错,重点就在于排序)。

    我们发现,假如我们按左端点为第一关键字,右端点为第二关键字,那么右指针的运动距离是 (O(n^2)) 的,而左指针却只有 (O(n))

    显然这应该是可以优化的。具体来说,我们并不一定要求左指针严格递增,只要它不超过 (O(sqrt{n})) 就好了。

    这样我们得出了一种排序:首先先将所有点按某一数字 (B) 分块,即 (b_i=frac i B)

    然后对于一个区间,先按左指针的所在块排序,再按右指针排序,即 (b_{l_i}) 为第一关键字,(r_i) 为第二关键字。

    可以发现,由于最多只有 (frac n B) 个块,所以左指针移动路程为 (O(frac{n^2} B)),而右指针在每个块中最多移动 (O(n)) 步,所以移动路程为 (O(nB))

    所以总复杂度 (O(frac{n^2} B+nB)),显然当 (B=sqrt m) 时最小,即 (O(nsqrt m+m))

    例题太多了,洛谷题单里面较简单的应该都是普通莫队。

    虽然它是一个根号算法,但是它常数真的很小。它还有一些优化,其中比较有用的是奇偶优化,即对于奇数的右端点从左到右排,偶数的从右到左排。

    这样由于处理完奇数后右端点在最右端,不用移回左端点重新做,所以可以减小将近一半的常数。

    2.带修莫队

    虽然普通莫队很优秀,常数也比分块高明了不少,但是它终究还是静态的,如果加一个修改就没办法了。

    所以我们就需要一个变形:带修莫队。

    首先我们把修改操作也离线下来,看成一个时间。然后对于某个询问,也加上时间,即 ((l,r,t)) 表示询问 ([l,r]) 区间,并且在 (t) 时间操作后进行询问。

    然后我们把时间也看成一个指针。特别,如果加入的修改在当前区间里面,应当立刻对区间答案进行修改处理影响。

    那么这样,我们的排序就变成:先按 (b_{l_i}) 排序,再按 (b_{r_i}) 排序,再按 (t_i) 排序。

    分析复杂度:(O(frac{n^3} {B^2}+frac{n^2} B+nB))。取 (B=n^{frac 2 3})。得到 (O(n^{frac 5 3}))

    同样,常数还是很小。不要相信莫队的时间复杂度

    3.回滚莫队

    按照莫队的做法,我们需要支持一个数据结构,使之能够快速插入/删除一个数字。

    但事实上有很多数据结构删除会存在问题,比如并查集/线性基/求最大值。这时候普通莫队就会出现问题。

    那么,有没有一种莫队能够只插入呢?有的,这就是回滚莫队。

    具体来说,对于两个区间 ([l_1,r_1],[l_2,r_2]),如果 (l_1,l_2) 在同一个块中,那么该块的右端点到 (min(r_1,r_2)) 这段区间是公共的。

    可以发现,按照莫队的排序,如果左指针在同一个块中,那么右指针单调递增。所以右指针是不会有删除操作的。

    所以我们不妨每次先记录左指针移动前的答案和移动时的所有变化,在移动结束之后,将这部分变化和左指针归位,即“回滚”。

    特别的,对于左右端点在同一块内的情况,暴力处理即可。如果两次操作不同块,直接暴力清空所有数据,然后左右指针直接移动到新块的右端点。

    可以发现,由于在同一块内,每次左指针移动距离 (O(sqrt n)),复原一次 (O(sqrt n)),右端点一个块中均摊 (O(m)),所以总复杂度还是 (O(nsqrt m+msqrt n))

    例:[joisc2014]歴史の研究

    4.树上莫队

    详见 [WC2013] 糖果公园。其实树上莫队应该叫树+莫队,因为莫队处理的还是序列。

    5.在线莫队

    这个其实和普通莫队有一些出入了。毕竟莫队的本质应该就是那个排序(?)。

    首先,我们要处理的询问是允许差分的,即对于 ([l,r]_x)(x) 对于区间 ([l,r]) 的贡献)的信息,我们可以通过 ([1,r]_x)([1,l-1]_x) 推出。

    考虑强制在线时,我们不能得到上一次的结果。所以我们对 (B) 个区间分别设置一个关键点,然后处理所有 ([1,b_i]_x)。对于块与块之间的信息,我们只需要记录答案就行。

    这样预处理时空复杂度 (O(Bn+frac{n^2} B))

    询问的时候我们从最近的一段块转移过来,时间复杂度 (O(Bm))

    显然当 (B=sqrt n) 时总时间复杂度 (O(msqrt n)) 最优。

    6.二次离线莫队

    一个很神仙的算法,不过不是很难理解。

    当然使用的前提还是询问可差分.

    但是我们发现直接莫队,由于插入/删除的时间 (T(n)) 比较大(比如区间逆序对中采用树状数组是 (T(n)=O(log n))),总时间可能会退化成 (O(T(n)nsqrt n)) 而难以接受。

    考虑如何优化。我们可以把操作看成若干次 (pm [l,r]_x) 操作。差分后就是若干次 (pm [1,r]_x)

    而这个可以直接离线从左往右 (O(nT(n)+M)) 完成。这里 (M) 是莫队移动指针的次数,即 (M=O(msqrt n))

    这样时间已经足够优秀了,不过空间是 (O(n+M))(O(msqrt n)) 的,可能会爆炸。

    考虑如何优化。因为 ([1,r]_x)(x=loperatorname{或}roperatorname{或}l-1operatorname{或}r+1),对于2,4种我们完全可以先预处理出来,直接计入答案。

    考虑1,3种,我们移动左指针时右指针并不会移动,所以此时连续的 (x_i) 一定构成一个区间。我们直接存下这个区间就好了。

    这样空间复杂度 (O(n+m)),时间 (O((n+m)T(n)+msqrt n)),十分优秀。

    例:【模板】莫队二次离线
    首先 (aoperatorname{xor} b=cRightarrow aoperatorname{xor} c=b),而 (operatorname{popcount}(c)=k),所以我们可以枚举所有 (c),得到 (b)

    因为一次插入/删除操作只能用比较暴力的枚举,即一次 (inom {14} k),最坏大约有3000多次。

    显然我们无法接受 (O(msqrt ninom {14} k))。考虑二次离线。

    显然一个数字对区间的贡献 ([l,r]_x=[1,r]_x-[1,l-1]_x) 可以差分。按照上述方式就可以做到 (O(ninom {14} k+msqrt n)) 的优秀复杂度。

    细节比较多。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<vector>
    #include<algorithm>
    #define ll long long
    #define N 100010
    #define B 14
    #define M 16400
    using namespace std;
    int g[3500],f[M],tot;
    int bl[N],a[N];
    inline void ins(int x,int v){for(int i=1;i<=tot;i++) f[g[i]^x]+=v;}
    struct node{
    	int l,r,id;
    	bool operator <(const node a)const{return bl[l]==bl[a.l]?r<a.r:bl[l]<bl[a.l];}
    }q[N];
    ll ans[N];
    struct ques{
    	int l,r,id;//[1,x]_l~[1,x]_r
    };
    vector<ques>v[N];
    ll res1[N],res2[N];//[1,x]_{x+1},[1,x+1]_{x}
    int fl=1,fr,uid;
    void addqr()
    {
    	++fr;
    	if(v[fl-1].size() && v[fl-1][v[fl-1].size()-1].id==-uid) v[fl-1][v[fl-1].size()-1].r=fr;
    	else v[fl-1].push_back((ques){fr,fr,-uid});
    }
    void delqr()
    {
    	if(v[fl-1].size() && v[fl-1][v[fl-1].size()-1].id==uid) v[fl-1][v[fl-1].size()-1].l=fr;
    	else v[fl-1].push_back((ques){fr,fr,uid});
    	--fr;
    }
    void addql()
    {
    	if(v[fr].size() && v[fr][v[fr].size()-1].id==-uid) v[fr][v[fr].size()-1].r=fl;
    	else v[fr].push_back((ques){fl,fl,-uid});
    	++fl;
    }
    void delql()
    {
    	--fl;
    	if(v[fr].size() && v[fr][v[fr].size()-1].id==uid) v[fr][v[fr].size()-1].l=fl;
    	else v[fr].push_back((ques){fl,fl,uid});
    }
    int main()
    {
    	int n,m,k;
    	scanf("%d%d%d",&n,&m,&k);
    	for(int i=0;i<1<<B;i++)
    	if(__builtin_popcount(i)==k) g[++tot]=i;
    	for(int i=1;i<=n;i++) scanf("%d",&a[i]),bl[i]=i/344;
    	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);
    	for(uid=1;uid<=m;uid++)
    	{
    		while(fl<q[uid].l) addql();
    		while(fl>q[uid].l) delql();
    		while(fr<q[uid].r) addqr();
    		while(fr>q[uid].r) delqr();
    	}
    	for(int i=1;i<=n;i++)
    	{
    		res1[i]=f[a[i]];
    		ins(a[i],1);
    		res2[i]=f[a[i]];
    		for(int j=0;j<(int)v[i].size();j++)
    		{
    			for(int k=v[i][j].l;k<=v[i][j].r;k++)
    			{
    				if(v[i][j].id<0) ans[-v[i][j].id]-=f[a[k]];
    				else ans[v[i][j].id]+=f[a[k]];
    			}
    		}
    	}
    	fl=1,fr=0;
    	for(uid=1;uid<=m;uid++)
    	{
    		ans[uid]+=ans[uid-1];
    		while(fl<q[uid].l) ans[uid]+=res2[fl],fl++;
    		while(fl>q[uid].l) --fl,ans[uid]-=res2[fl];
    		while(fr<q[uid].r) ++fr,ans[uid]+=res1[fr];
    		while(fr>q[uid].r) ans[uid]-=res1[fr],fr--;
    	}
    	for(int i=1;i<=m;i++) res1[q[i].id]=ans[i];
    	for(int i=1;i<=m;i++) printf("%lld
    ",res1[i]);
    	return 0;
    }
    

    7. 套一起

    总结:莫队是一个十分优秀的算法。虽然赛场上不一定会作为标程(眼泪的时代),就算出了我也不会写。但其也不乏为一个优秀的骗分技巧。

  • 相关阅读:
    ajax traditional
    阿里云OSS NET SDK 引用示范程序
    js对象的两种写法
    BZOJ NOIP提高组十连测第一场
    ikbc 时光机 F87 Ctrl 失灵 解决办法
    【读书笔记】阅读的危险
    51nod 1118 机器人走方格 解题思路:动态规划 & 1119 机器人走方格 V2 解题思路:根据杨辉三角转化问题为组合数和求逆元问题
    【算法】求逆元模板
    【复习资料】软件工程之快速原型模型
    VirtualBox安装linux mint教程
  • 原文地址:https://www.cnblogs.com/Flying2018/p/13626542.html
Copyright © 2011-2022 走看看