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的项链【玄学优化】

  • 相关阅读:
    HDU 1525
    kmp模板
    hdu 4616 Game(树形DP)
    hdu 4619 Warm up 2(并查集活用)
    hdu 4614 Vases and Flowers(线段树加二分查找)
    Codeforces 400D Dima and Bacteria(并查集最短路)
    poj 2823 Sliding Window (单调队列)
    hdu 2196 Computer(树形dp)
    hdu 4604 Deque
    最短路径
  • 原文地址:https://www.cnblogs.com/Shinomiya/p/14906834.html
Copyright © 2011-2022 走看看