朴素莫队

例题:HH的项链

题目描述

$HH$有一串由各种漂亮的贝壳组成的项链。$HH$ 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。$HH$ 不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

输入格式

一行一个正整数 $n$,表示项链长度。
第二行 $n$ 个正整数 $a_i$,表示项链中第 $i$ 个贝壳的种类。

第三行一个整数 $m$,表示 $HH$ 询问的个数。
接下来 $m$ 行,每行两个整数 $l,r$,表示询问的区间。

输出格式

输出 $m$ 行,每行一个整数,依次表示询问对应的答案。

样例输入 #1

1
2
3
4
5
6
6
1 2 3 4 3 5
3
1 2
3 5
2 6

样例输出 #1

1
2
3
2
2
4

提示

【数据范围】

对于 $20%$ 的数据,$1\le n,m\leq 5000$;
对于 $40%$ 的数据,$1\le n,m\leq 10^5$;
对于 $60%$ 的数据,$1\le n,m\leq 5\times 10^5$;
对于 $100%$ 的数据,$1\le n,m,a_i \leq 10^6$,$1\le l \le r \le n$。

本题可能需要较快的读入方式,最大数据点读入数据约 20MB


一、暴力算法

直接暴力枚举询问,然后再枚举区间,这样是$O(n^2)$的

优化

如果说询问是按照左端点递增和右端点递增的,
那么就可以离线排序,用线性的时间扫过去所有询问,用哈希表记录一下就行,同时记录答案。
但是两个端点不能同时递增的,最多保证一个递增。


二、莫队算法

把询问的左端点按照分块的思想分成$\sqrt{n}$块,然后每一块里面的询问$[l,r]$按照右端点递增排序。

在块中:询问区间的右端点是递增的,但是区间的左端点不是递增的;

在块间:左端点是递增的,而右端点不是递增的**(这样就需要指针的移动和回滚,才有了下面的奇偶优化)**。

定义两个指针$tt$(向$r$靠齐),$hh$(向$l$靠齐),
然后开始遍历排好序的询问,枚举每个询问$[l,r]$,移动$hh$,$tt$,分别向$l$,$r$靠齐,
同时用一个哈希表记录一下数字出现的个数,同时更新答案即可。

时间复杂度分析

原序列被分成$\sqrt{n}$块,每块长度是$\sqrt{n}$,每块中的询问按照询问右端点递增排序。

那么:

$tt$(向$r$靠齐)指针在每一块中,最坏可能移动$n$次,有$\sqrt{n}$块,所以是$O(n\sqrt{n})$。

$hh$(向$l$靠齐)指针如果在块中移动,最多移动$\sqrt{n}$次。在块间,则最多移动2$\sqrt{n}$次(从这一块的头移动到下一块的尾)。

最终,双指针时间复杂度取最大值,即为$O(n\sqrt{n})$。

奇偶优化

由于每一块中右端点是按从小到大排序的,所以每一块在滚$i$指针时。
滚到最后大概率会滚到整个序列的末尾,然后处理下一块时,右端点$r$又是从小到大排的。
所以$i$得从后面滚回来前面这个$r$,这样就浪费了时间。

如果按奇数块右端点递增排序,偶数块右端点递减排序。
那么指针$i$滚到末尾,然后下一块又是递减排序的话,则可以直接开始滚了,不用滚回来再从新开始。


三、完整代码

时间复杂度:$O(M\sqrt{N})$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <cmath>

using namespace std;

const int N = 1000010, M = 1000010, K = 1000010;

struct query {
int id, l, r;
} q[M];

int n, m;
int a[N];
int cnt[K], len;
int res[M];

int get(int x)
{
return x / len;
}

void add(int x, int& ans)
{
if(cnt[x] == 0) ans ++;
cnt[x] ++;
}

void del(int x, int& ans)
{
cnt[x] --;
if(cnt[x] == 0) ans --;
}

int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
scanf("%d", &m);
for(int i = 0; i < m; i ++)
{
int l, r;
scanf("%d%d", &l, &r);
q[i] = {i, l, r};
}

len = sqrt(n);

sort(q, q + m, [&](const query& x, const query& y) {
int i = get(x.l), j = get(y.l);
if(i == j)
{
// 奇升偶降优化
if(i & 1) return x.r < y.r;
else return y.r < x.r;
}
return i < j;
});

int sum = 0;
// tt为双指针的右指针, hh为双指针的左指针
for(int k = 0, tt = 0, hh = 1; k < m; k ++)
{
int id = q[k].id, l = q[k].l, r = q[k].r;
while(tt < r) add(a[++ tt], sum);
while(tt > r) del(a[tt --], sum);
while(hh < l) del(a[hh ++], sum);
while(hh > l) add(a[-- hh], sum);
res[id] = sum;
}

for(int i = 0; i < m; i ++) printf("%d\n", res[i]);
return 0;
}

四、树状数组(正解)

时间复杂度:$O(MlogN)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>

using namespace std;

const int N = 1000010, M = 1000010, K = 1000010;

int tr[N];
int n, m;
int a[N], last[K];
int res[M];

struct Query {
int id, l, r;
bool operator< (const Query &x) { return r < x.r; }
} q[M];

int lowbit(int x)
{
return x & -x;
}

void add(int x, int val)
{
for(int i = x; i <= n; i += lowbit(i)) tr[i] += val;
}

int query(int x)
{
int res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}

int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
scanf("%d", &m);
for(int i = 0; i < m; i ++)
{
int l, r;
scanf("%d%d", &l, &r);
q[i] = {i, l, r};
}
sort(q, q + m);
for(int i = 1, j = 0; i <= n; i ++)
{
if(last[a[i]]) add(last[a[i]], -1);
add(i, 1);
while(j < m && q[j].r == i)
{
int id = q[j].id, l = q[j].l;
res[id] = query(i) - query(l - 1);
j ++;
}
last[a[i]] = i;
}
for(int i = 0; i < m; i ++) printf("%d\n", res[i]);
return 0;
}