计蒜客_项链:(主席树 | 离线 + 树状数组)

题目大意就跟洛谷的HH项链一样,但是求一串区间内不重复数字的和。

一种做法是离线 + 树状数组,这里不讲。

主要讲一下新学习的技能,用主席树搞区间内不重复的数字(个数或者和)。
由于洛谷那个写炸了,一直MLE(不知道被什么数据卡了),只好拿计蒜客上面的题来试一下,结果计蒜客的能过。

主席树做法和树状数组相同,对于同一个数字,我们只纪录最后一个出现的位置,如果前面出现了,我们先把他删掉,然后再在现在这个位置加上1。

为什么只记录最后一次出现的位置?因为当我们维护到 i 这个位置的时候,如果a[i] 在前面出现过,我们肯定只能保留一个,而对于当前我们维护的区间来说,越后面对答案贡献越大(这个地方有点类似单调队列,当数值相同的元素入队,把先入队的去掉,留下后入队的,因为后入队的更“新鲜”,对答案贡献更大)。
其实也很容易想的,对于离线的树状数组来说,前面的区间我们已经全部查询过,对于当前查询的区间,如果当前更新的位置的值已经出现过,它有可能出现在我们查询的区间 [l,r]的前面,导致我们查不到,因此离线情况下我们每次保留最后一个,使得它对查询的区间总是有贡献,并且把前面的删掉,使得答案不会重复。

而主席树,是有二维的,可以在线搞,我们建树的次序就是第一维,当我们建第i 棵树的时候表示我们考虑了前i个数字,而第二维不像区间第K大建的是权值线段树,这里仍然用区间作下标建线段树,也就是说两维都是区间下标。

每个位置的线段树维护的 是 区间[1,n]内所有结点的不同数字的个数,当我们建第 i 棵树的时候,如果a[i]没出现过,那么我们直接在线段树 更新所有包含这个位置的区间出现的数字的个数 (让它+1,表示又多了一个没出现过的数字),如果a[i] 在前面出现了,那么先把它上一次出现的位置删掉,再在这个新的位置维护加1。理由还是越靠右的对答案贡献越大(因为第i 棵树是用来查询 [l,i]的答案的,但第i 棵树继承了第i - 1 棵树,a[i]上一次出现的位置可能会在我们查询的区间前面,每次有重复出现就保留最后一个对答案贡献大,又重复讲了一遍。。)

关于查询,给出要查询的区间【l,r】,我们只需要查第r棵树,【l,n】区间内的所有答案就行了。这一点需要对主席树有一定理解。因为第r棵树是我们维护了前r个数字的树,但它包括了前l个数字,怎么办?前面讲到这里的主席树,建的线段树也是用区间建的。也就是我们可以在第r棵树内查询区间【l,r】,即是答案。
为了方便我们可以直接查询区间【l,n】,因为r后面的数字在第r棵树没有维护,【l,n】就相当于【l,r】。

而且维护过程保证了每棵树区间内每个数字都是唯一的,且对查询区间贡献最大(既然是用下标建的,为什么不直接在第n棵树查【l,r】?这是个弱智问题啊。。。如果直接在第n棵树查,那不就是线段树了吗?且出现重复的我们会保留后一个,因此有些有效答案会被抹去,线段树和树状数组都只能离线,主席树因为有2维可以在线)

那怎么维护区间内不同数字的和呢?(思考)
(一如既往的废话连篇的博客。。。主要是为了能看懂)

代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e4 + 1;
struct ss{
 	int ls,rs,val;
}tree[maxn * 50];
int n,m,x,last[maxn * 2],a[maxn],root[maxn],sz;
void init(){
 	tree[0] = {0,0,0};
 	root[0] = 0;
}
void update(int &rt,int k,int l,int r,int v){
 	tree[++sz] = tree[rt];
 	rt = sz;
 	if(l == r){
  		tree[rt].val += a[k] * v;
  		return;
 	}
 	int mid = l + r >> 1;
 	if(k > mid) update(tree[rt].rs,k,mid + 1,r,v);
 	else update(tree[rt].ls,k,l,mid,v);
	tree[rt].val = tree[tree[rt].ls].val + tree[tree[rt].rs].val;
}
int query(int rt,int x,int l,int r){
 	if(!rt) return 0;
 	if(l >= x) return tree[rt].val;
 	int mid = l + r >> 1;
 	if(mid >= x) return query(tree[rt].ls,x,l,mid) + tree[tree[rt].rs].val;
 	else return query(tree[rt].rs,x,mid + 1,r);
}
int main(){
 	scanf("%d",&n);
 	init();
 	memset(last,0,sizeof(last));
 	for(int i = 1; i <= n; i++)
 		scanf("%d",&a[i]);
 	for(int i = 1; i <= n; i++){
  		root[i] = root[i - 1];
  		if(last[a[i]])
   		update(root[i],last[a[i]],1,n,-1);
  		update(root[i],i,1,n,1);
  		last[a[i]] = i;
 	}
 	scanf("%d",&m);
 	for(int i = 1; i <= m; i++){
  		int l,r;
  		scanf("%d%d",&l,&r);
  		printf("%d\n",query(root[r],l,1,n));
 	}
 	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值