← 目录 第 57 / 60 节 全部 60 节

57 · 根号分治与莫队入门

把 √n 当成一条分界线:参数小的预处理、参数大的现场算(P3396),或者按左端点分块排序、让区间指针只挪一小段(P2709 莫队)。两种技巧的复杂度都从 mB + n²/B 这类代价函数解出来,而不是背结论。
提高+预计 70 分钟根号分治莫队分块
本节出现的新函数 / 新语法(先看懂再用)

这一节的两件事看上去无关:一件是“把能整除的小模数全部预处理”,一件是“把询问排序后暴力挪指针”。它们的共同动作是往复杂度里塞一个 √n——用 √n 次廉价操作,换掉一次昂贵操作。

根号分治的本质是给暴力划一条线:线以下的预处理,线以上的现场算,两边的代价都压到 √n 量级。
做法P3396 哈希冲突(n = m = 1.5×10⁵)P2709 莫队(n = m = 10⁵)
纯暴力每次询问 O(n) → 2.3×10¹⁰,TLE每个询问 O(n) → 1×10¹⁰,TLE
全部预处理O(n²) 张表,空间放不下O(n²) 前缀表,空间放不下
根号分治 / 莫队O((n+m)√n) ≈ 1.2×10⁸O(n√m) ≈ 6.3×10⁷

形态一:按参数大小分治。以 P3396 为例。询问参数是模数 x:x 小的组合少(x ≤ T 时一共只有 Σx ≈ T²/2 张表),可以全部预存;x 大的时候池子里的数少(最多 n/x 个),现场跳着扫也不贵。取 T = √n,两种分支的代价都是 √n:查询 O(1) 或 O(√n),修改 O(√n)。

形态二:按规模分治。信这句话:一个长度为 n 的序列里,出现次数 > T 的值至多 n/T 个。于是把出现多的叫“重值”,只给重值做预处理(前缀出现次数表,代价 O(n·n/T)),出现少的“轻值”整串不超过 T 次,询问时直接枚举它的出现位置。树上同理:度数 > √m 的点至多 √m 个,这是虚树、分块图的第一步。

形态二和莫队是同一件事的两种外壳:都在用“总量有限”来给最坏情况封顶。形态一说“小的种类少”,形态二说“大的个数少”,两者相乘一定不超过 n。

先把三个语法零件拆开:阈值与两套分支、四个指针的移动、带奇偶优化的排序。

新函数拆解 · 阈值 T 与两套分支
const int T = 400; // ≈ √n if (x <= T) { /* 查预处理表 */ } else { /* 现场暴力 */ }
组成含义 / 格式要求
T 怎么定T = (int)sqrt(n) + 1,或直接写常数。n ≤ 1.5×10⁵ 时 √n ≈ 387,写 400 更好记也更快(省掉一次开方)。
表要开多大s[T+1][T+1]:第一维是模数,第二维是余数(最大 T−1)。400×400×4 字节 ≈ 640 KB,随便开。
小参数分支必须 O(1) 查表,所以预处理要覆盖所有可能出现的小参数,不能等询问来了才算。
大参数分支项数是 O(n/T),循环写成 for (i = 起点; i <= n; i += x),绝不能写成枚举 1..n 再判断 i % x == y——那是没优化。
代价交换预处理 O(nT),每次修改 O(T),每次大查询 O(n/T)。三条加起来才是要控制的量。
修改怎么维护改 a[x] 时,对所有 t ≤ T 把 s[t][x % t] 加上增量;大参数分支不用管,它每次现算。
最小可运行示例
示例
#include <bits/stdc++.h>
using namespace std;

const int N = 150005, T = 400;
int n = 10, a[N], s[T + 5][T + 5];

int main() {
    for (int i = 1; i <= n; ++i) a[i] = i;          // 假装输入是 1 2 3 ... 10

    for (int x = 1; x <= T; ++x)                    // 建表:只给小模数建
        for (int i = 1; i <= n; ++i)
            s[x][i % x] += a[i];

    int x = 2, y = 1, ans = 0;                      // 询问:模 2 余 1 的池子和
    if (x <= T) ans = s[x][y];                      // 小模数:O(1)
    else for (int i = y ? y : x; i <= n; i += x) ans += a[i];   // 大模数:O(n/T)

    cout << ans << "\n";                            // 1+3+5+7+9 = 25
    return 0;
}
注意

y = 0 时起点是 x 而不是 0:下标从 1 开始,0 号位不存在。

两重循环是 nT ≈ 6×10⁷ 次加法,比每次询问现算 O(n) 划算得多——这就是“预处理换查询”。

T 取得太大,预处理和修改会变慢;T 取得太小,大查询变慢。最优值解方程得到,见数学补给。

新函数拆解 · 莫队的四个指针移动
while (L > q[i].l) add(--L); while (R < q[i].r) add(++R); while (L < q[i].l) del(L++); while (R > q[i].r) del(R--);
组成含义 / 格式要求
L, R 初值L = 1, R = 0,表示当前维护的区间为空。写成 L = 0 会去 add 一个不存在的 a[0]。
维护的是闭区间四个 while 走完后必然 L == l 且 R == r,此时 cur 就是这一问的答案。
四条语句的顺序只要 add / del 一一对应,顺序无所谓;习惯先扩后缩,能一眼看出区间在怎么变。
add / del 必须互为逆操作add 一个元素与 del 同一个元素的效果要正好抵消,写不对称就会永久偏移。
为什么这样能快按块排序后,同一块内的询问 L 只在 B 范围内晃、R 单调推进,所以指针总步数是 mB + n²/B,而不是 m·n。
前提:add / del 是 O(1)维护的量必须能在加入/删除一个元素时 O(1) 更新(cnt、cnt²、重复值个数)。像“区间最大值”这种删除后无法还原的量,普通莫队做不了。
最小可运行示例
示例
#include <bits/stdc++.h>
using namespace std;

int a[9] = {0, 1, 2, 2};                        // 只用 1..3 号位置,值域 1..2
int cnt[9];
long long cur = 0;                              // 维护 Σ cnt²

void add(int p) { cur += 2LL * cnt[a[p]] + 1; ++cnt[a[p]]; }
void del(int p) { cur -= 2LL * cnt[a[p]] - 1; --cnt[a[p]]; }

int main() {
    int L = 1, R = 0;                           // 空区间
    int l = 1, r = 3;                           // 目标区间 [1, 3] = {1, 2, 2}
    while (L > l) add(--L);
    while (R < r) add(++R);
    while (L < l) del(L++);
    while (R > r) del(R--);
    cout << cur << "\n";                        // 1² + 2² = 5
    return 0;
}
注意

四个 while 总共走 O(n√m) 步,每步只做 O(1) 的 add/del,所以总复杂度就是这个步数。

对每题都从空区间出发,看起来笨,实际上是“让相邻询问的区间尽量接近”,这个笨办法就是莫队的全部思想。

新函数拆解 · 奇偶排序(莫队的常数优化)
bool cmp(const Query &x, const Query &y) { int bx = x.l / B, by = y.l / B; if (bx != by) return bx < by; return (bx & 1) ? x.r > y.r : x.r < y.r; }
组成含义 / 格式要求
x.l / B左端点落在第几块。B 是块长,取 n/√m(n = m 时就是 √n)。
块号不同按块号升序,保证 L 只在块内移动,这是 mB 那一项的来源。
同一块内 r 升序块内 r 单调推进,整块只走 O(n) 步。
bx & 1 的奇偶翻转奇数块让 r 降序。这样一块结束时 r 停在高端,下一块从高端接着往下走,省掉每块之间整整一次 n 的回程,实测常数接近减半。
为什么只翻转 r翻转 l 会破坏“L 只在块内移动”的性质;r 不受块限制,可以随便调头。
必须严格弱序两个询问同块同 r 时返回 false,sort 不会崩。写成 <= 会让 sort 越界,症状是 RE。
最小可运行示例
示例
#include <bits/stdc++.h>
using namespace std;

struct Query { int l, r, id; } q[6];
int B = 2;                                   // 块长:n/√m 的简化演示

bool cmp(const Query &x, const Query &y) {
    int bx = x.l / B, by = y.l / B;
    if (bx != by) return bx < by;             // 先按块
    return (bx & 1) ? x.r > y.r : x.r < y.r;  // 块内 r 的方向按块号奇偶翻转
}

int main() {
    int L[6] = {0, 1, 1, 3, 4, 5};
    int R[6] = {0, 5, 2, 6, 4, 8};
    for (int i = 1; i <= 5; ++i) { q[i].l = L[i]; q[i].r = R[i]; q[i].id = i; }
    sort(q + 1, q + 6, cmp);
    for (int i = 1; i <= 5; ++i) cout << q[i].l << " " << q[i].r << "\n";
    // 1 2 / 1 5 / 3 6 / 4 4 / 5 8:第 1、2 块 r 递增,第 3 块(奇数块)r 递减
    return 0;
}
注意

这是排序里唯一需要动脑的地方;写错方向不影响正确性,只影响常数,所以症状是 TLE 而不是 WA。

块长 B 与排序规则必须配套:改了 B 就要重新分析 mB + n²/B,别照抄 n = m 时的 316。

例题 · 哈希冲突洛谷 P3396提高-去洛谷做这题 ↗

B 君有一个正整数序列 value[1..n]。value[k] 会被存进 (k mod p) 这个池子,下文把模数统一记作 x。给出 m 次操作:

A x y:询问在模 x 时,y 池内所有数的总和,即所有满足 k mod x = y 的 value[k] 之和。
C x y:把 value[x] 改成 y,立即生效。

输入格式:第一行两个整数 n、m;第二行 n 个正整数;接下来 m 行,每行一个字符 cmd 和两个整数 x、y。
输出格式:每个 A 操作输出一行一个正整数。

数据范围:n, m ≤ 1.5×10⁵,1 ≤ valueᵢ ≤ 1000,修改后的值同样不超过 1000。

输入: 10 5 1 2 3 4 5 6 7 8 9 10 A 2 1 C 1 20 A 3 1 C 5 1 A 5 0 输出: 25 41 11
模数小就存,模数大就跳

暴力:每次询问扫一遍 1..n,判断 i mod x 是否为 y,O(n)。n = m = 1.5×10⁵ 时是 2.25×10¹⁰ 次取模,必然 TLE。

全部预处理:给每个 (x, y) 存一张表。x 有 n 种、y 有 x 种,合计 O(n²) —— 空间先炸。所以“存或不存”不能一刀切。

分治点:x 小的时候 (x, y) 组合少。x ≤ T 时只有 Σx ≈ T²/2 个格子,T = 387 时约 7.5×10⁴ 个,随便存;x 大的时候池子里的数少,模 x 余 y 的下标最多 n/x 个,跳着扫反而是 O(√n)。

于是:预处理所有 x ≤ T 的表 s[x][y];查询时 x ≤ T 直接查表,x > T 现场按 i += x 跳着扫。取 T = √n,两条分支的代价都是 √n 量级。

修改怎么办。改 a[x] 会影响所有小模数的表:对每个 t ≤ T 把 s[t][x % t] 加上增量 y − a[x]。大模数不需要维护,因为每次查询都是现算的。

总账。预处理 O(nT) ≈ 5.8×10⁷,m 次操作各 O(T) 或 O(n/T) ≈ 387,合计约 1.2×10⁸ 次简单加法。

数值。最大的池子和 = 1.5×10⁵ × 1000 = 1.5×10⁸,int 绰绰有余。写成 int 而不是 long long,表的内存直接减半,也在缓存里待得更久。

完整可编译代码(C++17):根号分治
#include <bits/stdc++.h>
using namespace std;

const int N = 150005, T = 400;         // T ≈ √150000 ≈ 387,取整到 400
int n, m, a[N];
int s[T + 5][T + 5];                   // s[x][y]:所有 i ≡ y (mod x) 的 a[i] 之和,只存 x ≤ T

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

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

    for (int x = 1; x <= T; ++x)                    // 建表:nT ≈ 6×10⁷ 次加法
        for (int i = 1; i <= n; ++i)
            s[x][i % x] += a[i];

    while (m--) {
        char op; int x, y;
        cin >> op >> x >> y;
        if (op == 'A') {
            if (x <= T) {
                cout << s[x][y] << "\n";            // 小模数:查表 O(1)
            } else {
                int r = 0;                          // 大模数:池内最多 n/x 个数,跳着扫
                for (int i = y ? y : x; i <= n; i += x) r += a[i];
                cout << r << "\n";
            }
        } else {
            for (int t = 1; t <= T; ++t)            // 改一个数,所有小模数的表都要动
                s[t][x % t] += y - a[x];
            a[x] = y;                               // 最后再改原数组,否则增量算错
        }
    }
    return 0;
}
逐行拆解
代码片段含义与理由
int s[T + 5][T + 5]第一维是模数 x(只到 T),第二维是余数 y(最大 T−1)。400×400 个 int 约 640 KB。
for (x = 1; x <= T; ++x)只给“小模数”建表。大模数的表不建——建了也存不下 n² 个格子。
s[x][i % x] += a[i]把每个元素放进它对应的 (模数, 余数) 格子。nT 次加法换后面每次查询 O(1)。
if (x <= T)分治的那条线。线以下查表,线以上暴力,两条分支的代价都 ≤ √n。
for (i = y ? y : x; i <= n; i += x)大模数分支的关键:直接按步长 x 跳,次数是 n/x 而不是 n。y = 0 时从 x 开始,因为下标是 1-based。
s[t][x % t] += y - a[x]用增量更新 T 张表,比“重新建表”便宜 T 倍。注意是 y − a[x],不是 y。
a[x] = y 放在最后若先改 a[x],y − a[x] 就变成 0,所有小表全部不更新 → WA。
T 写成常数 400省掉运行时开方,也让数组大小能写成编译期常量;n 变化时重新算一次即可。

现在换一个角度。前面是“按参数大小分工”,莫队是“按询问顺序分工”:把所有询问排个序,让相邻两个询问的区间尽量接近,然后暴力挪指针。单次询问是 O(n),但相邻两次之间只挪一小段,总步数被摊平到 O(n√m)。

莫队有三个硬性前提,缺一不可:① 能离线(询问的答案与顺序无关);② 维护的量在加入/删除一个元素时能 O(1) 更新;③ 区间能“增删元素”地维护,而不是只能整段重算。

例题 · 【模板】莫队 / 小 B 的询问洛谷 P2709普及/提高-去洛谷做这题 ↗

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

Σi=1..k cᵢ²,其中 cᵢ 表示数字 i 在 a[l..r] 中出现的次数。

输入格式:第一行三个整数 n、m、k;第二行 n 个整数;接下来 m 行,每行两个整数 l、r。
输出格式:输出 m 行,每行一个整数,对应一个询问的答案。

数据范围:1 ≤ n, m, k ≤ 10⁵。

增量公式 + 排序,就是莫队的全部

暴力:每个询问重扫一遍区间,O(nm) = 10¹⁰,TLE。

关键是增量公式。设当前某个值出现了 c 次,贡献是 c²。再放进一个它,贡献变成 (c+1)²,增量是 (c+1)² − c² = 2c + 1;删掉一个,减少 c² − (c−1)² = 2c − 1。两个都是 O(1),这就是莫队能成立的地基。

然后只管挪指针。维护当前区间 [L, R] 和答案 cur。要让 [L, R] 变成 [l, r],左右端点各自往里收或往外扩,每挪一格调用一次 add 或 del。全部挪完,cur 就是答案。

排序决定快慢。按 l 所在块排序,块内按 r 排序。于是同一块内 r 单调推进(整块只走 O(n) 步),而 l 只在块内晃(每个询问至多 O(B) 步)。总步数 mB + n²/B,取 B = n/√m 得 2n√m。

取 2c+1 时先算再改 cnt。cur += 2*cnt[v] + 1 用的是旧次数,写完才 ++cnt[v]。顺序颠倒就变成用新次数算增量,答案系统性偏大。

奇偶优化。块内 r 单调时,每换一块 r 都要从一段跑回另一端,白走一整趟 n。让奇数块 r 倒序,回程就变成顺路,常数接近减半。这只是常数优化,不影响正确性。

答案规模。最坏情况所有数相同,Σcᵢ² = n² = 10¹⁰,必须 long long。这是本题唯一的数值陷阱。

完整可编译代码(C++17):莫队算法
#include <bits/stdc++.h>
using namespace std;

const int N = 100005;
int n, m, k, B;
int a[N], cnt[N];                  // cnt[v]:值 v 在当前区间里出现几次
long long cur, ans[N];             // cur = Σ cnt[v]²,最大 1e10,必须 long long

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

bool cmp(const Query &x, const Query &y) {
    int bx = x.l / B, by = y.l / B;
    if (bx != by) return bx < by;                     // 先按左端点所在块
    return (bx & 1) ? x.r > y.r : x.r < y.r;          // 块内 r 单调,奇偶块方向相反
}

void add(int p) { cur += 2LL * cnt[a[p]] + 1; ++cnt[a[p]]; }   // (c+1)² − c² = 2c + 1
void del(int p) { cur -= 2LL * cnt[a[p]] - 1; --cnt[a[p]]; }   // c² − (c−1)² = 2c − 1

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> k;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    B = max(1, (int)(n / sqrt(m)));            // 理论最优块长 n/√m
    for (int i = 1; i <= m; ++i) { cin >> q[i].l >> q[i].r; q[i].id = i; }
    sort(q + 1, q + m + 1, cmp);

    int L = 1, R = 0;                          // 当前维护的区间为空
    for (int i = 1; i <= m; ++i) {
        while (L > q[i].l) add(--L);           // 左端点外扩
        while (R < q[i].r) add(++R);           // 右端点外扩
        while (L < q[i].l) del(L++);           // 左端点内收
        while (R > q[i].r) del(R--);           // 右端点内收
        ans[q[i].id] = cur;                    // 此刻 [L, R] == [l, r]
    }

    for (int i = 1; i <= m; ++i) cout << ans[i] << "\n";
    return 0;
}
逐行拆解
代码片段含义与理由
B = max(1, (int)(n / sqrt(m)))块长按 n/√m 取。n = m = 10⁵ 时约 316。用 max 防 m 很大时算出 0。
bx != by → 按块号升序同一块的询问连在一起处理,L 才不会跨块乱跳。
(bx & 1) ? x.r > y.r : x.r < y.r奇数块让 r 倒序,省掉块与块之间的一整趟回程。写反只是变慢,不会错。
cur += 2LL * cnt[a[p]] + 1用旧次数算增量。写 2LL 是为了让整个表达式的类型变成 long long——cur 最大 10¹⁰,int 只有 2.1×10⁹。
++cnt[a[p]] 在增量之后顺序不能换。先改 cnt 再算增量,等于用 (c+1) 代 c,每步都多算 2 → WA。
while (L > q[i].l) add(--L)先减指针再加元素。写成 add(L--) 会把旧位置算进去,还漏掉新位置。
while (L < q[i].l) del(L++)先删再减。del 完再 L++ 才对应“区间左端右移一格”。
ans[q[i].id] = cur排序打乱了询问顺序,答案必须按原编号回填,最后再按 1..m 输出。
cin >> n >> m >> kk 只是值域上界,用来给 cnt 定大小:cnt 的下标是数值本身,值域是 [1, k],所以按 k 开而不是按 n 开。
这一节的坑,按症状记

莫队按 l、r 双关键字排序(不按块):小样例全对,大数据必 TLE——L、R 各自乱跳,总步数退化回 O(nm)。

块长取 1 或取 n:B = 1 时 R 每块都要重走一趟 n,总步数 O(n²);B = n 时等于完全没排序。两种都是 TLE,而且 n 较小时看起来还挺快。

add / del 的增量写成同一个式子(都用 2c+1):只有“只扩不缩”的数据能碰对;正常数据 WA,且答案忽大忽小。

先 ++cnt 再算增量:每步系统性多算 2,答案单调偏大 → WA。

cur 用 int:最坏 Σcᵢ² = 10¹⁰,溢出成负数 → WA,小数据完全正常。

根号分治里大参数写成 for (i = 1; i <= n; ++i) if (i % x == y):等于没优化,n = m = 1.5×10⁵ 时 TLE。必须用 i += x 跳。

P3396 里先改 a[x] 再更新小表:增量 y − a[x] 变成 0,小模数分支全错 → WA,而大模数分支(现算)却是对的,查错时会被这个“一半对”误导。

cnt 按 n 开而值域是 k(本题 k 与 n 无关,k 可以大于 n):越界写 → RE 或随机 WA。数组按 值域开。

自己动手复现

关掉页面,把 P2709 的莫队从零敲一遍,提交确认能过(这道题是洛谷的莫队模板题)。

改一改①:把 cmp 里的奇偶翻转删掉(两个分支都写 x.r < y.r),在 n = m = 10⁵ 的随机数据上比较运行时间——答案不变,只是变慢。

改一改②:把 B 依次改成 1、316、100000,用同一份数据测时,对照数学补给里那张表:两头最慢,中间最快。

改一改③:把 add 里的 2LL * cnt[a[p]] + 1 改成 1,观察小数据下答案如何偏离 n 的平方和。

数学补给 · 两个复杂度是怎么解出来的

一、莫队:mB + n²/B 的最小值。块长 B 是唯一能调的量。同一块内 r 单调,整块至多走 n 步,共 n/B 块 → R 的总位移 ≤ n²/B;每个询问的 l 在同一块内差不超过 B,共 m 个 → L 的总位移 ≤ mB。

由均值不等式:mB + n²/B ≥ 2√(mB · n²/B) = 2n√m,取等号当且仅当 mB = n²/B,即 B = n/√m。n = m 时就是 B = √n。

注意 B 依赖 m,不依赖 n 单独:m 比 n 大很多时块长要变小(m ≫ n 时 B 可以到 1 附近),m ≪ n 时块长变大。所以别把 316 当常数抄。

块长 BL 总位移 ≤ mBR 总位移 ≤ n²/B合计(n = m = 10⁵)
11×10⁵1×10¹⁰约 1×10¹⁰,TLE
1001×10⁷1×10⁸1.1×10⁸
3163.2×10⁷3.2×10⁷6.3×10⁷,最优
10001×10⁸1×10⁷1.1×10⁸
1000001×10¹⁰1×10⁵约 1×10¹⁰,TLE

二、根号分治:T 取 √n。预处理 O(nT),每次操作 O(T)(修改)或 O(n/T)(大参数查询)。最坏情况每一种操作都可能拉满,所以看 max(T, n/T):两者相等时取最小,T² = n,T = √n,代价 O((n+m)√n)。

代入 n = m = 1.5×10⁵、T = 387:预处理 5.8×10⁷,m 次操作各 387 → 5.8×10⁷,合计约 1.2×10⁸ 次加法。C++ 里一次简单加法约 1~2 ns,总时间 0.2 s 量级。

三、别只看渐进次数,还要看常数。莫队的每一步都要读写数组、比较下标,实际代价是纯加法的 2~3 倍。经验值:6×10⁷ 步莫队 ≈ 0.3~0.6 s;10⁸ 次纯加法 ≈ 0.2 s;而 10⁹ 次无论怎么优化都会超 1 s。

四、阈值不一定是 √n。若修改 O(T) 很贵而查询便宜(比如修改要动 T 张表),就设“修改次数 m₁、查询次数 m₂”,对 nT + m₁T + m₂·n/T 求导,得 T = √(m₂n / (n + m₁))。先写出代价函数再解,永远比背 √n 可靠。

举一反三 · 想清楚这些再往下走
  1. 莫队必须是离线算法。如果询问强制在线(每问的输入依赖上一问的答案),莫队还能用吗?替代方案是什么?
  2. 如果 add 是 O(1) 但 del 是 O(log n)(比如维护区间内不同数的和),块长还取 n/√m 吗?代价函数该怎么写?
  3. 带修改的莫队(三维莫队)多一维“时间”,三个指针怎么排序?复杂度会从 O(n√m) 变成多少?
  4. 区间最大值能不能用普通莫队?“只加不删”的回滚莫队是怎么绕开这个限制的?
  5. 根号分治的两种形态(按参数、按出现次数)分别适合什么样的题干信号?各举一个你能想到的场景。
  6. 树上路径的颜色种数问题,怎么把“树上的路径”变成“序列上的区间”,从而直接套莫队?
自测:能全部回答再进下一节
挑战题 · 数列找不同
洛谷 P3901普及/提高-去洛谷做这题 ↗

给定长度为 N 的数列 A₁…A_N,以及 Q 个询问 (Lᵢ, Rᵢ),每次询问 A[Lᵢ..Rᵢ] 这一段是否互不相同。

输入格式:第一行两个整数 N、Q;第二行 N 个整数;接下来 Q 行,每行两个整数 Lᵢ、Rᵢ。
输出格式:每个询问输出一行,Yes 或 No。

数据范围:1 ≤ N, Q ≤ 10⁵,1 ≤ Aᵢ ≤ N,1 ≤ Lᵢ ≤ Rᵢ ≤ N。

引导性提示(先只看这里)

“互不相同”不需要知道每个数出现几次,只需要知道一件事:有没有哪个值出现了两次以上。

维护一个计数器 bad,表示“当前区间里出现次数 ≥ 2 的值有几个”。区间合法 ⟺ bad == 0。

加入一个值时 bad 什么时候加 1?删除一个值时 bad 什么时候减 1?想清楚这两个临界点(都是 1 和 2 之间),剩下全是模板。

想看思路了?点开完整思路

为什么可以直接用普通莫队。需要维护的量是“有多少个值重复了”,它在加入/删除一个元素时只可能变化 1,O(1) 更新,满足莫队的前提。

状态。cnt[v]:值 v 在当前区间的出现次数;bad:满足 cnt[v] ≥ 2 的 v 的个数。

add(p)。先 ++cnt[a[p]];若恰好变成 2,说明这个值刚刚跨过“只出现一次”的界线,++bad。次数从 2 变 3 时 bad 不变——它数的是“重复的值有几个”,不是“重复的对有几对”。

del(p)。先判断 cnt[a[p]] == 2:若成立,删掉它之后这个值不再重复,--bad;再 --cnt[a[p]]。顺序不能反,减完就分不清 1 和 2 了。

答案。bad == 0 ? Yes : No。按块排序 + 四个 while 挪指针,总移动 O(N√Q) ≈ 6×10⁷,每次 O(1)。

另解。离线按右端点排序 + 树状数组可以做到 O((N+Q) log N),更快但要另想一层;本题用莫队练手正好。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

const int N = 100005;
int n, m, B;
int a[N], cnt[N];
int bad;                                    // 出现次数 >= 2 的值的个数
int ans[N];

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

bool cmp(const Query &x, const Query &y) {
    int bx = x.l / B, by = y.l / B;
    if (bx != by) return bx < by;
    return (bx & 1) ? x.r > y.r : x.r < y.r;     // 奇数块 r 倒序,省一趟回程
}

void add(int p) {
    if (++cnt[a[p]] == 2) ++bad;             // 1 -> 2:这个值开始重复
}

void del(int p) {
    if (cnt[a[p]] == 2) --bad;               // 2 -> 1:删掉后不再重复
    --cnt[a[p]];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    B = max(1, (int)(n / sqrt(m)));          // 理论最优块长 n/√m
    for (int i = 1; i <= m; ++i) { cin >> q[i].l >> q[i].r; q[i].id = i; }
    sort(q + 1, q + m + 1, cmp);

    int L = 1, R = 0;
    for (int i = 1; i <= m; ++i) {
        while (L > q[i].l) add(--L);
        while (R < q[i].r) add(++R);
        while (L < q[i].l) del(L++);
        while (R > q[i].r) del(R--);
        ans[q[i].id] = (bad == 0);
    }

    for (int i = 1; i <= m; ++i) cout << (ans[i] ? "Yes" : "No") << "\n";
    return 0;
}
关键片段拆解
片段作用
if (++cnt[a[p]] == 2) ++bad;只在 1 → 2 这一刻计数。bad 记“重复的值有几个”,不是“重复的对有几对”。
if (cnt[a[p]] == 2) --bad;必须在 --cnt 之前判断:减完就再也分不清 1 和 2 了。
ans[q[i].id] = (bad == 0)区间互不相同 ⟺ 没有任何值的出现次数达到 2。存 0/1 便于最后输出。
B = max(1, (int)(n / sqrt(m)))N = Q = 10⁵ 时约 316。取 max 是为了防 B 算成 0——B = 0 会让 x.l / B 直接除零崩掉。
sort 后再回填 ans排序打乱了询问顺序,答案必须按原编号存回,否则输出顺序全错。
四个 while 的顺序先扩后缩只是习惯;真正要求的是 add/del 与指针移动方向严格对应,写错会统计到区间外的元素。
这题最容易错的地方
  • 把 bad 记成“重复对数”(每次 add 都加 cnt−1):删除时无法对称还原 → WA,而且 bad 会到 O(N²) 级别。
  • del 里先 --cnt 再判断是否等于 2:临界点永远碰不到,bad 只增不减 → 从某个询问开始全输出 No。
  • 块长取 1 或取 N:总移动退化到 O(N²) → TLE,小数据却看不出问题。
  • 答案按排序后的顺序输出而不是按 id 回填 → WA,而且错得很隐蔽:数值全对,只有顺序错。
  • cnt 数组按“最大询问长度”开而不是按值域开:Aᵢ 可以等于 N,必须开 N+1。
延伸阅读 / 参考