初识莫队
莫队
个人理解:
这是一种较为暴力的算法,适用于解决维护序列区间操作的问题。主要思想:
- 把所有的操作离线,按某种方式重新排序。
- 操作过程中不断转移当前区间的答案。
- (\([L, R]\Rightarrow [L \pm 1, R \pm 1]\))
- 希望转移的复杂度尽量的小(\(O(n\sqrt{m})\))
01-普通的莫队(无修改)
01-例题:P3901 数列找不同
01-题意
现有数列 \(A_1,A_2,\ldots,A_N\),\(Q\) 个询问 \((L_i,R_i)\),询问 \(A_{L_i} ,A_{L_i+1},\ldots,A_{R_i}\) 是否互不相同。
对每个询问输出一行,Yes
或 No
。
\(1 \le N,Q \le 10^5\),\(1 \le A_i \le N\),\(1 \le L_i \le R_i \le N\)。
02-分析
考虑维护两个指针l
,r
,对于每次询问 \([L, R]\) ,把 l
, r
移动到 \([L,R]\),在移动的过程中不断统计不同的数的个数tot
,如果 tot != r - l + 1
,那么答案就为No
,反之Yes
。
cin >> n >> q;
for (int i = 1; i <= n; i++)
cin >> a[i];
auto add = [&](int x){ if (cnt[x] == 0) tot++; cnt[x]++; };
auto del = [&](int x) {if (cnt[x] == 1) tot--; cnt[x]--; };
for (int l = 0, r = 0, L, R; q--; ){
cin >> L >> R;
while(l < L) del(a[l++]); // l L R
while(r < R) add(a[++r]); // L r R
while(l > L) add(a[--l]); // L l R
while(r > R) del(a[r--]); // L R r
cout << (tot == r - l + 1 ? "Yes" : "No") << endl;
}
显而易见的是,这并不能通过本题。
瓶颈在于每次询问都移动了l
,r
,这使得时间复杂度来到了 \(O(qn)\),无法接受。
有没有一种方法,让这两个指针动的快一点?(每次移动的距离尽可能小)
发现有以下性质:
- 询问的处理顺序与给出的顺序无关。
- 没了。
这启发我们可以通过改变询问的顺序,使得l
,r
更快的到达新的 \([L,R]\) 。
考虑先离线所有的询问,进行一个根号分块。
对于储存询问的两个结构体a, b
:
首先判断
a.l / B == b.l / B
,即是否在同一块。若在同一块,则按
r
大小排序,a.r < b.r
。反之,则按
l
排序,a.l > b.l
。
struct node {
int l, r, id;
} q[N];
for (int i = 1; i <= m; i++)
cin >> q[i].l >> q[i].r, q[i].id = i;
int B = n / sqrt(m);
sort(q + 1, q + 1 + m, [](node a, node b) {
return a.l / B == b.l / B ? a,r < b.r : a.l < b.l;
}); // 再也不需要cmp了
按照排序后的顺序操作,这样就使得复杂度降到了 \(O(n \sqrt{m})\) 。 足以通过本题。
02-例题:P1972 HH的项链 (LOJ)
- (莫队只能过LOJ的数据,洛谷加强了)
01-题意
HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。
有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。
一行一个正整数 \(n\),表示项链长度。
第二行 \(n\) 个正整数 \(a_i\),表示项链中第 \(i\) 个贝壳的种类。
第三行一个整数 \(m\),表示 HH 询问的个数。
接下来 \(m\) 行,每行两个整数 \(l,r\),表示询问的区间。
对于 \(100\%\) 的数据,\(1\le n,m,a_i \leq 10^6\),\(1\le l \le r \le n\)。
02-分析
与上一题一样,做法差不多,只是问的东西不一样了。
注意莫队只能过LOJ,洛谷的数据加强了,要用树桩数组。
03-例题:P2709 小B的询问
01-题意
小B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)。
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求:
其中 \(c_i\) 表示数字 \(i\) 在 \([l,r]\) 中的出现次数。
对于 \(100\%\) 的数据,\(1\le n,m,k \le 5\times 10^4\)。
02-分析
与上一题一致,依题意模拟即可。
需要推柿子。
04-例题 :P1494 小 Z 的袜子
01-题意
作为一个生活散漫的人,小 Z 每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小 Z 再也无法忍受这恼人的找袜子过程,于是他决定听天由命……
具体来说,小 Z 把这 \(N\) 只袜子从 \(1\) 到 \(N\) 编号,然后从编号 \(L\) 到 \(R\) 的袜子中随机选出两只来穿。尽管小 Z 并不在意两只袜子是不是完整的一双,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。
你的任务便是告诉小 Z,他有多大的概率抽到两只颜色相同的袜子。当然,小 Z 希望这个概率尽量高,所以他可能会询问多个 \((L,R)\) 以方便自己选择。
然而数据中有 \(L=R\) 的情况,请特判这种情况,输出0/1
。
输入文件第一行包含两个正整数 \(N\) 和 \(M\)。\(N\) 为袜子的数量,\(M\) 为小 Z 所提的询问的数量。接下来一行包含 \(N\) 个正整数 \(C_i\),其中 \(C_i\) 表示第 \(i\) 只袜子的颜色,相同的颜色用相同的数字表示。再接下来 \(M\) 行,每行两个正整数 \(L, R\) 表示一个询问。
包含 \(M\) 行,对于每个询问在一行中输出分数 \(A/B\) 表示从该询问的区间 \([L,R]\) 中随机抽出两只袜子颜色相同的概率。若该概率为 \(0\) 则输出 0/1
,否则输出的 \(A/B\) 必须为最简分数。
\(100\%\) 的数据中,\(N,M \leq 50000\),\(1 \leq L < R \leq N\),\(C_i \leq N\)。
02-分析
同样维护袜子的种类数,注意同时维护用来算概率的组合数。
05-例题 P7764 Poklon
统计区间内出现两次的数,依题意模拟即可。
02-带修莫队
注意到刚刚的例题中都没有修改的情况,莫队同样能处理修改,但更加复杂,不过本质相同。
P1903 数颜色 / 维护队列
01-题意
墨墨购买了一套 \(N\) 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:
-
\(Q\ L\ R\) 代表询问你从第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。
-
\(R\ P\ C\) 把第 \(P\) 支画笔替换为颜色 \(C\)。
为了满足墨墨的要求,你知道你需要干什么了吗?
第 \(1\) 行两个整数 \(N\),\(M\),分别代表初始画笔的数量以及墨墨会做的事情的个数。
第 \(2\) 行 \(N\) 个整数,分别代表初始画笔排中第 \(i\) 支画笔的颜色。
第 \(3\) 行到第 \(2+M\) 行,每行分别代表墨墨会做的一件事情,格式见题干部分。
对于每一个 Query 的询问,你需要在对应的行中给出一个数字,代表第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。
对于所有数据,\(n,m \leq 133333\)
所有的输入数据中出现的所有整数均大于等于 \(1\) 且不超过 \(10^6\)。
先写到这里吧
2023年12月5日17:22:22
02-分析
在原来的基础上增加了时间这一个维度,如果按照原来的排序方式,显而易见会得出错误的答案。
如果不想抛弃莫队的思路,如何保证答案的正确性?
既然多了时间这一维度,不妨在转移l
,r
的同时转移一个时间变量t
:
- 如果当前的
t
小于询问的t
,那么upt(++t)
- 如果当前的
t
大于询问的t
,那么upt(t--)
像这样操作,就可以保证时间也对的上了。
注意代码细节:
- 如果在转移
t
的过程中对[l,r]
中的元素进行了修改,那么需要更新当前的答案。 l, r, t
的转移顺序建议是l -> l'
,r -> r'
,t -> t'
。- 分开记录操作序列与询问序列,调整块的大小
B = n / pow(m, 0.666)
。 - 询问的排序方式需要改变:
a.l / B == b.l / B ?
(a.r / B == b.r / B ? a.lst < b.lst : a.r < b.r)
: a.l < b.l
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, m, l, r, t, B;
int qcnt, ccnt, ans;
int a[N], res[N], cnt[N];
char op;
struct node {
int l, r, lst, id;
} q[N], c[N];
void add(int x, int f = 0) {
x = f ? x : a[x];
ans += !cnt[x]++;
}
void del(int x, int f = 0) {
x = f ? x : a[x];
ans -= !--cnt[x];
}
void upt(int k, int x) {
int L = q[k].l, R = q[k].r, idx = c[x].l, &to = c[x].r;
if (L <= idx && idx <= R) {
del(idx);
add(to, 1);
}
swap(a[idx], to);
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
B = pow(n, 0.666);
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1, x, y; i <= m; i++) {
cin >> op >> x >> y;
if (op == 'Q') q[++qcnt] = {x, y, ccnt, qcnt};
else c[++ccnt] = {x, y, 0, 0};
}
sort(q + 1, q + 1 + qcnt, [](node a, node b) {
return a.l / B == b.l / B ? (a.r / B == b.r / B ? a.lst < b.lst : a.r < b.r) : a.l < b.l;
});
l = 1;
for (int i = 1; i <= qcnt; i++) {
while (l < q[i].l) del(l++);
while (l > q[i].l) add(--l);
while (r < q[i].r) add(++r);
while (r > q[i].r) del(r--);
while (t < q[i].lst) upt(i, ++t);
while (t > q[i].lst) upt(i, t--);
res[q[i].id] = ans;
}
for (int i = 1; i <= qcnt; i++)
cout << res[i] << endl;
return 0;
}
莫队进阶用法
03-回滚莫队
04-P5906 最远距离
题目描述
给定一个序列,多次询问一段区间 \([l,r]\),求区间中相同的数的最远间隔距离。
序列中两个元素的间隔距离指的是两个元素下标差的绝对值。
数据范围
之后 \(m\) 行,每行两个整数 \(l,r\) 表示询问区间。
对于 \(100\%\) 的数据,满足 \(1\leq n,m\leq 2\cdot 10^5\),\(1\leq a_i\leq 2\cdot 10^9\)。
分析
对于这样的题目,单单只是普通的莫队难以下手。
添加操作比较简单,而删除操作十分困难。
既然困难,那就不删除了呗
这时候,可以用回滚莫队解决,在做法上与普通莫队稍有不同:
- 对询问按照左端点分块排序,左端点在相同块的询问按照右端点递增排序。
- 依次处理每一个块内的所有询问。
- 对于同一个块的每一个询问,让指针
l
从块的最右边开始移动到对应的位置。 - 而对于右指针
r
,则继承同一块内上一个询问的的位置与答案。 - 每处理完一个询问,将
l
指针挪回最左边,路上消除每个位置的影响。
时间复杂度 \(O(n\sqrt n\ +\) 巨大的常数 \()\) 。
记得离散化和不要用太多的memset
!
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, m, l, r, ans;
int a[N], b[N], res[N];
int cntL[N], cntR[N], cntL_[N], cntR_[N];
int B, tot;
int kuai[N], L[N], R[N];
int used_[N], top_;
struct node{
int l, r, id;
}q[N];
inline void cmin(int &a, int b) { a = min(a, b); }
inline void cmax(int &a, int b) { a = max(a, b); }
void add(int x, int f = 0){
int p = a[x];
if (f) cmin(cntL[p], x);
cmax(cntR[p], x), cmax(ans, cntR[p] - min(cntL[p], x));
}
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> n; B = sqrt(n); tot = n / B;
for (int i = 1; i <= n; i++)
cin >> a[i], b[i] = a[i];
cin >> m;
for (int i = 1; i <= m; i++)
cin >> q[i].l >> q[i].r, q[i].id = i;
sort(b + 1, b + 1 + n);
int len = unique(b + 1, b + 1 + n) - b - 1;
for (int i = 1; i <= n; i++)
a[i] = lower_bound(b + 1, b + 1 + len, a[i]) - b,
kuai[i] = (i - 1) / B + 1;
for (int i = 1; i <= tot; i++)
L[i] = R[i - 1] + 1, R[i] = L[i] + B - 1;
if (R[tot] < n) ++tot, L[tot] = R[tot - 1] + 1, R[tot] = n;
sort(q + 1, q + 1 + m, [](node a, node b){
return kuai[a.l] == kuai[b.l] ? a.r < b.r : a.l < b.l;
});
memset(cntL, 0x3f, sizeof cntL), memset(cntR, 0, sizeof cntR);
memset(cntL_, 0x3f ,sizeof cntL_), memset(cntR_, 0, sizeof cntR_);
for (int i = 1, num = 1; i <= tot; i++){
memset(cntL, 0x3f, sizeof cntL), memset(cntR, 0, sizeof cntR);
// while(top) cntL[used[top]] = 0x3f3f3f3f, cntR[used[top]] = 0, top--;
for (l = R[i] + 1, r = R[i], ans = 0; kuai[q[num].l] == i; num++){
if (kuai[q[num].l] == kuai[q[num].r]){
while(top_) cntL_[used_[top_]] = 0x3f3f3f3f, cntR_[used_[top_]] = 0, top_--;
for (int k = q[num].l; k <= q[num].r; k++) used_[++top_] = a[k], cmin(cntL_[a[k]], k), cmax(cntR_[a[k]], k);
for (int k = q[num].l; k <= q[num].r; k++) cmax(res[q[num].id], cntR_[a[k]] - cntL_[a[k]]);
continue;
}
while(r < q[num].r) add(++r, 1);
int now = ans;
while(l > q[num].l) add(--l);
res[q[num].id] = ans, ans = now;
while(l < R[i] + 1) {
if (cntR[a[l]] == l) cntR[a[l]] = 0;
l++;
}
}
}
for (int i = 1; i <= m; i++)
cout << res[i] << endl;
return 0;
}
Written with StackEdit中文版.