【算法】莫队

这篇博客起源于本人把一道 p o w ( 2 , n ) pow(2,n) pow(2,n) 的问题考虑成求组合数前缀和的问题qwq,于是接触到了这个新算法来总结一下

参考自这篇文章,写得太好了

首先是一道模板题

题目意思是,给出一个数组a,再给出多个区间,问这些区间里分别有多少不一样的数字

焗个栗子:
【算法】莫队_第1张图片
比如给出的是这样一个数组,询问的两个区间是红色和绿色标注的区间

如果我们分别遍历每一个区间,复杂度就太高了,因此就有大佬提出这样一种算法——

首先定义双指针 l l l r r r,初始化为 l = 0 , r = − 1 l=0,r=-1 l=0,r=1(表示此时区间内没有任何元素),然后用unorderd_map map存储当前区间内每个元素的个数

看我们要求的第一个区间 [ 0 , 5 ] [0,5] [0,5]

l l l 此时等于 0 0 0,和所求区间的左端点相同,因此无需移动

r r r 此时等于 − 1 -1 1,在所求区间右端点的左侧,因此需要向右移

首先向右移一位变成 0 0 0,此时区间内添加了一个元素 5,因为map[5] = 0,元素5不存在于原来的区间里,因此元素种类数cnt加一,map[5] ++

然后 r r r 再右移一位变成 1,此时区间内添加了一个元素 7,因为map[7] = 0,元素7不存在于原来的区间里,因此元素种类数cnt加一,map[7] ++

依此类推,直到 r r r 右移到了 5,此时区间内添加了一个元素 7,但是添加前的map[7] = 1,添加了当前的 7 并不会让区间内元素的个数增多,因此cnt无需改变,map[7] ++

之后从红色区间挪到绿色区间的操作也是类似的

上文介绍了如何往区间内添加数,当然,删除一个数的操作也是类似的,只不过添加数时,我们要先看被添加的数有没有出现在原来的区间里,也就是map[x]是否等于0,如果等于0说明不存在于原来的区间,区间内元素种类数cnt才能加一,而删除数时,我们要先把当前数删除,也就是map[x]减一,然后再判断当前区间里还有没有x,如果没有x也就是map[x] == 0,那么区间内元素种类数cnt减一

code

// 添加数
void add(int pos)
{
    if (!map[a[pos]]) cnt ++ ; // 在区间中新出现,总数要+1
    map[a[pos]] ++ ;
}

// 删除数
void del(int pos)
{
    map[a[pos]] -- ;
    if (!map[a[pos]]) cnt -- ; // 在区间中不再出现,总数要-1
}

int main()
{
    for (int i = 1; i <= q; ++i)
    {
        // 输入询问区间的左右端点
        int ql, qr;
        cin >> ql >> qr;

        while (l < ql) del(l++); // 如左指针在查询区间左方,左指针向右移直到与查询区间左端点重合
        while (l > ql) add(--l); // 如左指针在查询区间左端点右方,左指针左移
        while (r < qr) add(++r); // 右指针在查询区间右端点左方,右指针右移
        while (r > qr) del(r--);        // 否则左移
        cout << cnt;
    }
}

然后就 结束啦 qwq

才怪

还可以对这个算法继续优化

莫队算法优化的核心是分块排序

把长度为 n n n 的序列分成 n \sqrt{n} n 个块,然后按照左端点所在的块排序,如果左端点在同一个块内,则按右端点排序

优化1
这种排序方法也可以接着优化:如果左端点在同一奇数块内,则按右端点从小到大排序,如果左端点在同一偶数块内,则按右端点从大到小排序,这样排序是为了减少右端点移动的次数进而提高效率)

优化2
听说开O2优化能产生奇妙反应,实在没办法了可以考虑考虑这个

#pragma GCC optimize(2)

优化3
某佬把上面的一长串代码简化成了下面这样

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

于是模板题的代码如下

#include 

using namespace std;

typedef pair<pair<int, int>, int> PPI;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    int n;
    cin >> n;

    int size = sqrt(n);
    int bnum = ceil((double)n / size); // 把序列分成了多少块
    vector<int> belong(1e6 + 10);

    for (int i = 1; i <= bnum; i ++ )
        for (int j = (i - 1) * size + 1; j <= i * size; j ++ )
        {
            belong[j] = i; // 标记每个元素属于哪个块
        }

    vector<int> a(n + 1);
    for (int i = 1; i <= n; i ++ ) cin >> a[i];
    
    int m;
    cin >> m;
    vector<PPI> q(m);
    for (int i = 0; i < m; i ++ )
    {
        cin >> q[i].first.first >> q[i].first.second;
        q[i].second = i;
    }

    function<bool(PPI a, PPI b)> cmp = [&](PPI a, PPI b)
    {
        // return (belong[a.first.first] ^ belong[b.first.first]) ? belong[a.first.first] < belong[b.first.first] : ((belong[a.first.first] & 1) ? a.first.second < b.first.second : a.first.second > b.first.second);
        if (belong[a.first.first] != belong[b.first.first]) return belong[a.first.first] < belong[b.first.first];
        else
        {
            if (belong[a.first.first] % 2 == 1) return belong[a.first.second] < belong[b.first.second];
            else return belong[a.first.second] > belong[b.first.second];
        }
    };

    sort(q.begin(), q.end(), cmp);

    int l = 1, r = 0, cnt = 0;
    unordered_map<int, int> map;
    vector<int> ans(m);

    for (int i = 0; i < m; i ++ )
    {
        int ql = q[i].first.first, qr = q[i].first.second;

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

        ans[q[i].second] = cnt;
    }

    for (int i = 0; i < m; i ++ ) cout << ans[i] << '\n';
}

剩下的之后更新~~qwq

你可能感兴趣的:(基础算法,算法,开发语言)