这一节的两件事看上去无关:一件是“把能整除的小模数全部预处理”,一件是“把询问排序后暴力挪指针”。它们的共同动作是往复杂度里塞一个 √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 怎么定 | 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 取得太小,大查询变慢。最优值解方程得到,见数学补给。
| 组成 | 含义 / 格式要求 |
|---|---|
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,所以总复杂度就是这个步数。
对每题都从空区间出发,看起来笨,实际上是“让相邻询问的区间尽量接近”,这个笨办法就是莫队的全部思想。
| 组成 | 含义 / 格式要求 |
|---|---|
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。
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。
暴力:每次询问扫一遍 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,表的内存直接减半,也在缓存里待得更久。
#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 有一个长为 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。这是本题唯一的数值陷阱。
#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 >> k | k 只是值域上界,用来给 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 当常数抄。
| 块长 B | L 总位移 ≤ mB | R 总位移 ≤ n²/B | 合计(n = m = 10⁵) |
|---|---|---|---|
| 1 | 1×10⁵ | 1×10¹⁰ | 约 1×10¹⁰,TLE |
| 100 | 1×10⁷ | 1×10⁸ | 1.1×10⁸ |
| 316 | 3.2×10⁷ | 3.2×10⁷ | 6.3×10⁷,最优 |
| 1000 | 1×10⁸ | 1×10⁷ | 1.1×10⁸ |
| 100000 | 1×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 可靠。
给定长度为 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 与指针移动方向严格对应,写错会统计到区间外的元素。 |
--cnt 再判断是否等于 2:临界点永远碰不到,bad 只增不减 → 从某个询问开始全输出 No。