「学习笔记」优美的暴力——莫队
前置芝士
分块的思想。
暴力
字母/变量的含义
\(n\):序列的长度。
\(m\):询问个数。
\(d\):块的大小。
\(num[i]\):\(i\) 这个元素所在的块的编号。
\(q[i]\):一个询问。
\(q[i].x\):一个询问的左端点。
\(q[i].y\):一个询问的右端点。
\(q[i].id\):一个询问的编号。
普通莫队
引
考虑这样一个问题:
一个长为 \(n\) 的序列,\(m\) 次询问,每次询问 \([l,r]\) 这个区间的元素的和。
\(0 < n,m \leq 100000\)
考虑暴力,每次扫描 \([l,r]\) 这个区间得到答案,时间复杂度 \(O(nm)\)。
考虑优化,假如已知 \([x,y]\) 这个区间的答案,下次询问 \([x - 2, y + 2]\) 的答案,可以从上一个答案转移出来。
但这样有可能会被卡比如,\([1,1],[n,n],[2,2],[n-1,n-1],\dots\),交替问你。
再考虑优化,对询问离线,按照一种规则去排序,使得左右指针移动次数尽可能少。
流程
先将原序列进行一次分块操作(仅用到了编号),将询问按照如下规则排序,左端点在同一块内的按右端点升序,否则按块的序号升序。
询问排好序之后就按暴力转移的方法来搞。
例题讲解
计算当前多次出现的数的个数。
Code:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
#define M 100001
bool ans[M];
int n, m, sqrn, a[M], num[M], cnt[M];
struct query {
int x, y, id;
friend bool operator < (query q1, query q2) {
if (num[q1.x] == num[q2.x]) return q1.y < q2.y;
return num[q1.x] < num[q2.x];
}
}q[M];
void del(int x, int &now) {
if (cnt[x] == 2) --now;
--cnt[x];
}
void add(int x, int &now) {
++cnt[x];
if (cnt[x] == 2) ++now;
}
int main() {
scanf("%d %d", &n, &m), sqrn = sqrt(n);
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
num[i] = (i - 1) / sqrn + 1;
}
for (int i = 1; i <= m; ++i) {
scanf("%d %d", &q[i].x, &q[i].y);
q[i].id = i;
}
std::sort(q + 1, q + m + 1);
int l = 1, r = 1, now = 0;
cnt[a[1]] = 1;
for (int i = 1; i <= m; ++i) {
while (l < q[i].x) del(a[l++], now);
while (l > q[i].x) add(a[--l], now);
while (r < q[i].y) add(a[++r], now);
while (r > q[i].y) del(a[r--], now);
if (!now) ans[q[i].id] = true;
else ans[q[i].id] = false;
}
for (int i = 1; i <= m; ++i) {
if (ans[i]) puts("Yes");
else puts("No");
}
return 0;
}
处理的顺序应该按照先插入元素再删除元素,否则,如果维护了一个集合并且先删除元素的话可能会出现删除集合中不存在的元素的情况。
要求
- 已知 \([l,r]\) 的答案要能够 \(O(1)\) 的转移出 \([l - 1, r], [l, r - 1], [l + 1, r], [l, r + 1]\) 的答案。
- 可以离线。
- 无修改。
时间复杂度分析
-
同一块内的询问右端点最多总共移动 \(n\) 次,有 \(\dfrac{n}{d}\) 个块,时间复杂度为 \(O(\dfrac{n^2}{d})\)。
-
换块时,右端点最多移动 \(n\) 次(从最右边移动到最左边),最多换 \(\dfrac{n}{d}\) 次块,时间复杂度为 \(O(\dfrac{n^2}{d})\)。
-
同一个块内的询问左端点每次移动不超过 \(d\) 次,共 \(m\) 次询问,时间复杂度为 \(O(dm)\)。
-
换块时左端点最多总共移动 \(n\) 次,太少了忽略掉。
总的时间复杂度为 \(O(dm+\dfrac{n^2}{d})\)。是一个对勾函数,最小值在 \(\dfrac{n}{\sqrt m}\) 处取得(注意有可能 \(n\) 很小,\(m\) 很大,这时块的大小变成了 \(0\))。
练习题
CF617E XOR and Favorite Number
带修莫队
引
上述莫队算法实在没有修改的要求下成立的,如果带修改还想用莫队该怎么办。
多了一个修改的操作,(应该是单点修改,窝还没见过区间修改的),改了一个点可以当做删一个数再加一个数,类似普通莫队。
像是在普通莫队的左右端点上又加了一个时间端点(已经修改了几次)
流程
基本与普通莫队一致,不一样的是处理询问时多了两个 while
循环来移动时间端点。
例题讲解
计算当前区间内不同的数的个数。
需要注意的操作有对修改的存储,对询问的存储/排序,对时间端点的移动。
Code:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
#define M 1033335
inline void read(int &T) {
int x = 0;
bool f = 0;
char c = getchar();
while (c < '0' || c > '9') {
if (c == '-') f = !f;
c = getchar();
}
while (c >= '0' && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
T = f ? -x : x;
}
int n, m, cq, cc, sqrn, a[M], num[M], cnt[M], ans[M];
struct query {
int x, y, t, id;
friend bool operator < (query q1, query q2) {
if (num[q1.x] == num[q2.x] && num[q1.y] == num[q2.y]) return q1.t < q2.t;
if (num[q1.x] == num[q2.x]) return q1.y < q2.y;
return num[q1.x] < num[q2.x];
}
}q[M];
struct change {
int pos, w;
}c[M];
void add(int x, int &now) {
++cnt[x];
if (cnt[x] == 1) ++now;
}
void del(int x, int &now) {
--cnt[x];
if (cnt[x] == 0) --now;
}
int main() {
read(n), read(m); sqrn = pow(n, 2.0 / 3.0);
for (int i = 1; i <= n; ++i) {
read(a[i]);
num[i] = (i - 1) / sqrn + 1;
}
char opt;
for (int i = 1, x, y; i <= m; ++i) {
std::cin >> opt;
read(x), read(y);
if (opt == 'R') {
c[++cc].pos = x, c[cc].w = y;
}
else {
q[++cq].t = cc, q[cq].id = cq;
q[cq].x = x, q[cq].y = y;
}
}
std::sort(q + 1, q + cq + 1);
int l = 1, r = 1, t = 0, now = 1;
cnt[a[1]] = 1;
for (int i = 1; i <= cq; ++i) {
while (l > q[i].x) add(a[--l], now);
while (r < q[i].y) add(a[++r], now);
while (l < q[i].x) del(a[l++], now);
while (r > q[i].y) del(a[r--], now);
while (t > q[i].t) {
if (q[i].y >= c[t].pos && q[i].x <= c[t].pos) del(a[c[t].pos], now);
std::swap(a[c[t].pos], c[t].w);
if (q[i].y >= c[t].pos && q[i].x <= c[t].pos) add(a[c[t].pos], now);
--t;
}
while (t < q[i].t) {
++t;
if (q[i].y >= c[t].pos && q[i].x <= c[t].pos) del(a[c[t].pos], now);
std::swap(a[c[t].pos], c[t].w);
if (q[i].y >= c[t].pos && q[i].x <= c[t].pos) add(a[c[t].pos], now);
}
ans[q[i].id] = now;
}
for (int i = 1; i <= cq; ++i) {
printf("%d\n", ans[i]);
}
return 0;
}
要求
- 已知 \([l,r]\) 的答案要能够 \(O(1)\) 的转移出 \([l - 1, r], [l, r - 1], [l + 1, r], [l, r + 1]\) 的答案。
- 可以离线。
回滚莫队
在当前已知的区间中要加入元素或移除元素来得到新的答案,可能其中一种操作很难做到 \(O(1)\)。
那么就想到都变成一种操作。对于一个块内的 \(O(d)\) 来统计答案。
如果移除元素不好做就按下面的方式排序
struct query {//存询问的结构体
int x, y, id;
friend bool operator < (query q1, query q2) {
if (num[q1.x] != num[q2.x]) return num[q1.x] < num[q2.x];
return q1.y < q2.y;
}
}q[MAXN];
询问的左端点和右端点不在一个块内的话,对于左端点在同一块内的这一类型的询问,右端点一定递增,左端点所在的块暴力计算 \(O(d)\),右端点的可以继承所以是 \(O(n)\)。
按照这个思想,每一个块,左端点应该设为 \(\min(num[q[i].x] \times d, n) + 1\),右端点应该设为 \(\min(num[q[i].x] \times d, n)\)。
如果加入元素不好做就按照下面的方式排序
struct query {//存询问的结构体
int x, y, id;
friend bool operator < ( query q1, query q2) {
if (num[q1.x] != num[q2.x]) return num[q1.x] < num[q2.x];
return q1.y > q2.y;
}
}q[MAXN];
询问的左端点和右端点不在一个块内的话,对于左端点在同一块内的这一类型的询问,右端点一定递减,左端点所在的块暴力计算 \(O(d)\),右端点的可以继承所以是 \(O(n)\)。
按照这个思想,每一个块,左端点应该设为 \((num[q[i].x] - 1) \times d\),右端点应该设为 \(n\)。
看几个题
P5906 【模板】回滚莫队&不删除莫队
题解
AtCoder 1219 歴史の研究
上面俩是删除操作不好搞的。
P4137 Rmq Problem / mex
这个是加数的操作不好搞的,需要卡卡常。
树上莫队
用欧拉序将树上变成序列上
想不到这么短吧,有时间再改