浅谈莫队1
莫队
基础莫队
[SDOI2009] HH的项链
这道题是卡莫队的,但是确实练习莫队的好题。
首先想一下暴力:
直接暴力枚举询问,然后再枚举区间,这样是 \(O(n^2)\) 的;
想一下优化:
如果说询问是按照 左端点递增 && 右端点递增 的;
那么我们就可以离线排序,用线性的时间扫过去所有询问,用桶记录一下就行,同时记录答案;
但是可能没有这种好情况...可能是两个端点不能同时递增的,最多保证一个递增。
那么在这种情况下要怎么优化呢?
我们可以折中一下:(莫队思想:离线 + 分块 + 双指针)
把询问的左端点按照分块的思想分成 \(sqrt(n)\) 块,然后每一块里面的询问 \([l,r]\) 按照右端点递增排序
在块中:我们询问区间右端点是递增的,但是我们区间左端点不是递增的
在块间:则相反,左端点是递增的,而右端点不是递增的
然后我们定义两个指针 \(i\) (向 \(r\) 靠齐),\(j\) (向 \(l\) 靠齐)
然后开始遍历排好序的询问,每个询问 \([l,r]\) 我们的 \(i,j\) 就移动,分别向 \(l,r\) 靠齐;
同时用一个桶记录一下数字出现的个数,并更新答案就行
#include <bits/stdc++.h>
#define int unsigned
#define rint register int
#define endl '\n'
using namespace std;
const int N = 1e6 + 5;
const int M = 3e6 + 5;
struct node
{
int id, l, r;
} q[N];
int n, m, len = 1;
int a[N], ans[M];
int cnt[M];
int get(int i){return i / len;}
bool cmp(node a, node b)
{
int l = get(a.l), r = get(b.l);
if (l != r) return l < r;
return a.r > b.r;
}
void add(int w, int &res)
{
if (++cnt[w] == 1) res++;
}
void del(int w, int &res)
{
if (--cnt[w] == 0) res--;
}
int read()
{
register int x = 0,f = 1;
register char ch;
ch = getchar();
while(ch > '9' || ch < '0'){if(ch == '-') f = -f;ch = getchar();}
while(ch <= '9' && ch >= '0'){x = x * 10 + ch - 48;ch = getchar();}
return x * f;
}
signed main()
{
n = read();
for (rint i = 1; i <= n; i++)
{
a[i] = read();
}
m = read();
unsigned lll = 1;
len = max(lll, (int)sqrt((double)n * n / m));
for (rint i = 1; i <= m; i++)
{
int l = read(), r = read();
q[i] = {i, l, r};
}
sort(q + 1, q + m + 1, cmp);
int res = 0;
for (rint k = 1, i = 1, j = 0; k <= m; k++)
{
int id = q[k].id, l = q[k].l, r = q[k].r;
while (j < r) add(a[++j], res);
while (j > r) del(a[j--], res);
while (i < l) del(a[i++], res);
while (i > l) add(a[--i], res);
ans[id] = res;
}
for (rint i = 1; i <= m; i++)
{
cout << ans[i] << endl;
}
return 0;
}
带修改的莫队
[国家集训队] 数颜色
由于增加修改操作,考虑给每一个修改操作按先后顺序编号
将一维改为二维,纵坐标为时间 $time$ ,横坐标为位置 $l,r$
当时间为 $k$ 时,表示已经经过了前 $k$ 个修改操作
若序列长度为 $n$ ,分块后每段长度为 $a$ ,共有 $\frac{n}{a}$ 块, $m$ 次询问, $t$ 次修改(根据数据范围,取合适的 $len$ )
$i$ 表示左指针, $j$ 表示右指针, $time$ 表示时间戳
排序标准为:
1. 比较左端点所在的块,从左到右排序
2. 若左端点所在块相同,则比较右端点所在块,从从左到右排序
3. 若右端点所在块也相同,则比较时间 $times$ ,从左到右排序
指针 $time$ 移动次数: $\frac{n^2t}{a^2}$
指针 $i$ 移动次数: $am+n$
指针 $j$ 移动次数: $am+\frac{n^2}{a}$
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e6 + 5;
const int M = 1e7 + 5;
int n, m;
int times, idx, len;
int a[N], ans[N], cnt[M];
struct node
{
int l, r, t, id;
} q[N];
struct Modify
{
int p, c;
} f[N];
int get(int i){return i / len;}
bool cmp(node a, node b)
{
int al = get(a.l), bl = get(b.l);
int ar = get(a.r), br = get(b.r);
if (al != bl) return al < bl;
if (ar != br) return ar < br;
return a.t < b.t;
}
void add(int w, int &res)
{
if (++cnt[w] == 1) res++;
}
void del(int w, int &res)
{
if (--cnt[w] == 0) res--;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
}
for (rint i = 1; i <= m; i++)
{
char op[2];
int a, b;
scanf("%s%lld%lld", op, &a, &b);
if (*op == 'Q') idx++, q[idx] = {a, b, times, idx};
else f[++times] = {a, b};
}
len = cbrt((double)n * max(times, 1ll)) + 1;
sort(q + 1, q + idx + 1, cmp);
for (int k = 1, i = 1, j = 0, t = 0, res = 0; k <= idx; k++)
{
int l = q[k].l, r = q[k].r, tim = q[k].t, id = q[k].id;
while (t < tim)
{
t++;
if (f[t].p >= i && f[t].p <= j)
{
add(f[t].c, res);
del(a[f[t].p], res);
}
swap(a[f[t].p], f[t].c);
}
while (t > tim)
{
if (f[t].p >= i && f[t].p <= j)
{
add(f[t].c, res);
del(a[f[t].p], res);
}
swap(a[f[t].p], f[t].c);
t--;
}
while (i < l) del(a[i++], res);
while (i > l) add(a[--i], res);
while (j < r) add(a[++j], res);
while (j > r) del(a[j--], res);
ans[id] = res;
}
for (rint i = 1; i <= idx; i++)
{
cout << ans[i] << endl;
}
return 0;
}
回滚莫队
AcWing 2523. 历史研究
回滚莫队又称不删除莫队
用于维护一些不删除属性的操作
例如最大值,加入一个数后只需取一次max,删除一个数却很难维护
设序列长度为$n$,每块长度为$a$,共有$\frac{n}{a}$块,$m$次询问
- 循环$1\leq i \leq \frac{n}{a}$,找到所有左端点在第$i$块的询问$l$,$r$
- 若$r$也在第$i$块,那么就暴力求,时间复杂度$O(am)$
- 否则,右端点用指针$j$维护,左端点每次回到第i块的右端,暴力求,时间复杂度$O(am+\frac{n^2}{a})$
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e6 + 5;
int n, m, len;
int w[N], cnt[N];
int ans[N];
vector<int> nums;
struct node
{
int id, l, r;
} q[N];
int get(int i){return i / len;}
bool cmp(node a, node b)
{
int l = get(a.l), r = get(b.l);
if (l != r) return l < r;
return a.r < b.r;
}
void add(int x, int &res)
{
cnt[x]++;
res = max(res, nums[x] * cnt[x]);
}
signed main()
{
cin >> n >> m;
len = sqrt(n);
for (rint i = 1; i <= n; i++)
{
cin >> w[i];
nums.push_back(w[i]);
}
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
for (rint i = 1; i <= n; i++)
{
w[i] = lower_bound(nums.begin(), nums.end(), w[i]) - nums.begin();
}
for (rint i = 0; i < m; i++)
{
int l, r;
cin >> l >> r;
q[i] = {i, l, r};
}
sort(q, q + m, cmp);
for (rint x = 0; x < m;)
{
int y = x;
while (y < m && get(q[x].l) == get(q[y].l)) y++;
int right = get(q[x].l) * len + len - 1;
while (x < y && q[x].r <= right)
{
int res = 0;
int id = q[x].id, l = q[x].l, r = q[x].r;
for (rint i = l; i <= r; i++) add(w[i], res);
ans[id] = res;
for (rint i = l; i <= r; i++) cnt[w[i]]--;
x++;
}
int res = 0;
int i = right + 1, j = right;
while (x < y)
{
int id = q[x].id, l = q[x].l, r = q[x].r;
while (j < r) add(w[++j], res);
int backup = res;
while (i > l) add(w[--i], res);
ans[id] = res;
while (i < right + 1) cnt[w[i++]]--;
res = backup;
x++;
}
memset(cnt, 0, sizeof cnt);
}
for (rint i = 0; i < m; i++)
{
cout << ans[i] << endl;
}
return 0;
}