普通莫队
【引入】
例题
题目大意:
给定一个长度为 \(n\) 的数组 \(arr\),一共有 \(q\) 条查询,格式如下:
查询 \(l, r :\) 打印 \(arr[l...r]\) 范围上有几种不同的数字.
解析:
把每个区间看作一个窗口,对于每个区间,维护一个数组 \(cnt\),以及一个变量 \(kind\)(数组内不同数字的个数),遍历到一个数字 \(i\) 那么cnt[i]++,当 cnt[i] = 1 时,kind++. 查询区间 \([l - 1, r]\) ,\([l + 1, r]\),\([l, r + 1]\),\([l, r - 1]\) 是非常方便的,只需要针对新加入或者新退出窗口的数对 \(cnt\) 数组进行维护,再根据它去更新 \(kind\) (如果 \(cnt[i] > 1\) 则不需要更新,\(cnt[i] = 1\) 则 kind++,\(cnt[i] = 0\) 则kind--),通过这种方法,我们可以得到每个区间的答案.但是不难发现,这种方法的时间复杂度达到了惊人的 \(O(mn)\) ,那么有没有什么方法能使复杂度降到 \(o(n \cdot \sqrt{n})\) 呢?因此我们引入“莫队”这个概念.
普通莫队是什么呢?就是对每个查询进行排序,达到降低时间复杂度的目的.那么具体怎么排序呢?
设数组 \(arr\) 的下标为 \(1\) 到 \(9\),下标 \(1\) 到 \(3\) 为块 \(1\),下标 \(4\) 到 \(6\) 为块 \(2\),下标 \(7\) 到 \(9\) 为块 \(3\).假设有以下几个查询:\((7,8),(5,5),(1,9),(4,9),(3,7),(2,3)\),设区间的左端点为 \(winl\),右端点为 \(winr\),排序策略主要有两点 :
- \(winl\) 所在的块号,从小到大.
- \(winr\) 的值从小到大.
排序后查询的顺序为:\((2,3),(3,7),(1,9),(5,5),(4,9),(7,8)\).
PS:\((2,3),(3,7),(1,9)\) 的左端点均属于块 \(1\),于是根据它们的右端点排序. \((5,5),(4,9)\) 的左端点均属于块 \(2\),也是根据它们的右端点排序,最后是 \((7,8)\),因为它左端点属于块 \(3\) 是最大的.
排序完之后,在根据一开始的方法进行窗口滑动即可,时间复杂度为 \(o(n \cdot \sqrt{n})\).
但是再举一个一个例子,设查询为:\((1,6),(3,9),(4,5),(6,9)\) 按照传统方式排序后,他的顺序应该是不变的,但是不难发现从 \((3,9)\) 到 \((4,5)\) 这个跨度是很大的,因此我们需要优化,可以使用“奇偶排序”.具体方式如下:
- \(winl\) 的块号,从小到大.
- 块号为奇,\(winr\) 从小到大.
- 块号为偶,\(winr\) 从大到小.
安排此方法排序后,查询顺序为:\((1,6),(3,9),(6,9),(4,5)\) 这样就能优化不少.
代码实现:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n, m;
int a[MAXN]; // 贝壳种类
int cnt[MAXN]; // 当前区间内各颜色出现次数
int ans[MAXN]; // 存储每个询问的答案
int curAns = 0; // 当前区间不同颜色个数
struct Query {
int l, r, id, block;
};
// 奇偶排序优化:块号不同按块号,块号相同按右端点升降交替
bool cmp(const Query &x, const Query &y) {
if (x.block != y.block) return x.block < y.block;
if (x.block & 1) return x.r > y.r; // 奇数块 r 降序
return x.r < y.r; // 偶数块 r 升序
}
inline void add(int pos) {
if (cnt[a[pos]] == 0) curAns++;
cnt[a[pos]]++;
}
inline void remove(int pos) {
cnt[a[pos]]--;
if (cnt[a[pos]] == 0) curAns--;
}
int main() {
// 快速输入输出
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
cin >> m;
vector<Query> qs(m);
// 块大小通常取 sqrt(n),也可根据 m 调整
int block = max(1, (int)sqrt(n));
for (int i = 0; i < m; i++) {
cin >> qs[i].l >> qs[i].r;
qs[i].id = i;
qs[i].block = qs[i].l / block;
}
sort(qs.begin(), qs.end(), cmp);
int curL = 1, curR = 0; // 初始空区间 [1, 0]
for (int i = 0; i < m; i++) {
int l = qs[i].l, r = qs[i].r;
// 扩展或收缩区间
while (curL > l) add(--curL);
while (curR < r) add(++curR);
while (curL < l) remove(curL++);
while (curR > r) remove(curR--);
ans[qs[i].id] = curAns;
}
for (int i = 0; i < m; i++) {
cout << ans[i] << '\n';
}
return 0;
}
PS:代码是AI生成的
浙公网安备 33010602011771号