莫队 学习笔记

莫队 学习笔记

引入

莫队算法是一种优美的暴力算法,可以用来处理大量的区间询问。前提是,维护的信息可以高效地插入、删除。

我们就以一道题为例,来初探莫队:洛谷P3901 数列找不同

题意:给定一个数列,\(q\) 次询问,问区间 \([l, r]\) 内的数是否各不相同。

首先,我们很容易想到,问某个区间内的数各不相同,等价于去问这个区间的长度是否等于其中不同数的个数。那对于一个询问,我们先考虑暴力做:拿一个桶维护一下区间内每个数的个数,以及区间内不同种数的个数,从区间左侧扫到区间右侧,求出答案。但是,如果暴力去做,最后复杂度是 $q \times n $ 的,因为可能每次询问都要把整个数列扫描一遍。

我们又发现,其是我们并不需要每次都从头扫,因为有些区间的信息是重复的,那我们就要多利用重复的信息,于是就有了第二个思路:把询问离线下来,按左端点为第一关键字,右端点为第二关键字排序,用双指针来维护元素的插入和删除。这样,左端点的数就可以保证利用充分,但是,如果左端点各不相同,右端点还是会左右横跳,复杂度还是逃不过 \(q \times n\)

现在问题就变为,如何进行排序,来让每次左右端点尽量少移动,于是就有了莫队。

普通莫队

思想

莫队把区间分块,把询问以左端点所在的块为第一关键字,右端点为第二关键字进行排序,让我们来看一看这样做后的复杂度:

我们设块长为 \(size\)。首先,对于每个块内的询问,每次询问后左端点最多移动 \(size\) 次,那么所有询问后左端点移动的次数就是 \(q \times size\);然后我们来考察右端点。我们发现,在每个块内,右端点的移动都是单调不减的,可以做到线性;对于两个块之间的转换,右端点最多从最右侧移动到最左侧,也就是说,右端点最多移动 \(n \times \frac{n}{size}\) 次。又因为 \(n\)\(q\) 同阶,所以最终,莫队的复杂度就是 \(O(n \times size + n\times \frac{n}{size})\)

根据基本不懂不等式,我们发现当 \(size = \sqrt{n}\) 时,复杂度最小。所以按照 \(\sqrt{n}\) 分块,复杂度就变成 \(O(n \sqrt{n})\) 了。

一点点细节:左指针 \(l\) 初值最好赋成 \(1\),防止出现修改 \(a[0]\) 的情况(修改了不存在的值)。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+100;

inline int read(){
    int x = 0; char ch = getchar();
    while(ch<'0' || ch>'9') ch = getchar();
    while(ch>='0'&&ch<='9') x = x*10+ch-48, ch = getchar();
    return x;
}
int bel[N];
struct Question{
    int l, r, id;
    int part;
    bool operator <(const Question &b) const{
        if(part == b.part){
            if(part & 1) return r<b.r;//玄学优化,按照奇偶性分别对右端点进行升/降序排序,让每次右端点回退最少。
            else return r>b.r;
        } else return part<b.part;
    }
}q[N];
bool ans[N];
int a[N], n;
int Q;
int l = 1, r = 0;

int vis[N], tot;
void del(int pos){
    vis[a[pos]]--;
    if(!vis[a[pos]]) tot--;
}
void add(int pos){
    if(!vis[a[pos]]) tot++;
    vis[a[pos]]++;
}

int main(){
    n = read(), Q = read();
    int lth = sqrt(n);
    for(int i = 1; i<=n; ++i) a[i] = read();
    for(int i = 1; i<=Q; ++i){
        q[i].l = read(), q[i].r = read();
        q[i].id = i;
        q[i].part = ((q[i].l-1)/lth)+1;
    }
    sort(q+1, q+Q+1);
    for(int i = 1; i<=Q; ++i){
        while(l<q[i].l) del(l++);
        while(r>q[i].r) del(r--);
        while(l>q[i].l) add(--l);
        while(r<q[i].r) add(++r);
        ans[q[i].id] = (tot == (r-l+1));
    }
    for(int i = 1; i<=Q; ++i){
        ans[i]?puts("Yes"):puts("No");
    }
    return 0;
}

例题

洛谷P4396 作业

我们发现两个询问都可以用树状数组维护,只不过维护的信息有所差异:一个维护存在与否,另一个维护数量。剩下的就是普通莫队了。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+100;

inline int read(){
    int x = 0; char ch = getchar();
    while(ch<'0' || ch>'9') ch = getchar();
    while(ch>='0'&&ch<='9') x = x*10+ch-48, ch = getchar();
    return x;
}
int n, m;
int a[N];

inline int lowbit(int x){
    return x&(-x);
}
struct TC{//树状数组
    int tc[N];
    void insert(int pos, int val){
        for(int i = pos; i<=n; i+=lowbit(i)){
            tc[i]+=val;
        }
    }
    int query(int pos){
        int ret = 0;
        for(int i = pos; i; i-=lowbit(i)){
            ret+=tc[i];
        }
        return ret;
    }
}ta, tb;//a:出现定义域内数的个数 b:出现定义域内数的种类数
struct Question{
    int l, r, part, id;
    int x, y;
    bool operator < (const Question &b) const{
        if(part == b.part){
            if(part & 1) return r<b.r;
            else return r>b.r;
        } else{
            return part < b.part;
        }
    }
}q[N];

int l = 1, r, vis[N];
void add(int x){
    if(!vis[a[x]]) tb.insert(a[x], 1);//如果新加入一个数,存在性+1,删去同理
    ta.insert(a[x], 1);//数量+1
    vis[a[x]]++;
}

void del(int x){
    vis[a[x]]--;
    ta.insert(a[x], -1);
    if(!vis[a[x]]) tb.insert(a[x], -1);
}
int ansa[N], ansb[N];
int main(){
    n = read(), m = read();
    for(int i = 1; i<=n; ++i) a[i] = read();
    int lth = sqrt(n);
    for(int i = 1; i<=m; ++i){
        q[i].l = read(), q[i].r = read(), q[i].x = read(), q[i].y = read();
        q[i].part = (q[i].l/lth)+1;
        q[i].id = i;
    }
    sort(q+1, q+m+1);
    for(int i = 1; i<=m; ++i){
    	while(r>q[i].r) del(r--);
        while(r<q[i].r) add(++r);
        while(l<q[i].l) del(l++);
        while(l>q[i].l) add(--l);
        ansa[q[i].id] = ta.query(q[i].y)-ta.query(q[i].x-1);
        ansb[q[i].id] = tb.query(q[i].y)-tb.query(q[i].x-1);
    }
    for(int i = 1; i<=m; ++i){
        printf("%d %d\n", ansa[i], ansb[i]);
    }
    return 0;
}

回滚莫队

在刚才的问题中,我们需要修改的信息往往都是易于撤销的,但是有时候,我们要维护一些添加容易删除难,或删除容易添加难的信息,该如何实现呢?

我们以歴史の研究为例。

题意:给定一个数列,和一些询问,每次询问区间内出现过的数与其出现次数乘积的最大值。

这里的最大值就是一个典型的便于添加不便于删除的信息,因为你每次的最大值都会覆盖上一次。怎么办呢?

我们就会想:能不能不删呢?也就是说,我们想办法让信息只添加不删除。

我们回到莫队上来。我们发现,排序的时候,每一个块内右端点是单调不减的,也就是说,这一部分可以实现只加不删;而左端点是乱序的。这时候就可以通过“回滚”来解决这一问题了:我们对于在同一个块内的左右端点,暴力求解;如果左右端点在不同的块,就让左右指针都指向左端点所在块的块尾(因为右端点一定在这个块之后),每次先移动右指针,添加信息,并记录,然后再移动左指针,添加信息。每个询问处理完后,都要把左指针更新的信息清空,答案恢复到左指针移动之前。

这样做的正确性有无保证呢?首先,如果是同一块内,暴力求肯定没问题;如果两个指针在不同的块,那么右指针只会单调不减向右移动来添加信息,所以对于左端点在同一个块内的询问,右指针的信息是不会被破坏的;我们再考虑左指针,每次通过回滚,就可以实现撤销操作。

关于复杂度,同一个块内是 \(\sqrt{n}\) 的,每次回滚也是 \(\sqrt{n}\) 的,所以复杂度其实没有变,变大的只是常数。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+100;

int n, Q;
int prt[N];
inline int read(){
    int x = 0; char ch = getchar();
    while(ch<'0' || ch>'9') ch = getchar();
    while(ch>='0'&&ch<='9') x = x*10+ch-48, ch = getchar();
    return x;
}
struct node1{
    int x, id;
    bool operator <(const node1 &b){
        return x<b.x;
    }
}ori[N];

int a[N], fa[N];
void init(){//离散化
    sort(ori+1, ori+1+n);
    int lst = 0, cnt = 0;
    for(int i = 1; i<=n; ++i){
        if(lst ^ ori[i].x) lst = ori[i].x, cnt++;
        a[ori[i].id] = cnt;
        fa[cnt] = ori[i].x;
    }
}

struct Question{
    int l, r, part, id;
    bool operator < (const Question &b) const{
        if(part == b.part) return r<b.r;
        return part<b.part;
    }
}q[N];

long long vis[N], ans[N];
long long visb[N];
int lp[N], rp[N];
int l = 1, r;

long long as1, as2;

void mo(){
    int bl = 0;
    for(int i = 1; i<=Q; ++i){
        if(prt[q[i].l] == prt[q[i].r]){
            for(int j = q[i].l; j<=q[i].r; ++j){//暴力求解,注意不要混用数组
                visb[a[j]]++;
                ans[q[i].id] = max(ans[q[i].id], visb[a[j]]*fa[a[j]]);
            }
            for(int j = q[i].l; j<=q[i].r; ++j){
                visb[a[j]]--;
            }
        } else{
            int now = prt[q[i].l];
            if(bl ^ now){//换块后,要把上一个块询问的所有信息清空
                as1 = 0;
                for(int j = l; j<=r; ++j) vis[a[j]]--;
                l = rp[now];
                r = l-1;
                bl = now;
            }
            while(r<q[i].r){
                ++r;
                vis[a[r]]++;
                as1 = max(as1, vis[a[r]]*fa[a[r]]);
            }
            as2 = as1;
            while(l>q[i].l){
                --l;
                vis[a[l]]++;
                as2 = max(as2, vis[a[l]]*fa[a[l]]);
            }//移动左指针添加信息
            ans[q[i].id] = as2;//记录答案
            while(l<rp[now]){//清楚左指针的贡献
                vis[a[l]]--;
                ++l;
            }
        }
    }
}

int main(){
    n = read(), Q = read();
    int lth = sqrt(n);
    for(int i = 1; i<=n; ++i) prt[i] = (i-1)/lth+1, ori[i].x = read(), ori[i].id = i;
    init();
    for(int i = 1; i<=Q; ++i){
        q[i].l = read(), q[i].r = read();
        q[i].part = prt[q[i].l];
        q[i].id = i;
    }
    for(int i = 1; i<=prt[n]; ++i){
        rp[i] = (i == prt[n]) ? n:i*lth;
    }
    sort(q+1, q+Q+1);
    mo();
    for(int i = 1; i<=Q; ++i){
        printf("%lld\n", ans[i]);
    }
    system("pause");
    return 0;
}
posted @ 2023-06-26 20:26  霜木_Atomic  阅读(16)  评论(0编辑  收藏  举报