莫队算法

莫队算法是离线处理一类区间不修改查询类问题的算法。

如果你在知道了[L,R]的答案时,可以在O(1)的时间下得到[L,R-1]和[L,R+1]和[L-1,R]和[L+1,R]的答案的话,就可以使用莫队算法。时间复杂度大概是O(n^1.5)。

莫队算法就是在知道一个区间的ans时,暴力的转移到所有的相邻区间。所以我们预先知道了所有的询问时,在整个询问过程中维护一个区间和这个区间转移到相邻位置所需的参数,就可以知道每次的ans了。

莫队算法通过合理的组织计算每个询问的顺序以此来降低复杂度。要知道我们算完[L,R]的答案后现在要算[L',R']的答案。由于可以在O(1)的时间下得到[L,R-1]和[L,R+1]和[L-1,R]和[L+1,R]的答案.所以计算

[L',R']的答案花的时间为|L-L'|+|R-R'|。如果把询问[L,R]看做平面上的点a(L,R).询问[L',R']看做点b(L',R')的话。那么时间开销就为两点的曼哈顿距离。所以对于每个询问看做一个点。我们要按一定顺序计算

每个值。那开销就为曼哈顿距离的和。要计算到每个点。那么路径至少是一棵树。所以问题就变成了求二维平面的最小曼哈顿距离生成树。

所以莫队算法的框架基本都是一样的,对于每个问题,我们要思考的应该是Del(x)和Add(x)函数,即如何去掉当前位置在当前区间的影响,或者如何增加当前位置在当前区间的影响。

例题:

小Z的袜子

可知当前区间[L, R]的所有颜色的袜子个数是a, b, ... ,z的时候,ans = ((a*(a-1)/2+b*(b-1)/2+......+z*(z-1)/2))/((R-L+1)*(R-L)/2).

化简即:(a^2+b^2+......+z^2-(R-L+1))/(R-L+1)*(R-L).

问题的关键变成如何求当前区间的所有颜色的袜子数的平方和。

Ans = 0; // 全局变量,维护当前的ans.

cnt[maxn]; // 记录每种袜子当前区间出现的数量.

a[maxn]; 记录i位置的袜子是什么颜色.

void Del(int x) {

  Ans -= cnt[a[x]]*cnt[a[x]];

  cnt[a[x]]--;

  Ans += cnt[a[x]]*cnt[a[x]];
}

void Add(int x) {

  Ans -= cnt[a[x]]*cnt[a[x]];

  cnt[a[x]]++;

  Ans += cnt[a[x]]*cnt[a[x]];

}

代码:HYSBZ似乎挂了,未测试是否AC.

#include <stdio.h>
#include <string.h>
#include <iostream>
#define maxn 50010
#include <math.h>
#include <algorithm>
#define LL long long
using namespace std;

int num[maxn];
LL temp[maxn];
int pos[maxn];

struct Query{
    int l, r;
    int id;
}query[maxn];

bool cmp(Query a, Query b) {
    if (pos[a.l] == pos[b.l])
        return a.r < b.r;
    else return pos[a.l] < pos[b.l];
}

int gcd(int x, int y) {
     if (y == 0) return x;
    return gcd(y, x%y);
}

struct anss{
    int z, m;
    void sample() {
        int d = gcd(z,m);
        z /= d, m /= d;
    }
}ans[maxn];

int L, R;
LL Ans;

void Add(int x) {
    Ans -= temp[num[x]]*temp[num[x]];
    temp[num[x]]++;
    Ans += temp[num[x]]*temp[num[x]];
}

void Del(int x) {
    Ans -= temp[num[x]]*temp[num[x]];
    temp[num[x]]--;
    Ans += temp[num[x]]*temp[num[x]];
}


int main() {
    freopen("in.cpp", "r", stdin);
    int n, m;
    while(~scanf("%d%d", &n, &m)) {
        for (int i=1; i<=n; ++i) {
            scanf("%d", &num[i]);
        }
        int ku = (int)sqrt(n);
        for (int i=1; i<=m; ++i) {
            scanf("%d%d", &query[i].l, &query[i].r);
            query[i].id = i;
            pos[query[i].id] = i/ku;
        }
        sort(query+1, query+m+1, cmp);

        L = 1, R = 0, Ans = 0;
        memset(temp, 0, sizeof(temp));

        for (int i=1; i<=m; ++i) {
            while(L < query[i].l) {
                Del(L);
                L++;
            }
            while(L > query[i].l) {
                L--;
                Add(L);
            }
            while(R < query[i].r) {
                R++;
                Add(R);
            }
            while(R > query[i].r) {
                Del(R);
                R--;
            }
            ans[query[i].id].z = Ans - (R - L + 1);
            ans[query[i].id].m = (R - L + 1)*(R - L);
            ans[query[i].id].sample();
        }

        for (int i=1; i<=m; ++i) {
            cout << ans[i].z << "/" << ans[i].m << endl;
        }
    }
    return 0;
}

感觉莫队算法就是利用合理安排询问的时间,使得区间转移的代价最少(曼哈顿距离)。问题必须是能够:

知道了[L,R]的答案时,可以在O(1)的时间下得到[L,R-1]和[L,R+1]和[L-1,R]和[L+1,R]的答案。

难点就是在,如何区间转移,即Add()和Del()函数。

之所以是O(n^1.5),是因为莫队把询问先按照块排序,然后按照询问区间的右端点排序。n的数组,分成sqrt(n)块。本来觉得先按l排序,再按r排序就可以了。发现这样可能,会浪费一些时间。

参考链接:

http://www.tuicool.com/articles/mYzQZzF

原文地址:https://www.cnblogs.com/icode-girl/p/5746059.html