zoukankan      html  css  js  c++  java
  • 莫队+带修莫队 及优化详解

    莫队

    莫队算法(Mo's algorithm)莫涛队长发明的算法,尊称莫队。
    先膜一下莫队(\%\%\%)莫涛 - 知乎

    思路A:two pointers处理

    two pointers处理是一种优美的暴力。

    例如此题:P3901 数列找不同

    现有数列 (A_1,A_2,ldots,A_N)(M)个询问 ((L_i,R_i)),询问 (A_{L_i} ,A_{L_i+1},ldots,A_{R_i})是否互不相同。

    (N,Mle 10^5)

    (以下取(N,M)同阶)

    扩展一下这个问题,变成"求((L{i},R_i))区间内有多少对数字相同".

    如果完完全全暴力的话,开类似于桶排序的桶,每次在((L,R))的区间内更新桶,并计算答案,复杂度(O(N^3)).

    莫队①优化一下这个过程:

    • 用两个指针(pl)(pr),两个指针所指的标号内是我们维护的区间。于是我们可以仅移动这两个指针来维护区间。
    • 每一步移动的复杂度是(O(1)).
    • (pans)表示这个区间内的答案

    对于移动指针的过程,假设要将指针移动到((l,r))

    • 对于移动左指针((pl o l)
      • ①若(pl > l),表示当前区间左端点短所求,则一步步向左移动左端点。
        • 此时:每移动左端点一次,相当于向区间内增加一个(a[pl-1]),对答案的贡献为当前已有的(a[pl-1])的个数,即(buc[a_{[pl-1]}]).
        • 故此处为add(--pl).
      • ②若(pl<l),表示当前区间左端点长于所求,则一步步向右移动左端点。
        • 此时:每移动左端点一次,相当于向区间内减少一个(a[pl]),对答案的贡献为当前已有的(a[pl])的个数(-1),即(buc[a_{[pl]}])(-1).
        • 故此处为del(pl++).
    • 对于移动右指针((pr o r)
      • (pr < r),与上①相同
        • add(++pr).
      • (pr > r),与上②相同
        • del(pr--).
    • 关于add:在计算后要将(buc++),故为qans += buc[a[k]]++.
    • 关于del:先(-1)再计算,故为qans -= --buc[a[k]];

    将每一次进行add()del()称为一次扩展

    另外,(pl)(pr)初值应赋为(1)(0),此时表示扩展内没有任何元素。


    关于扩展的代码:

    ll pans;int pl=1,pr;
    inline void add(int k){pans += buc[a[k]]++;}
    inline void del(int k){pans -= --buc[a[k]];}
    inline int calc(int l,int r){
    	while(pl > l)add(--pl);
    	while(pr < r)add(++pr);
    	while(pl < l)del(pl++);
    	while(pr > r)del(pr--);
    	return pans;
    }
    

    此时一次扩展的复杂度是(O(1))的。

    这样可以将复杂度压缩到(O(M*N)).
    但是它还会TLE。如何进一步优化?



    思路A+

    对于以上算法,复杂度的上限是什么?

    假设有以下(N=10^5)((L_i,R_i))查询数据:

    1 1
    100000 100000
    1 1 
    100000 100000
    ………
    

    会发生什么?

    每次操作,都会将(pl:1 o Nspace ,space pr:1 o N)(pl:N o 1space,space pr:N o1)

    这样会出现很多很多次无用的扩展

    考虑如何优化这些扩展:

    莫队的思路是分块

    取正整数(B),把序列每(B)个分为一段,即分段长度是(B).

    对于左端点在同一块内的相邻询问,右端点递增,一共至多扩展(N) 次,左端点每次询问至多扩展(B) 步;对于左端点不在同一块内的相邻询问,至多有(frac nB) 个。复杂度为(O(Nfrac NB + BN)) 次扩展的复杂度。
    利用均值不等式,(Nfrac NB+BN ge2sqrt{Nfrac NB*BN}=2Nsqrt N),当且仅当 $B =sqrt N $时等号成立。

    故取(B=sqrt N),最优复杂度为(O(Nsqrt N)) 次扩展的复杂度。

    这样分块后,对于询问的区间,按左端点所在块的编号为第一关键字,右端点为第二关键字从小到大排序。按顺序求每个区间的答案,每次直接从上一个区间暴力扩展移动到这个区间。

    所以可以知道:莫队一定是离线的




    代码实现:

    开一个(bel[i])数组记录(i)所在块的编号,易得(bel[i] = frac i B + 1).
    用结构体记录每次查询的区间信息和编号。
    按以上方法排序后,每次操作将所得值还原为原顺序最后输出即可。

    struct ask{int l,r,id;}q[N];
    inline bool operator < (const ask a,const ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
    bool ans[N];
    
    main:{
    	len = sqrt(n);
    	for(int i:1->n)
    		bel[i] = i / len + 1;
    	for(int i:1->m)
    		q[i].l = read() , q[i].r = read() , q[i].id = i;
    	sort(q+1,q+m+1);
    	for(int i:1->m)
    		ans[q[i].id]= calc(q[i].l,q[i].r);
        for(int i:1->m)
            print(ans[i]);
    }
    

    总复杂度为(O(Nsqrt N)).

    数列找不同AC代码:

    #include <bits/stdc++.h>
    #define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout);
    using namespace std;
    const int INF = 0x3f3f3f3f,N = 1e5+5;
    typedef long long ll;
    typedef unsigned long long ull;
    inline ll read(){
    	ll ret = 0 ;char ch = ' ' , c = getchar();
    	while(!(c >= '0' && c <= '9'))ch = c , c = getchar();
    	while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
    	return ch == '-' ? -ret : ret;
    }
    int n,m;
    int a[N],buc[N],bel[N];
    int len;
    ll qans;int pl=1,pr;
    inline void add(int k){qans += buc[a[k]]++;}
    inline void del(int k){qans -= --buc[a[k]];}
    inline int calc(int l,int r){
    	while(pl > l)add(--pl);
    	while(pr < r)add(++pr);
    	while(pl < l)del(pl++);
    	while(pr > r)del(pr--);
    	return qans;
    }
    struct ask{int l,r,id;}q[N];
    inline bool operator < (const ask a,const ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
    bool ans[N];
    signed main(){
    	n = read() , m = read();
    	len = ceil(sqrt(n));
    	for(int i = 1 ; i <= n ; i ++)
    		a[i] = read(),
    		bel[i] = i / len + 1;
    	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);
    	for(int i = 1 ; i <= m ; i ++){
    		int l = q[i].l , r = q[i].r , id = q[i].id;
    		ans[id]= !calc(l,r);
    	}
    	for(int i = 1 ; i <= m ; i ++)
    		printf("%s
    ",ans[i]?"Yes":"No");
    	return 0;
    }
    



    基本的莫队已经结束了。我们来看一道例题:P1494 [国家集训队] 小Z的袜子

    给定序列(A_n),每次询问查询((L_i,R_i))区间内任选两个数,选到相同数字的概率。

    (nle5 imes 10^5)

    把上面一题的扩展"求((L{i},R_i))区间内有多少对数字相同"稍作修改即可。

    答案为(frac{calc(l,r)}{C^2_{r-l+1}})

    约分时,上下同除(gcd)就可以。

    #include <bits/stdc++.h>
    #define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout);
    using namespace std;
    const int INF = 0x3f3f3f3f,N = 5e4+5;
    typedef long long ll;
    typedef unsigned long long ull;
    inline ll read(){
    	ll ret = 0 ;char ch = ' ' , c = getchar();
    	while(!(c >= '0' && c <= '9'))ch = c , c = getchar();
    	while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
    	return ch == '-' ? -ret : ret;
    }
    int n,m;
    int a[N],buc[N],bel[N],len;
    ll qans;int pl=1,pr;
    inline void add(int k){qans += buc[a[k]]++;}
    inline void del(int k){qans -= --buc[a[k]];}
    inline int calc(int l,int r){
    	while(pl > l)add(--pl);
    	while(pr < r)add(++pr);
    	while(pl < l)del(pl++);
    	while(pr > r)del(pr--);
    //	printf("(%d,%d):%lld
    ",pl,pr,ans);
    	return qans;
    }
    struct ask{int l,r,id;}q[N];
    inline bool operator < (ask a,ask b){return bel[a.l] != bel[b.l] ? a.l < b.l : a.r < b.r;}
    ll gcd(ll x,ll y){return y ? gcd(y,x%y):x;}
    ll ans[N][2];
    signed main(){
    	n = read() , m = read();
    	len = ceil(sqrt(n));
    	for(int i = 1 ; i <= n ; i ++)
    		a[i] = read(),
    		bel[i] = i/len + 1;
    	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);
    	for(int i = 1 ; i <= m ; i ++){
    		int l = q[i].l , r = q[i].r , id = q[i].id;
    		if(l == r){ans[id][0] = 0,ans[id][1] = 1;continue;}
    		ans[id][0] = calc(l,r),ans[id][1] = 1ll * (r-l+1) * (r-l) / 2;
    		ll k = gcd(ans[id][0],ans[id][1]);
    		ans[id][0] /= k,ans[id][1] /= k;
    	}
    	for(int i = 1 ; i <= m ; i ++)
    		printf("%lld/%lld
    ",ans[i][0],ans[i][1]);
    	return 0;
    }
    



    优化

    对于普通的莫队,还是存在一些可优化的地方的。下面我们来具体分析。





    奇偶优化

    如图(红箭头表示(pl)的移动,绿箭头表示(pr)的移动)

    可以发现,对于左端点进入新的一个区间时,右端点需要从(N)处回到最左边,再跑回到最右边。这样也是进行了很多次多余的扩展

    提供一种奇偶优化的方案:

    对于分块标号为奇数的,按(p[i].r)升序排列,反之偶数按(p[i].r)降序排列。效果如下:

    只需将排序一处的代码修改:

    • 如果(a.l)(b.l)在同一个块内,则:
      • 如果所在块编号是奇数,则按(a.l<b.l)排列;
      • 如果所在块编号是偶数,则按(a.l>b.l)排列。
    • 如果不在同一个块内,则左端点编号小的在前。

    所以代码为:

    inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? (bel[a.l] & 1 ? a.r < b.r : a.r > b.r): a.l < b.l;}
    

    这个优化是优化在常数,不过可以使大数据快一倍。





    带修莫队

    前面我们说到,莫队一定是离线的。但是如果遇到一些题目,要求修改?
    例如P1903 [国家集训队]数颜色 / 维护队列

    (A_N)的序列和(M)次操作,每次操作进行查询((L,R))区间内不同的数字个数(A_p)修改为(x).

    (N,M le 1.5*10^5).

    题里要求必须支持修改。

    我们来考虑如何修改。

    对于普通的莫队,我们的操作是:

    • 每次维护(pl)(pr)((L,R))的位置。

    那么对于修改呢?

    莫队提供的方案是:增加一个时间轴(T).

    • 可以增加一个变量(now),表示当前处理的询问之前执行过多少次修改。
    • 对于每一次查询,记录当前查询之前进行多少修改。

    这样,可以把每次扩展改成:

    inline int calc(int l,int r,int id,int t){
    	while(pl > l)add(--pl);
    	while(pr < r)add(++pr);
    	while(pl < l)del(pl++);
    	while(pr > r)del(pr--);
    	while(now < t)mod(++now,id);
    	while(now > t)mod(now--,id);
    	return pans;
    }//其中的6,7行为带修莫队新增。
    

    其中,我们在扩展需要额外传入(id,t)两个参量。

    对于(mod)(modify)函数,写成如下:

    inline void mod(int k,int id){
    	if(q[id].l <= mo[k].p && mo[k].p <= q[id].r)
    		pans ......  ;
    	swap(mo[k].v,a[mo[k].p]);
    }
    

    首先,所对应的修改位置在当前查询的区间内才需要修改(pans). 修改(pans)的操作因题而定。

    比如:例题,则为pans += !buc[mo[k].v]++ , pans -= !--buc[a[mo[k].p]];,表示分别对修改前和修改后进行答案贡献计算。

    最后的swap​比较巧妙:直接接将序列内的值修改操作的值进行调换,这样能保证多次修改,进行(修改->还原->修改(cdots))的操作。

    此时,我们仍要修改排序

    • 考虑普通莫队的排序:先比较(a.l,b.l),再比较(a.r,b.r).
    • 那么:对于添加了一维的带修莫队,就可以写成:
      • 先比较(a.l,b.l),再比较(a.r,b.r),最后比较(a.t,b.t).
    inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? bel[a.r] == bel[b.r] ? a.t<b.t : a.r<b.r : a.l < b.l;}
    

    复杂度分析

    等以后慢慢写……

    所以代码

    #include <bits/stdc++.h>
    #define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
    using namespace std;
    const int INF = 0x3f3f3f3f , N = 1.4e5+5 , M = 1e6+5;
    typedef long long ll;
    typedef unsigned long long ull;
    inline ll read(){
    	ll ret = 0 ; char ch = ' ' , c = getchar();
    	while(!(c >= '0' && c <= '9')) ch = c , c = getchar();
    	while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
    	return ch == '-' ? - ret : ret;
    }
    int n,m;
    int a[N];
    int pl=1,pr,pans,len,bel[N];
    int buc[M],now;
    int ans[N];
    struct ask{int l,r,id,t;}q[N];int qcnt;
    inline bool operator < (const ask a,const ask b){return bel[a.l] == bel[b.l] ? bel[a.r] == bel[b.r] ? a.t<b.t : a.r<b.r : a.l < b.l;}
    struct mdf{int p,v;}mo[N];int mcnt;
    inline void mod(int k,int id){
    	if(q[id].l <= mo[k].p && mo[k].p <= q[id].r)
    		pans += !buc[mo[k].v]++,
    		pans -= !--buc[a[mo[k].p]];
    	swap(mo[k].v,a[mo[k].p]);
    }
    inline void add(int k){pans += !buc[a[k]]++;}
    inline void del(int k){pans -= !--buc[a[k]];}
    inline int calc(int l,int r,int id,int t){
    	while(pl > l)add(--pl);
    	while(pr < r)add(++pr);
    	while(pl < l)del(pl++);
    	while(pr > r)del(pr--);
    	while(now < t)mod(++now,id);
    	while(now > t)mod(now--,id);
    	return pans;
    }
    signed main(){
    	n = read() , m = read();
    	len = pow(n,2.0/3);
    	for(int i = 1 ; i <= n ; i ++)
    		a[i] = read(),
    		bel[i] = i / len + 1;
    	for(int i = 1 ; i <= m ; i ++){
    		char ch[2];int x,y;
    		scanf(" %s %d %d",ch,&x,&y);
    		if(ch[0] == 'Q')q[++qcnt] = (ask){x,y,qcnt,mcnt};
    		else mo[++mcnt] = (mdf){x,y};
    	}
    	sort(q+1,q+qcnt+1);
    	for(int i = 1 ; i <= qcnt ; i ++)
    		ans[q[i].id] = calc(q[i].l,q[i].r,i,q[i].t);
    	for(int i = 1 ; i <= qcnt ; i ++)
    		printf("%d
    ",ans[i]);
    	return 0;
    }
    

    剩下的优化还不会,等以后再来更新吧……


    例题

    P2709 小B的询问【板子中的板子】
    P1972 [SDOI2009] HH的项链【玄学优化】

  • 相关阅读:
    LightOJ 1341 Aladdin and the Flying Carpet 数学
    NOIP2013 花匠 DP 线段树优化
    LightOJ 1370 Bi-shoe and Phi-shoe 欧拉函数+线段树
    BZOJ2818: Gcd 欧拉函数求前缀和
    SPOJ3267 D-query 离线+树状数组 在线主席树
    BZOJ 2588: Spoj 10628. Count on a tree 主席树+lca
    拓展欧几里得算法
    POJ1845Sumdiv(求所有因子和 + 唯一分解定理)
    UVA1635 Irrelevant Elements(唯一分解定理 + 组合数递推)
    codeforce 626E(二分)
  • 原文地址:https://www.cnblogs.com/Shinomiya/p/14906834.html
Copyright © 2011-2022 走看看