初识莫队

莫队

个人理解:
这是一种较为暴力的算法,适用于解决维护序列区间操作的问题。

主要思想:

  • 把所有的操作离线,按某种方式重新排序。
  • 操作过程中不断转移当前区间的答案。
  • (\([L, R]\Rightarrow [L \pm 1, R \pm 1]\))
  • 希望转移的复杂度尽量的小(\(O(n\sqrt{m})\)

01-普通的莫队(无修改)

01-例题:P3901 数列找不同

01-题意

现有数列 \(A_1,A_2,\ldots,A_N\)\(Q\) 个询问 \((L_i,R_i)\),询问 \(A_{L_i} ,A_{L_i+1},\ldots,A_{R_i}\) 是否互不相同。

对每个询问输出一行,YesNo

\(1 \le N,Q \le 10^5\)\(1 \le A_i \le N\)\(1 \le L_i \le R_i \le N\)

02-分析

考虑维护两个指针lr,对于每次询问 \([L, R]\) ,把 lr移动到 \([L,R]\),在移动的过程中不断统计不同的数的个数tot,如果 tot != r - l + 1,那么答案就为No,反之Yes

cin >> n >> q;
for (int i = 1; i <= n; i++)
    cin >> a[i];
    
auto add = [&](int x){ if (cnt[x] == 0) tot++; cnt[x]++; };
auto del = [&](int x) {if (cnt[x] == 1) tot--; cnt[x]--; };
    
for (int l = 0, r = 0, L, R; q--; ){
    cin >> L >> R;
    while(l < L) del(a[l++]); // l L R
    while(r < R) add(a[++r]); // L r R
    while(l > L) add(a[--l]); // L l R
    while(r > R) del(a[r--]); // L R r
    cout << (tot == r - l + 1 ? "Yes" : "No") << endl;
}

显而易见的是,这并不能通过本题

瓶颈在于每次询问都移动了lr,这使得时间复杂度来到了 \(O(qn)\),无法接受。

有没有一种方法,让这两个指针动的快一点?(每次移动的距离尽可能小)

发现有以下性质:

  • 询问的处理顺序与给出的顺序无关。
  • 没了。

这启发我们可以通过改变询问的顺序,使得lr更快的到达新的 \([L,R]\)

考虑先离线所有的询问,进行一个根号分块。

对于储存询问的两个结构体a, b:

首先判断a.l / B == b.l / B,即是否在同一块。

若在同一块,则按r大小排序,a.r < b.r

反之,则按l排序,a.l > b.l

struct node {
        int l, r, id;
} q[N];

for (int i = 1; i <= m; i++)
        cin >> q[i].l >> q[i].r, q[i].id = i;

int B = n / sqrt(m);

sort(q + 1, q + 1 + m, [](node a, node b) {
        return a.l / B == b.l / B ? a,r < b.r : a.l < b.l;
}); // 再也不需要cmp了

按照排序后的顺序操作,这样就使得复杂度降到了 \(O(n \sqrt{m})\)足以通过本题。

02-例题:P1972 HH的项链 (LOJ)

  • (莫队只能过LOJ的数据,洛谷加强了)

01-题意

HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

一行一个正整数 \(n\),表示项链长度。
第二行 \(n\) 个正整数 \(a_i\),表示项链中第 \(i\) 个贝壳的种类。

第三行一个整数 \(m\),表示 HH 询问的个数。
接下来 \(m\) 行,每行两个整数 \(l,r\),表示询问的区间。

对于 \(100\%\) 的数据,\(1\le n,m,a_i \leq 10^6\)\(1\le l \le r \le n\)

02-分析

与上一题一样,做法差不多,只是问的东西不一样了。
注意莫队只能过LOJ,洛谷的数据加强了,要用树桩数组

03-例题:P2709 小B的询问

01-题意

小B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求:

\[\sum\limits_{i=1}^k c_i^2 \]

其中 \(c_i\) 表示数字 \(i\)\([l,r]\) 中的出现次数。

对于 \(100\%\) 的数据,\(1\le n,m,k \le 5\times 10^4\)

02-分析

与上一题一致,依题意模拟即可
需要推柿子。

04-例题 :P1494 小 Z 的袜子

01-题意

作为一个生活散漫的人,小 Z 每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小 Z 再也无法忍受这恼人的找袜子过程,于是他决定听天由命……

具体来说,小 Z 把这 \(N\) 只袜子从 \(1\)\(N\) 编号,然后从编号 \(L\)\(R\) 的袜子中随机选出两只来穿。尽管小 Z 并不在意两只袜子是不是完整的一双,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。

你的任务便是告诉小 Z,他有多大的概率抽到两只颜色相同的袜子。当然,小 Z 希望这个概率尽量高,所以他可能会询问多个 \((L,R)\) 以方便自己选择。

然而数据中有 \(L=R\) 的情况,请特判这种情况,输出0/1

输入文件第一行包含两个正整数 \(N\)\(M\)\(N\) 为袜子的数量,\(M\) 为小 Z 所提的询问的数量。接下来一行包含 \(N\) 个正整数 \(C_i\),其中 \(C_i\) 表示第 \(i\) 只袜子的颜色,相同的颜色用相同的数字表示。再接下来 \(M\) 行,每行两个正整数 \(L, R\) 表示一个询问。

包含 \(M\) 行,对于每个询问在一行中输出分数 \(A/B\) 表示从该询问的区间 \([L,R]\) 中随机抽出两只袜子颜色相同的概率。若该概率为 \(0\) 则输出 0/1,否则输出的 \(A/B\) 必须为最简分数。

\(100\%\) 的数据中,\(N,M \leq 50000\)\(1 \leq L < R \leq N\)\(C_i \leq N\)

02-分析

同样维护袜子的种类数,注意同时维护用来算概率的组合数。

05-例题 P7764 Poklon

统计区间内出现两次的数,依题意模拟即可。

02-带修莫队

注意到刚刚的例题中都没有修改的情况,莫队同样能处理修改,但更加复杂,不过本质相同。

P1903 数颜色 / 维护队列

01-题意

墨墨购买了一套 \(N\) 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:

  1. \(Q\ L\ R\) 代表询问你从第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。

  2. \(R\ P\ C\) 把第 \(P\) 支画笔替换为颜色 \(C\)

为了满足墨墨的要求,你知道你需要干什么了吗?


\(1\) 行两个整数 \(N\)\(M\),分别代表初始画笔的数量以及墨墨会做的事情的个数。

\(2\)\(N\) 个整数,分别代表初始画笔排中第 \(i\) 支画笔的颜色。

\(3\) 行到第 \(2+M\) 行,每行分别代表墨墨会做的一件事情,格式见题干部分。

对于每一个 Query 的询问,你需要在对应的行中给出一个数字,代表第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。


对于所有数据,\(n,m \leq 133333\)

所有的输入数据中出现的所有整数均大于等于 \(1\) 且不超过 \(10^6\)

先写到这里吧
2023年12月5日17:22:22

02-分析

在原来的基础上增加了时间这一个维度,如果按照原来的排序方式,显而易见会得出错误的答案。

如果不想抛弃莫队的思路,如何保证答案的正确性?

既然多了时间这一维度,不妨在转移lr的同时转移一个时间变量t

  • 如果当前的t小于询问的t,那么upt(++t)
  • 如果当前的t大于询问的t,那么upt(t--)

像这样操作,就可以保证时间也对的上了。

注意代码细节:

  • 如果在转移t的过程中对[l,r]中的元素进行了修改,那么需要更新当前的答案。
  • l, r, t的转移顺序建议是 l -> l'r -> r't -> t'
  • 分开记录操作序列与询问序列,调整块的大小B = n / pow(m, 0.666)
  • 询问的排序方式需要改变:
a.l / B == b.l / B ? 
(a.r / B == b.r / B ? a.lst < b.lst : a.r < b.r) 
: a.l < b.l
#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 5;

int n, m, l, r, t, B;
int qcnt, ccnt, ans;
int a[N], res[N], cnt[N];

char op;

struct node {
    int l, r, lst, id;
} q[N], c[N];

void add(int x, int f = 0) {
    x = f ? x : a[x];
    ans += !cnt[x]++;
}

void del(int x, int f = 0) {
    x = f ? x : a[x];
    ans -= !--cnt[x];
}

void upt(int k, int x) {
    int L = q[k].l, R = q[k].r, idx = c[x].l, &to = c[x].r;

    if (L <= idx && idx <= R) {
        del(idx);
        add(to, 1);
    }

    swap(a[idx], to);
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0);

    cin >> n >> m;
    B = pow(n, 0.666);

    for (int i = 1; i <= n; i++)
        cin >> a[i];

    for (int i = 1, x, y; i <= m; i++) {
        cin >> op >> x >> y;

        if (op == 'Q') q[++qcnt] = {x, y, ccnt, qcnt};
        else c[++ccnt] = {x, y, 0, 0};
    }

    sort(q + 1, q + 1 + qcnt, [](node a, node b) {
        return a.l / B == b.l / B ? (a.r / B == b.r / B ? a.lst < b.lst : a.r < b.r) : a.l < b.l;
    });
    l = 1;

    for (int i = 1; i <= qcnt; i++) {
        while (l < q[i].l) del(l++);

        while (l > q[i].l) add(--l);

        while (r < q[i].r) add(++r);

        while (r > q[i].r) del(r--);

        while (t < q[i].lst) upt(i, ++t);

        while (t > q[i].lst) upt(i, t--);

        res[q[i].id] = ans;
    }

    for (int i = 1; i <= qcnt; i++)
        cout << res[i] << endl;

    return 0;
}

莫队进阶用法

03-回滚莫队

04-P5906 最远距离

题目描述

给定一个序列,多次询问一段区间 \([l,r]\),求区间中相同的数的最远间隔距离

序列中两个元素的间隔距离指的是两个元素下标差的绝对值

数据范围

之后 \(m\) 行,每行两个整数 \(l,r\) 表示询问区间。

对于 \(100\%\) 的数据,满足 \(1\leq n,m\leq 2\cdot 10^5\)\(1\leq a_i\leq 2\cdot 10^9\)

分析

对于这样的题目,单单只是普通的莫队难以下手。

添加操作比较简单,而删除操作十分困难。

既然困难,那就不删除了呗

这时候,可以用回滚莫队解决,在做法上与普通莫队稍有不同:

  • 对询问按照左端点分块排序,左端点在相同块的询问按照右端点递增排序。
  • 依次处理每一个块内的所有询问。
  • 对于同一个块的每一个询问,让指针 l 从块的最右边开始移动到对应的位置。
  • 而对于右指针r,则继承同一块内上一个询问的的位置与答案。
  • 每处理完一个询问,将l指针挪回最左边,路上消除每个位置的影响。

时间复杂度 \(O(n\sqrt n\ +\) 巨大的常数 \()\)

记得离散化和不要用太多的memset

#include <bits/stdc++.h>
using namespace std;

const int N = 2e5 + 5;

int n, m, l, r, ans;
int a[N], b[N], res[N];

int cntL[N], cntR[N], cntL_[N], cntR_[N];

int B, tot;
int kuai[N], L[N], R[N];

int used_[N], top_;

struct node{
    int l, r, id;
}q[N];

inline void cmin(int &a, int b) { a = min(a, b); }
inline void cmax(int &a, int b) { a = max(a, b); }

void add(int x, int f = 0){
    int p = a[x];
    if (f) cmin(cntL[p], x);
    cmax(cntR[p], x), cmax(ans, cntR[p] - min(cntL[p], x));
}



signed main(){
    
    ios::sync_with_stdio(0); cin.tie(0);
    
    cin >> n; B = sqrt(n); tot = n / B;
    
    for (int i = 1; i <= n; i++)
        cin >> a[i], b[i] = a[i];
    
    cin >> m;
    
    for (int i = 1; i <= m; i++)
        cin >> q[i].l >> q[i].r, q[i].id = i;
    
    sort(b + 1, b + 1 + n);
    
    int len = unique(b + 1, b + 1 + n) - b - 1;
    
    for (int i = 1; i <= n; i++)
        a[i] = lower_bound(b + 1, b + 1 + len, a[i]) - b,
        kuai[i] = (i - 1) / B + 1;
    
    for (int i = 1; i <= tot; i++)
        L[i] = R[i - 1] + 1, R[i] = L[i] + B - 1;
    
    if (R[tot] < n) ++tot, L[tot] = R[tot - 1] + 1, R[tot] = n;
    
    sort(q + 1, q + 1 + m, [](node a, node b){
        return kuai[a.l] == kuai[b.l] ? a.r < b.r : a.l < b.l;
    });
    
    memset(cntL, 0x3f, sizeof cntL), memset(cntR, 0, sizeof cntR);
    memset(cntL_, 0x3f ,sizeof cntL_), memset(cntR_, 0, sizeof cntR_);
    
    for (int i = 1, num = 1; i <= tot; i++){
        
        memset(cntL, 0x3f, sizeof cntL), memset(cntR, 0, sizeof cntR);
//        while(top) cntL[used[top]] = 0x3f3f3f3f, cntR[used[top]] = 0, top--;
        for (l = R[i] + 1, r = R[i], ans = 0; kuai[q[num].l] == i; num++){
            if (kuai[q[num].l] == kuai[q[num].r]){
                while(top_) cntL_[used_[top_]] = 0x3f3f3f3f, cntR_[used_[top_]] = 0, top_--;
                for (int k = q[num].l; k <= q[num].r; k++) used_[++top_] = a[k], cmin(cntL_[a[k]], k), cmax(cntR_[a[k]], k);
                for (int k = q[num].l; k <= q[num].r; k++) cmax(res[q[num].id], cntR_[a[k]] - cntL_[a[k]]);
                continue;
            }
            
            while(r < q[num].r) add(++r, 1);
            int now = ans;
            while(l > q[num].l) add(--l);
            
            res[q[num].id] = ans, ans = now;
            
            while(l < R[i] + 1) {
                if (cntR[a[l]] == l) cntR[a[l]] = 0;
                l++;
            }
        }
    }
    
    for (int i = 1; i <= m; i++)
        cout << res[i] << endl;
    
    
    return 0;
}

Written with StackEdit中文版.

posted @ 2024-03-29 13:27  afhuds  阅读(7)  评论(0编辑  收藏  举报