莫队
唉,可惜了,分块要是在线算法多好啊!
本质与来历
莫队算法是由我国莫涛选手提出的一种基于分块思想的离线区间查询算法。它通过对查询问题进行排序,优化双指针移动的暴力扫描过程,从而将原本 \(O(NM)\) 的复杂度降低到 \(O(N\sqrt{N})\)。
普通莫队
核心
通过两个指针 \(l, r\) 表示所到达的区间,我们通过移动两个指针,边移边算,在到达我们要查询的区间 \([Q_l, Q_r]\) 刚好把答案算出来了。
排序
既然是移动两个指针,那么一定要有一个规则。
排序规则:
1、将长度为 \(N\) 的序列划分为若干个大小为 \(B\)(通常取 \(B = \sqrt{N}\))的块。
2、对所有询问进行排序:
- 优先按左端点所在的块编号升序排序。
- 若左端点在同一个块内,则按右端点升序排序。
还有一个常数优化:
如果左端点在奇数块,右端点按升序排序;如果在偶数块,右端点按降序排序。这样右指针在处理完一个块后不需要“跑回起点”,而是可以“蛇形折返”,能直接砍掉一半的常数。
那么剩下的就简单了。
用个例题练练手?
D-query
简化题意
给你一个序列 \(a_1,a_2,\dots,a_n\),每次查询一个区间 \([l,r]\),求这个区间有多少个不同数的数量。
做法
我们看到这个首先可以想到数据结构,但是这里不是数据结构专题,所以不讲。
那么对于莫队太简单了。
我们用一个 \(cnt\) 数组存储我们当前区间 \([l, r]\) 中不同数的数量。
那么我们在移动指针的时候就顺便处理了。
看代码会好理解一点。
戳我喵~
struct node { //查询
int l, r, id;
}q[N];
int pos[N], block, sum; //分块排序
int now;
int a[N];
int cnt[N]; //记录不同数字的数量
int ans[N]; //记录答案
bool cmp(node a, node b) {
return pos[a.l] == pos[b.l] ? a.r < b.r : pos[a.l] < pos[b.l]; //神秘的排序
}
void add(int x) { //添加答案
if (!cnt[a[x]]) now++;
cnt[a[x]]++;
}
void del(int x) { //删掉
cnt[a[x]]--;
if (!cnt[a[x]]) now--;
}
signed main() {
int n = re;
block = (int)sqrt(n), sum = n / block;
if (n % block) sum++;
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / block + 1;
for (int i = 1; i <= n; i++) a[i] = re;
int m = re;
for (int i = 1; i <= m; i++) q[i] = {re, re, i};
sort(q + 1, q + 1 + m, cmp); //对查询排个序会减小复杂度
int l = 1, r = 0;
for (int i = 1; i <= m; i++) {
while (l > q[i].l) add(--l); //最好先向两个不同的方向移,不然有可能 l > r
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] = now; //按顺序记录答案
}
for (int i = 1; i <= m; i++) wr(ans[i]), endl;
}
带修莫队
普通的莫队不能修改,但是他进化啦!(超进化)~
带修莫队可以修改了,但是加了一维时间戳,用于返回与更新状态用。
\(T\)的定义: 表示在这个询问发生之前,原序列一共进行了多少次修改操作。
双指针变三指针: 除了维护区间左右边界的 \(l\) 和 \(r\) 指针,我们还需要增加一个时间指针 \(t\)。
- 当
t < 询问的 T时,说明当前状态“太旧了”,我们需要执行时间轴上的下一条修改指令。 - 当
t > 询问的 T时,说明当前状态“太超前了”,我们需要撤销时间轴上的上一条修改指令。
它又需要在排序规则如下:
- 左端点所在的块编号升序。
- 若左端点同块,按右端点所在的块编号升序(注意,这里不再是右端点本身,而是右端点所在的块)。
- 若左右端点都在各自相同的块内,按时间戳 \(T\)升序。
- 奇偶化排序(常数优化(普通莫队适用))依然适用:若右端点所在块是奇数,时间戳升序;若是偶数,时间戳降序,同样可以优化时间指针的回调常数。
最后的平均复杂度为 \(O(N^{\frac{5}{3}})\)
例题:P1903 【模板】带修莫队 / [国家集训队] 数颜色 / 维护队列
题面
墨墨购买了一套 \(N\) 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:
-
\(Q\ L\ R\) 代表询问你从第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。
-
\(R\ P\ C\) 把第 \(P\) 支画笔替换为颜色 \(C\)。
做法
我们在双指针 \(l\) 和 \(r\) 的基础上,增加时间指针 \(t\)。 当移动 \(t\) 指针去追赶目标查询的时间戳时,我们需要修改原数组的值。如果被修改的位置恰好在当前维护的 \([l,r]\) 区间内,我们需要先将该颜色的贡献从当前答案中扣除,完成修改后,再将新颜色的贡献加入答案。
撤销操作: 我们在执行修改时,直接使用 swap(a[pos], q[t].val)。这样不仅把新颜色填入了原数组,顺便把“旧颜色”存回去了(顺手的事)。当需要撤销修改时,同上,旧颜色就被换回去了。
戳我喵~
struct node {
int l, r, id, t;
}q[N], qq[N];
int n, m, block;
int now;
int a[N], cnt[N], ans[N];
int x, y;
bool cmp(const node &a, const node &b) {
return a.l / block == b.l / block ? a.r / block == b.r / block ? a.t < b.t : a.r < b.r : a.l < b.l;
}
inline void add(int x) { now += !cnt[x]++; }
inline void del(int x) { now -= !--cnt[x]; }
inline void update(int x, int t) { //这是主要!
if (q[x].l <= qq[t].l && qq[t].l <= q[x].r) del(a[qq[t].l]), add(qq[t].r);
swap(a[qq[t].l], qq[t].r);
}
signed main() {
n = re, m = re;
block = pow(n, 2.0 / 3.0);
for (int i = 1; i <= n; i++) a[i] = re;
for (int i = 1; i <= m; i++) {
char op;
cin >> op;
int l = re, r = re;
if (op == 'Q') ++x, q[x].id = x, q[x].l = l, q[x].r = r, q[x].t = y;
else qq[++y].l = l, qq[y].r = r;
}
sort(q + 1, q + 1 + x, cmp);
int l = 1, r = 0, t = 0;
for (int i = 1; i <= x; i++) {
while (l > q[i].l) add(a[--l]);
while (l < q[i].l) del(a[l++]);
while (r > q[i].r) del(a[r--]);
while (r < q[i].r) add(a[++r]);
while (t < q[i].t) update(i, ++t);
while (t > q[i].t) update(i, t--);
ans[q[i].id] = now;
}
for (int i = 1; i <= x; i++) wr(ans[i]), endl;
}

浙公网安备 33010602011771号