题目链接: HH的项链
2021.7.13 更新了第三种主席树做法
2021.12.2 翻新了博客
大致题意
给定长度为 n n n的序列, 有 m m m次询问, 每次询问 [ l , r ] [l, r] [l,r]中不同数字的个数.
解题思路
离线查询 + 线段树/ 树状数组 or 主席树
解法一: 主席树维护区间做法
主席树的思路其实比较好想. 这个题的问题本质是, 询问每段区间, 看看有多少个不重复的数字. 那么我们按照每一个位置, 都去建立一棵线段树, 树中维护 [ 1 , i n d e x ] [1, index] [1,index]位置有多少个不重复元素.
当加到第
i
n
d
e
x
index
index个位置时, 有两种情况: ①该数字在之前没有出现过 ②该数字在之前出现在
p
o
s
pos
pos位置过
(
p
o
s
<
i
n
d
e
x
)
(pos < index)
(pos<index). (关于位置
p
o
s
pos
pos的维护, 我们可以采用哈希表)
对于情况①, 我们正常统计即可. 情况②, 我们同样要更新当前点的情况, 但我们也要把前面点的贡献删除掉, 相当于在 i n d e x index index位置 + 1 +1 +1, p o s pos pos位置 − 1 -1 −1. 那么为什么我们要这样去做呢?
考虑到已经是第 i n d e x index index版本的线段树, 我们需要处理的询问区间是 [ l , i n d e x ] [l, index] [l,index], 很显然, 第 p o s pos pos位置出现的编号只能保证在 [ 1 , p o s ] [1, pos] [1,pos]区间产生贡献, 而 [ p o s + 1 , i n d e x ] [pos + 1, index] [pos+1,index]区间是无法产生贡献的, 因此会导致这个区间的贡献少 1 1 1. 而在 i n d e x index index位置出现的编号一定能够保证在查询区间产生贡献. 因此 i n d e x index index位置上的贡献是优于 p o s pos pos位置的, 又考虑到防止 [ 1 , p o s ] [1, pos] [1,pos]区间重复计算两次贡献, 所以要在 p o s pos pos位置 − 1 -1 −1, i n d e x index index位置 + 1 +1 +1.
这样我们查询 [ l , r ] [l, r] [l,r]区间时, 我们在 第 r r r棵线段树中直接查询 [ l , r ] [l, r] [l,r]即可. 考虑到优化空间, 于是采用主席树.
解法二: 离线查询 + 线段树(树状数组)
这个思路的做法核心是离线查询的思维, 线段树和树状数组只是为了维护区间贡献.
我们考虑将所有的查询存储下来, 按照右端点
r
r
r进行小到大排序.
在处理询问时, 我们假设一个pos变量, 表示当前树中已经维护了前
p
o
s
pos
pos个位置的情况, 每次我们都让
p
o
s
pos
pos维护到当前查询的右边界
r
r
r, 这样就保证了树中维护的区间是
[
1
,
r
]
[1, r]
[1,r].
同主席树的思路, 当我们添加到第 p o s pos pos个位置时, 也是有两种情况. 处理思路也是相同的.
对于询问 [ l , r ] [l, r] [l,r], 我们直接在树中查询 [ l , r ] [l, r] [l,r]区间即可.
解法三: 主席树维护权值做法
这种做法和前两种做法就有一些区别了. 我们在树内维护当前位置颜色的前一次出现的位置.
当这种颜色没出现时, 不妨认为它在
0
0
0位置出现了, 这样树中维护权值区间
[
0
,
n
]
[0, n]
[0,n]即可.
考虑到第 i n d e x index index版本的线段树 r o o t [ i n d e x ] root[index] root[index]. 如果某个叶子结点 p o s pos pos位置为 1 1 1, 表示序列区间 [ p o s + 1 , i n d e x ] [pos + 1, index] [pos+1,index]中, 存在一个位置 x x x, 使得 c o l [ x ] = c o l [ p o s ] col[x] = col[pos] col[x]=col[pos].
对于询问区间 [ l , r ] [l, r] [l,r], 我们统计 [ 0 , l − 1 ] [0, l - 1] [0,l−1]位置的情况, 如果某个位置 p o s pos pos的值为 1 1 1, 表示 p o s pos pos位置后, 存在一个 x x x位置满足 c o l [ x ] = = c o l [ p o s ] col[x] == col[pos] col[x]==col[pos], 如果 x x x位于 [ l , r ] [l, r] [l,r]区间, 则 x x x会对答案产生贡献. 因此我们只需要找到所有位于 [ l , r ] [l, r] [l,r]的 x x x即可.
我们查询区间 [ 0 , l − 1 ] [0, l - 1] [0,l−1], 用 r o o t [ r ] root[r] root[r]的信息减去 r o o t [ l − 1 ] root[l - 1] root[l−1]的信息, 即可得到 [ l , r ] [l, r] [l,r]区间中产生贡献的位置数量.
我们发现这种做法的主席树会比解法一省了 O ( n l o g n ) O(nlogn) O(nlogn)的空间.
解法四: 普通莫队
这不是模版题吗?
AC代码
主席树维护区间
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 5E4 + 10;
struct node {
int l, r;
int val;
}t[N << 5]; //这里要开 n * 2logn 空间
int root[N], ind;
int build(int a, int c, int tl, int tr, int p) {
int x = ++ind; t[x] = t[p];
t[x].val += c;
if (tl == tr) return x;
int mid = tl + tr >> 1;
if (a <= mid) t[x].l = build(a, c, tl, mid, t[p].l);
else t[x].r = build(a, c, mid + 1, tr, t[p].r);
return x;
}
int ask(int l, int r, int tl, int tr, int x) {
if (l <= tl and r >= tr) return t[x].val;
int mid = tl + tr >> 1;
int res = 0;
if (l <= mid) res = ask(l, r, tl, mid, t[x].l);
if (r > mid) res += ask(l, r, mid + 1, tr, t[x].r);
return res;
}
int main()
{
unordered_map<int, int> mp;
int n; cin >> n;
rep(i, n) {
int x; scanf("%d", &x);
root[i] = build(i, 1, 1, n, root[i - 1]);
if (mp.count(x)) root[i] = build(mp[x], -1, 1, n, root[i]);
mp[x] = i;
}
int m; cin >> m;
rep(i, m) {
int l, r; scanf("%d %d", &l, &r);
// 查询[l, n] 和 [l, r]是一样的, 因为第r棵线段树没有[r + 1, n]的信息
printf("%d\n", ask(l, n, 1, n, root[r]));
}
return 0;
}
离线查询 + 树状数组
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 5E4 + 10, M = N << 2;
int w[N], res[M];
struct operation {
int l, r, id;
bool operator< (const operation& t) const { return r < t.r; }
}; vector<operation> area;
int t[N];
int lowbit(int x) { return x & -x; }
void add(int x, int c) { for (int i = x; i < N; i += lowbit(i)) t[i] += c; }
int ask(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += t[i];
return res;
}
int ask(int l, int r) { return ask(r) - ask(l - 1); }
int main()
{
int n; cin >> n;
rep(i, n) scanf("%d", &w[i]);
int m; cin >> m;
rep(i, m) {
int l, r; scanf("%d %d", &l, &r);
area.push_back({ l, r, i });
}
sort(area.begin(), area.end());
unordered_map<int, int> mp;
int pos = 0;
for (auto& op : area) {
int l = op.l, r = op.r, id = op.id;
while (pos + 1 <= r) {
pos++;
add(pos, 1);
if (mp[w[pos]]) add(mp[w[pos]], -1);
mp[w[pos]] = pos;
}
res[id] = ask(l, r);
}
rep(i, m) printf("%d\n", res[i]);
return 0;
}
主席树维护权值
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 5E4 + 10, QAQ = 1E6 + 10, L = 0, R = N - 5;
int last[QAQ];
struct node {
int l, r;
int val;
}t[N * 17];
int root[N], ind;
int build(int a, int c, int tl, int tr, int p) {
int x = ++ind; t[x] = t[p];
t[x].val += c;
if (tl == tr) return x;
int mid = tl + tr >> 1;
if (a <= mid) t[x].l = build(a, c, tl, mid, t[p].l);
else t[x].r = build(a, c, mid + 1, tr, t[p].r);
return x;
}
int ask(int l, int r, int tl, int tr, int p, int x) {
if (l <= tl and r >= tr) return t[x].val - t[p].val;
int mid = tl + tr >> 1;
int res = 0;
if (l <= mid) res += ask(l, r, tl, mid, t[p].l, t[x].l);
if (r > mid) res += ask(l, r, mid + 1, tr, t[p].r, t[x].r);
return res;
}
int main()
{
int n; cin >> n;
rep(i, n) {
int col; scanf("%d", &col);
root[i] = build(last[col], 1, L, R, root[i - 1]);
last[col] = i;
}
int m; cin >> m;
rep(i, m) {
int l, r; scanf("%d %d", &l, &r);
printf("%d\n", ask(0, l - 1, L, R, root[l - 1], root[r]));
}
return 0;
}