莫队算法

莫队算法

莫队算法是什么

莫队算法主要是用来离线查询区间答案。一般分为两类:一是莫队维护区间答案,二是维护区间的数据结构。还有树上莫队,带修改莫队,二维莫队等等。

一道例题:SPOJ-DQUERY - D-query

题意

给你一个长度不大于30000的序列,其中数值都小于等于 1 0 6 10^6 106,有1

思路:

普通暴力肯定是过不了的,我们可以尝试优化一下:

优化1:每次枚举到一个数值num,增加出现次数时判断一下 c n t n u m cnt_num cntnum是否为0,如果为0,则这个数值之前没有出现过,现在出现了,那么结果+1。反之在区间中删除num后也判断一下 c n t n u m cnt_num cntnum是否为0,如果为0数值总数-1。

优化2:我们可以用两个指针l,r,每次询问不直接枚举,而是移动l,r指针到询问的区间,直到l,r与询问区间重合。在统计答案时,我们也只在指针处加减cnt,然后我们用优化1中的方法快速地统计答案。

优化3:我们可以考虑把所有查询区间按左端点排序,从而使左指针最多移动O(n)次。但这样的话右端点又是无序的,又让整体复杂度变回原来。莫队算法的核心是分块和排序。我们将大小为n的序列分为 s q r t n sqrt{n} sqrtn块,从1到 s q r t n sqrt{n} sqrtn编号,然后根据这个对查询区间进行排序。一种方法是把查询区间按照左端点所在块的序号排序,如果左端点所在块相同,再按右端点排序。排完序后我们再进行左右指针跳动操作。

关于复杂度:

区间排序:建个结构体,用sort排一遍,复杂度为 O ( n ∗ l o g n ) O(n*logn) O(nlogn)

左指针的移动: O ( n ∗ n ) O(n*\sqrt{n}) O(nn )

右指针的移动: O ( n ∗ n ) O(n*\sqrt{n}) O(nn )

得出莫队的总时间复杂度为 O ( n ∗ n ) O(n*\sqrt{n}) O(nn )

关于排序:

int cmp(query a, query b) {
    return belong[a.l] == belong[b.l] ? a.r < b.r : belong[a.l] < belong[b.l];
}
重点来了:莫队卡常技巧

1、#pragma GCC optimize(2)

开O2优化

2、莫队玄学奇偶性排序

主要操作:把查询区间的排序函数

int cmp(query a, query b) {
    return belong[a.l] == belong[b.l] ? a.r < b.r : belong[a.l] < belong[b.l];
}

改为:

int cmp(query a, query b) {
    return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}

3、移动指针的常数压缩

void add(int pos) {
    if(!cnt[aa[pos]]) ++now;
    ++cnt[aa[pos]];
}
void del(int pos) {
    --cnt[aa[pos]];
    if(!cnt[aa[pos]]) --now;
}
int main()
{
    while(l < ql) del(l++);
    while(l > ql) add(--l);
    while(r < qr) add(++r);
    while(r > qr) del(r--);
}

压缩为

while(l < ql) now -= !--cnt[aa[l++]];
while(l > ql) now += !cnt[aa[--l]]++;
while(r < qr) now += !cnt[aa[++r]]++;
while(r > qr) now -= !--cnt[aa[r--]];

4、加快读、快输

本题的代码:

#include 
using namespace std;
#pragma GCC optimize(2)
const int maxn = 1e6+10;
int aa[maxn],cnt[maxn], belong[maxn];
int n, m, size, bnum, now, ans[maxn];
struct query{
    int l, r, id;
}q[maxn];
int cmp(query a, query b)
{
    return ((belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : (belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
inline int read()
{
    int s = 0 ,f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9') {
        if(ch == '-')
            f = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9') {
        s = s * 10 + ch - '0';
        ch = getchar();
    }
    return s*f;
}
inline void write(int x){
    if(x < 0) {
        putchar('-');
        x = -x;
    }
    if(x > 9 ) {
        write(x / 10);
    }
    putchar(x%10+'0');
}
int main()
{
    n = read();
    size = sqrt(n); //每块的规模
    bnum = ceil((double)n / size); //bnum代表分的块数,ceil()向上取整, floor()向下取整, round()四舍五入,
    for(int i = 1; i <= bnum; i++) {
        for(int j = (i-1) * size + 1; j <= i * size; j++) {
            belong[j] = i;
        }
    }
    for(int i = 1; i <= n; ++i) {
        aa[i] = read();
    }
    m = read();
    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);
    int l = 1, r = 0;
    for(int i = 1; i <= m; i++) {
        int ql = q[i].l, qr = q[i].r;
        while(l < ql) {
            now -= !--cnt[aa[l++]];
        }
        while(l > ql) {
            now += !cnt[aa[--l]]++;
        }
        while(r > qr) {
            now -= !--cnt[aa[r--]];
        }
        while(r < qr) {
            now += !cnt[aa[++r]]++;
        }
        ans[q[i].id] = now;
    }
    for(int i = 1; i <= m; i++) {
        write(ans[i]);
        printf("\n");
    }
    return 0;
}

莫队算法的扩展——带修改的莫队

例题:BZOJ-2120: 数颜色
题意:
Q操作来查询从第L支画笔到第R支画笔中共有几种不同颜色的画笔。
R操作来把第P支画笔替换为颜色Col。
思路:

可以把莫队加上一维,变为带修莫队。

那么加上一维什么呢?具体怎么实现?我们的做法是把修改操作编号,称为"时间戳",而查询操作的时间戳沿用之前最近的修改操作的时间戳。跑主算法时定义当前时间戳为 tt ,对于每个查询操作,如果当前时间戳相对太大了,说明已进行的修改操作比要求的多,就把之前改的改回来,反之往后改。只有当当前区间和查询区间左右端点、时间戳均重合时,才认定区间完全重合,此时的答案才是本次查询的最终答案。

通俗地讲,就是再弄一指针,在修改操作上跳来跳去,如果当前修改多了就改回来,改少了就改过去,直到次数恰当为止。

这样,我们当前区间的移动方向从四个([l−1,r][l−1,r]、[l+1,r][l+1,r]、[l,r−1][l,r−1]、[l,r+1][l,r+1])变成了六个([l−1,r,t][l−1,r,t]、[l+1,r,t][l+1,r,t]、[l,r−1,t][l,r−1,t]、[l,r+1,t][l,r+1,t]、[l,r,t−1][l,r,t−1]、[l,r,t+1][l,r,t+1])

带修改莫队的排序:

int cmp(query a, query b) {
    return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.r] ^ belong[b.r]) ? belong[a.r] < belong[b.r] : a.time < b.time);
}

分块大小和复杂度:

有的dalao证明了当块的大小设 n 2 / 3 n^{2/3} n2/3时理论复杂度达到最优.
代码:

#include 
using namespace std;
const int maxn = 50500;
const int maxc = 1001000;
struct node{
    int l, r, id, time;
}q[maxn];
struct node2{
    int pos, color;
}c[maxn];
int n, m;
int a[maxn], cnt[maxc], belong[maxn], ans[maxn];
int tot = 0, tot2 = 0, size, bnum;
bool cmp(node xx, node yy)
{
    return (belong[xx.l] ^ belong[yy.l]) ? belong[xx.l] < belong[yy.l] : ((belong[xx.r] ^ belong[yy.r]) ? belong[xx.r] < belong[yy.r] : xx.time < yy.time);
}
inline int read()
{
    int s = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9') {
        if(ch == '-')
            f = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9') {
        s = s * 10 + ch - '0';
        ch = getchar();
    }
    return s * f;
}
inline void write(int x)
{
    if(x < 0) {
        putchar('-');
        x = -x;
    }
    if(x > 9){
        write(x/10);
    }
    putchar(x%10+'0');
}
int main()
{
    n = read(); m = read();
    size = pow(n, 2.0/3.0);
    bnum = ceil((double)n/size);
    for(int i = 1; i <= bnum; i++)
        for(int j = (i-1)*size+1; j <= i*size; j++)
            belong[j] = i;
    for(int i = 1; i <= n; i++)
        a[i] = read();
    char ch[50];
    int ll, rr;
    for(int i = 1; i <= m; i++) {
        scanf("%s",ch);
        ll = read();
        rr = read();
        if(ch[0] == 'Q') {
            q[++tot].l = ll;
            q[tot].r = rr;
            q[tot].id = tot;
            q[tot].time = tot2;
        }
        else if(ch[0] == 'R') {
            c[++tot2].pos = ll;
            c[tot2].color = rr;
        }
    }
    sort(q+1, q+tot+1, cmp);
    int ql, qr, qt;
    int l = 1, r = 0, time = 0, now = 0;
    for(int i = 1; i <= tot; i++) {
        ql = q[i].l, qr = q[i].r;
        qt = q[i].time;
        while(l < ql) now -= !--cnt[a[l++]];
        while(l > ql) now += !cnt[a[--l]]++;
        while(r < qr) now += !cnt[a[++r]]++;
        while(r > qr) now -= !--cnt[a[r--]];
        while(time < qt) {
            ++time;
            if(ql <= c[time].pos && c[time].pos <= qr)  now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
            swap(a[c[time].pos] , c[time].color);
        }
        while(time > qt) {
            if(ql <= c[time].pos && c[time].pos <= qr)  now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
            swap(a[c[time].pos] , c[time].color);
            --time;
        }
        ans[q[i].id] = now;
    }
    for(int i = 1; i <= tot; i++) {
        //write(ans[i]);
        printf("%d\n", ans[i]);
    }
    return 0;
}

你可能感兴趣的:(技巧)