莫队算法¶
2025-04-28
莫队算法是一种处理区间询问的离线算法, 通过分块可以达到 $n \sqrt{n} $ 的时间复杂度。
最基础的莫队算法是, 把长度为 \(n\) 的区间, 分成大小为 \(\sqrt{n}\) 的块, 然后把询问的 \([l,r]\) 先按照 \(l\) 所在的块的位置排序, 再按 \(r\) 的大小排序。
我们定义 \(L, R\) 用来记录当前维护的答案的区间, 只需要根据当前需要查询的区间动态地去移动 \(L\) 和 \(R\) 。 所以要使用莫队,需要保证能从 \([l, r]\) 的贡献推出 \([l, r+1]\) 的贡献。
莫队也算是一种暴力的算法, 但是时间复杂度却可以做到 $n \sqrt{n} $ 。在一个块内, \(L\) 最多只会移动 \(\sqrt {n} \sqrt{n}\) 次, \(R\) 最多只会移动 \(\sqrt{n}\) 次,而总共只有 \(\sqrt{n}\) 块,所以总体操作的次数最多会是 \(n\sqrt{n} + n\) 次。
普通的¶
基本会是比较板的,直接写就行。
P2709 小B的询问¶
比较模板的题,不需要做什么改变。
struct node {
int l, r, idx;
};
void ChatGptDeepSeek() // Date: 2025-04-25
{ // Time: 15:33:26
int n, m, k;
cin >> n >> m >> k;
vi a(n + 1), c(k + 1), ans(m), pos(n + 1);
int siz = sqrt(n);
for (int i = 1; i <= n; i++)
cin >> a[i], pos[i] = i / siz;
vector<node> Q(m);
for (int i = 0; i < m; i++)
cin >> Q[i].l >> Q[i].r, Q[i].idx = i;
sort(all(Q), [&](node x, node y) {
return (pos[x.l] == pos[y.l]) ? (x.r < y.r) : (pos[x.l] < pos[y.l]);
});
int L = 1, R = 0;
ll res = 0;
auto add = [&](int i) {
assert(i > 0 && i <= n);
res -= (ll)c[a[i]] * c[a[i]];
c[a[i]]++;
res += (ll)c[a[i]] * c[a[i]];
};
auto sub = [&](int i) {
assert(i > 0 && i <= n);
res -= (ll)c[a[i]] * c[a[i]];
c[a[i]]--;
res += (ll)c[a[i]] * c[a[i]];
};
for (int i = 0; i < m; i++) {
auto [l, r, idx] = Q[i];
while(L > l) add(--L);
while(R < r) add(++R);
while(L < l) sub(L++);
while(R > r) sub(R--);
// cerr << L << " " << R << '\n';
ans[idx] = res;
}
for(int i = 0; i < m; i++)
cout << ans[i] << "\n";
}
CF86D¶
做 CF 按 AC 数排序的第一题, 然后感觉不会, 一看题解说要用莫队。 就是从这开始想着要学一下莫队的。
也是只涉及基本的操作的一题。
但是这题也有些帮助, 用 vector 装结构体的提交是 2062ms
, 仅仅把 vector 换成数组, 时间就变成了 1280ms
, 在数据量较大的情况下, 差异还是挺大的。
constexpr int N = int(1e6)+5;
int cnt[N];
struct node{
int l, r, idx;
};
node Q[N];
void ChatGptDeepSeek() // Date: 2025-04-25
{ // Time: 21:27:09
int n, q;
cin >> n >> q;
vi a(n + 1), pos(n + 1);
vl ans(q + 1);
int siz = sqrt(n);
for(int i = 1; i <= n; i++)
cin >> a[i], pos[i] = i / siz;
// vector<node>Q(q);
for(int i = 0; i < q; i++){
cin >> Q[i].l >> Q[i].r;
Q[i].idx = i;
}
sort(Q, Q + q, [&](node x, node y){
return (pos[x.l] == pos[y.l]) ? (x.r < y.r) : (pos[x.l] < pos[y.l]);
});
// sort(all(Q), [&](node x, node y){
// return (pos[x.l] == pos[y.l]) ? (x.r < y.r) : (pos[x.l] < pos[y.l]);
// });
ll res = 0;
auto add = [&](int i){
res -= (ll)cnt[a[i]] * cnt[a[i]] * a[i];
cnt[a[i]]++;
res += (ll)cnt[a[i]] * cnt[a[i]] * a[i];
};
auto sub = [&](int i){
res -= (ll)cnt[a[i]] * cnt[a[i]] * a[i];
cnt[a[i]]--;
res += (ll)cnt[a[i]] * cnt[a[i]] * a[i];
};
int L = 1, R = 0;
for(int i = 0; i < q; i++){
auto [l, r, idx] = Q[i];
while(L > l) add(--L);
while(R < r) add(++R);
while(L < l) sub(L++);
while(R > r) sub(R--);
ans[idx] = res;
}
for(int i = 0; i < q; i++)
cout << ans[i] << "\n";
}
P1494 [国家集训队] 小 Z 的袜子¶
比较板的题。 std::format
真好用hhh, 不过得 C++ 20 才能用。
求一下区间的取到两个相同数字的方式的数量,和总数量取一下 gcd 就行。
constexpr int N = int(5e4)+5;
struct node{
int l, r, idx;
};
node Q[N];
void ChatGptDeepSeek() // Date: 2025-04-26
{ // Time: 00:48:12
/*
洛谷P1494
询问区间的抽到两个相同数字的概率
*/
int n, m;
cin >> n >> m;
int siz = sqrt(n);
vi c(n + 1), pos(n + 1), cnt(n + 1);
for(int i = 1; i <= n; i++)
cin >> c[i], pos[i] = i / siz;
for(int i = 0; i < m; i++){
cin >> Q[i].l >> Q[i].r;
Q[i].idx = i;
}
sort(Q, Q + m, [&](node x, node y){
return (pos[x.l] == pos[y.l]) ? (x.r < y.r) : (pos[x.l] < pos[y.l]);
});
ll res = 0;
auto add = [&](int i){
res -= (ll)cnt[c[i]] * (cnt[c[i]] - 1) / 2;
cnt[c[i]]++;
res += (ll)cnt[c[i]] * (cnt[c[i]] - 1) / 2;
};
auto sub = [&](int i){
res -= (ll)cnt[c[i]] * (cnt[c[i]] - 1) / 2;
cnt[c[i]]--;
res += (ll)cnt[c[i]] * (cnt[c[i]] - 1) / 2;
};
int L = 1, R = 0;
vector<string> ans(m);
for(int i = 0; i < m; i++){
auto [l, r, idx] = Q[i];
while(L > l) add(--L);
while(R < r) add(++R);
while(L < l) sub(L++);
while(R > r) sub(R--);
ll X = (ll)(R - L + 1) * (R - L) / 2;
ll g = __gcd(X, res);
// cout << res / g << "/" << X / g << '\n';
if(g == 0) ans[idx] = "0/1";
else
ans[idx] = format("{}/{}",res/g,X/g);
}
for(int i = 0; i < m; i++)
cout << ans[i] << '\n';
}
P4462 [CQOI2018] 异或序列¶
这题没想出来, 还是挺有细节的。
查询的区间 \([l, r]\) 直接给处理成 $[l - 1, r] $, 查询区间 \([l, r]\) 的值其实是问是否存在 \([l-1, r]\) 之间的两个前缀使得 \(s_x \oplus s_y = k\) 。
加减也是需要注意一些东西。
struct node{
int l, r, idx;
};
node Q[100001];
int cnt[200002];
void ChatGptDeepSeek() // Date: 2025-04-26
{ // Time: 20:37:22
int n, m, k;
cin >> n >> m >> k;
int siz = sqrt(n);
vector<int> a(n + 1), pos(n + 1), pre(n + 1);
vl ans(m);
for(int i = 1; i <= n; i++){
cin >> a[i];
pre[i] = pre[i - 1] ^ a[i];
pos[i] = i / siz;
}
for(int i = 0; i < m; i++){
cin >> Q[i].l >> Q[i].r;
Q[i].idx = i;
}
sort(Q, Q + m, [&](node x, node y){
return (pos[x.l] == pos[y.l]) ? (x.r < y.r) : (pos[x.l] < pos[y.l]);
});
ll res = 0;
auto add = [&](int i){
res += cnt[pre[i] ^ k];
cnt[pre[i]]++;
};
auto sub = [&](int i){
cnt[pre[i]]--;
res -= cnt[pre[i] ^ k];
};
cnt[0] = 1;
int L = 0, R = 0;
for(int i = 0; i < m; i++){
auto [l, r, idx] = Q[i];
--l;
while(L > l) add(--L);
while(R < r) add(++R);
while(L < l) sub(L++);
while(R > r) sub(R--);
ans[idx] = res;
}
for(int i = 0; i < m; i++)
cout << ans[i] << '\n';
}
带修莫队¶
带修莫队目前只写了一题, 后面会更新。
如果是带修的莫队, 一般会先按 \(l\) 的块的顺序排, 再按 \(r\) 的顺序排, 再按 \(t\) 的大小来排, \(t\) 表示的是前面的修改的次数, 块的大小一般选择 \(\sqrt[3]{n^2}\) , 也就是 \(n^{\frac{2}{3}}\) , 即 pow(n, 0.67) 。
P1903 [国家集训队] 数颜色 / 维护队列¶
因为 \(t\) 的大小会不同, 我们还需要取消操作, 其实就是记一下进行每次修改时, 那个格子是什么颜色, 取消修改就是给它颜色还原回去嘛。
当时 color 那个数组大小我开错了, 找了半天, 还是让 deepseek 帮我看的。 我当时是把那个数组的大小开成了查询次数的。
struct node{
int l, r, t, idx;
};
node Q[133333];
int cnt[int(1e6)+1];
void ChatGptDeepSeek() // Date: 2025-04-26
{ // Time: 01:16:57
int n, m;
cin >> n >> m;
vi c(n + 1), pos(n + 1);
int siz = pow(n, 0.67) + 1;
for(int i = 1; i <= n; i++)
cin >> c[i], pos[i] = i / siz;
vector<pii> upd;
int tot = 0;
for(int i = 0, cnt = 0; i < m; i++){
char op;
int x, y;
cin >> op >> x >> y;
if(op == 'R') upd.push_back({x, y}), cnt++;
else Q[tot++] = {x, y, cnt, tot-1};
}
if(!tot) return;
sort(Q, Q + tot, [&](node x, node y){
if(pos[x.l] == pos[y.l]){
if(pos[x.r] == pos[y.r]) return x.t < y.t;
return pos[x.r] < pos[y.r];
}
return pos[x.l] < pos[y.l];
});
vi ans(tot), color(sz(upd));
int L = 1, R = 0, T = 0, res = 0;
auto add = [&](int i){
cnt[c[i]]++;
if(cnt[c[i]] == 1) res++;
};
auto sub = [&](int i){
cnt[c[i]]--;
if(cnt[c[i]] == 0) res--;
};
auto change = [&](int t){
auto [P, C] = upd[t];
color[t] = c[P];
if(P >= L && P <= R){
cnt[c[P]]--;
if(cnt[c[P]] == 0) res--;
cnt[C]++;
if(cnt[C] == 1) res++;
}
c[P] = C;
};
auto recover = [&](int t){
auto [P, C] = upd[t];
c[P] = color[t];
if(P >= L && P <= R){
cnt[C]--;
if(cnt[C] == 0) res--;
cnt[c[P]]++;
if(cnt[c[P]] == 1) res++;
}
};
for(int i = 0; i < tot; i++){
auto [l, r, t, idx] = Q[i];
while(L > l) add(--L);
while(R < r) add(++R);
while(L < l) sub(L++);
while(R > r) sub(R--);
while(T < t) change(T++);
while(T > t) recover(--T);
ans[idx] = res;
}
for(int i = 0; i < tot; i++)
cout << ans[i] << '\n';
}