HH的项链 (三种解法) (离线查询 + 线段树/树状数组 or 主席树)

18 篇文章 1 订阅
13 篇文章 1 订阅

题目链接: 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,l1]位置的情况, 如果某个位置 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,l1], 用 r o o t [ r ] root[r] root[r]的信息减去 r o o t [ l − 1 ] root[l - 1] root[l1]的信息, 即可得到 [ 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;
}

END

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逍遥Fau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值