Appearance
默认的莫队时间复杂度分析仅考虑指针移动的次数,具体问题中,还需要考虑指针移动时维护信息的时间复杂度,和处理询问答案时的时间复杂度,因此具体实现时,可以根据这个均衡块长。
若无特殊说明,本文时间复杂度分析均为指针移动次数分析。
离线莫队
正经莫队。
普通莫队
普通莫队是一个基于分块的离线解决静态区间询问问题的算法。
设询问区间为
算法流程:
对于每个询问
此时,升序后的
时间复杂度分析:
综上:处理完
当
认为
优化:
奇偶化排序:对奇数块内的询问的
这是很自然的,因为按原本的排序方式,每次进入一个新块时,
点击展开代码
c++
struct Query {
int l, r, id;
bool operator<(const Query &t) const {
return (kind[l] ^ kind[t.l]) ? kind[l] < kind[t.l]
: ((kind[l] & 1) ? r < t.r : r > t.r);
}
};
void add(int x) {
/*
将 a[x] 加入答案
*/
}
void del(int x) {
/*
将 a[x] 删去
*/
}
void solve() {
for (int i = 1; i <= n; i++)
kind[i] = (i - 1) / B;
sort(q + 1, q + 1 + m);
int l = 1, r = 0;
for (int i = 1; i <= m; i++) {
while (q[i].l < l) add(--l);
while (q[i].r > r) add(++r);
while (q[i].l > l) del(l++);
while (q[i].r < r) del(r--);
/*
ans[q[i].id] = ...
*/
}
}回滚莫队
回滚莫队用于 add 容易维护,del 不易维护的问题,可以将 del 操作转换成 add 操作。
算法流程:
在普通莫队中,处理
- 若
也在 的块内,询问区间长度 ,直接暴力处理。 - 若
不在 的块内,按 升序 add,对于相同的 的不同 ,因为 在同一块内,所以直接重新从 所在块的右边界 add 过去。
这样实现时,不同块的
左端点在块内回滚时,最无脑的做法就是开一个 history 栈,把所有数组和修改的位置和值存下来,通过清空栈回退。
时间复杂度分析:
处理过程中,右端点移动
点击展开代码
cpp
struct Query {
int l, r, id;
bool operator<(const Query &t) const {
if (kind[l] != kind[t.l])
return kind[l] < kind[t.l];
return r < t.r;
}
} q[N];
void solve() {
for (int i = 1; i <= n; i++)
kind[i] = (i - 1) / B + 1;
sort(q + 1, q + 1 + m);
int i = 1, l, r, res = 0;
auto add = [&](){
};
while (i <= m) {
int j = i;
while (j <= m && kind[q[i].l] == kind[q[j].l])
j++;
int right = min(n, kind[q[i].l] * B);
while (i < j && q[i].r <= right) {
res = 0;
for (int k = q[i].l; k <= q[i].r; k++)
add(k);
/*
ans[q[i].id] = ...;
*/
// 清空信息
i++;
}
l = right + 1, r = right;
while (i < j) {
while (r < q[i].r)
add(++r);
/*
拷贝旧信息
*/
while (l > q[i].l)
add(--l);
/*
ans[q[i].id] = ...;
*/
while(l < right + 1){
l++;// 回滚
}
/*
回滚到旧信息
*/
i++;
}
// 清空信息
}
}只减不删的回滚莫队
如果信息是 del 容易维护,add 不容易维护,那么可以每次,左端点从块的左端点向右移动,右端点从
全局信息从
综上,回滚莫队严格强于普通莫队,唯一的问题是常数较普通莫队略大,且实现相对复杂。
带修莫队
算法流程:
带修莫队与普通莫队相比,多了一维时间轴表示每修改一次,时间增加
把询问表示成
将三元组按照某种规则排序后考虑指针移动次数。
令
| 排序关键字 | L 移动次数 | R 移动次数 | T 移动次数 | 总移动次数 |
|---|---|---|---|---|
把
| 排序关键字 | 总移动次数 |
|---|---|
时间复杂度分析:
对于所有排序规则分析取最小值,只有
认为序列大小
优化:
和普通莫队奇偶排序类似地,带修莫队多一维,进行蛇形排序。
点击展开代码
cpp
struct Query {
int l, r, t, id;
bool operator<(const Query &x) const
{
if (kind[l] != kind[x.l])
return l < x.l;
if (kind[r] != kind[x.r])
return (kind[l] & 1) ? r < x.r : r > x.r;
return (kind[r] & 1) ? t < x.t : t > x.t;
}
};
void solve() {
for (int i = 1; i <= n; i++)
kind[i] = (i - 1) / B;
for (int i = 1; i <= m; i++) {
{
m1++;
q[m1] = {l, r, m1, m2};
}
{
c[++m2] = {x, v};
}
}
sort(q + 1, q + 1 + m1);
auto add = [&](int x){
};
auto del = [&](int x){
};
int l = 1, r = 0, t = 0;
for (int i = 1; i <= m1; i++) {
while (q[i].l < l) add(a[--l]);
while (q[i].r > r) add(a[++r]);
while (q[i].l > l) del(a[l++]);
while (q[i].r < r) del(a[r--]);
while (t < q[i].t)
{
t++;
auto &[now, cur] = c[t];
if (now >= l && now <= r)
{
del(a[now]);
add(cur);
}
swap(a[now], cur);
}
while (t > q[i].t)
{
auto &[now, cur] = c[t];
if (now >= l && now <= r)
{
del(a[now]);
add(cur);
}
swap(a[now], cur);
t--;
}
/*
ans[q[i].id] = ...;
*/
}
}树上莫队
树上莫队就是莫队上树,询问树上路径信息,因为莫队通过 add 和 del 维护信息,所以在括号序上跑莫队即可。括号序会通过一次 add 和一次 del 将树上两点的 LCA 的祖先信息去掉。
如果是询问子树信息,那么通常通过 dfs 序转换成连续的区间处理,后续同其他莫队。
算法流程:
通过「欧拉序」将树上的一条路径转换成序列上一段连续的区间。
这一部分本质是「欧拉序」的技巧。
对于路径
- 若
,则路径上的点在 上出现奇数次。 - 若
,则除 外路径上的点在 上出现奇数次,此时要给这组询问特殊加一个 单点的指针扩展。
指针移动时,维护变化元素的奇偶次数,奇相当于加入,偶相当于删去。
注:不难发现,树上莫队维护时,必定需要支持 add 和 del,所以不能处理「树上回滚莫队」。
时间复杂度分析:
与普通莫队一致。
二次离线莫队
算法流程:
普通莫队进行指针移动时,如果 add 和 del 操作的时间复杂度不为
具体而言,处理每一个询问的指针移动时,
以
如果
继续拆成两部分:
第一部分,令
第二部分,以一个区间形式「二次离线」(目的是做到线性空间),需要被计算的
回滚莫队二次离线同理。
剩下三种指针移动情况类似,具体贡献范围下标略有区别。
注意最后要将询问的贡献按莫队排序后的顺序前缀累计。
时间复杂度分析:
第二部分的贡献仍然计算
点击展开代码
cpp
auto addEvent = [&](int p, int L, int R, int id, int k)
{
event[p].push_back({L, R, id, k});
};
for (int i = 1; i <= m; i++)
{
int ql = q[i].l, qr = q[i].r, id = q[i].id;
if (ql < l)
{
int L = ql;
int R = l - 1;
addEvent(r, L, R, id, 1);
ans[id] -= sum2(L, R);
l = ql;
}
if (qr > r)
{
int L = r + 1;
int R = qr;
ans[id] += sum1(L, R);
addEvent(l - 1, L, R, id, -1);
r = qr;
}
if (ql > l)
{
int L = l;
int R = ql - 1;
addEvent(r, L, R, id, -1);
ans[id] += sum2(L, R);
l = ql;
}
if (qr < r)
{
int L = qr + 1;
int R = r;
ans[id] -= sum1(L, R);
addEvent(l - 1, L, R, id, 1);
r = qr;
}
}在线莫队
和“莫队”实际没有什么关系,随着时代发展,普及的技巧而已。
在线普通莫队
对原序列预处理
询问
卡常优化:从离
时间复杂度:
以区间不同数的个数为例,那么就是
空间复杂度:
此时,
如果信息可以差分,以区间不同数的个数为例,维护数的个数,可以调用
注:也可以说这是「在线的回滚莫队」,因为它可以处理仅 add 或仅 del。
在线带修莫队
在在线普通莫队的基础上,为
所以时间复杂度加上
莫队卡常
计算变化量
莫队移动指针时,考虑对答案的影响,可以不重新计算,只考虑变化量。
下标连续访问
莫队指针移动在序列上是连续的,如果问题涉及位置上的值,使用桶维护值域的时候,可以采用特殊的离散化方式,将原序列上连续的一段尽可能连续。或者直接把统计方式放在序列的位置上,因为位置的指针移动本身就是连续的。
