P1972 [SDOI2009] HH的项链

P1972 [SDOI2009] HH的项链

【解法一】 树状数组解法

本题核心:如何判断一个区间内的贝壳是否重复?

当右端点 \(r\) 固定时,不论 \(l\) 取何值,对于任意一组重复的贝壳,都可以只统计最右端的贝壳。

原因:设一组重复贝壳中最右端的贝壳所在的位置为 \(pos_r\),那么当 \(pos_r < l\) 时,其他贝壳也不可能算进统计中,当 $pos_r \ge l $时,无论其他贝壳是否被包括,对于区间的贡献都只有 \(1\),因此,只计算最右端的贝壳即可。

因此,只需要将所有询问区间按 \(r\) 从小到大排序,计算答案即可。

【树状数组作用】

以位置为下标,每遇到一个新的数 \(num(num \le r)\),判断它是否重复,如果重复,那么将上一个相同的数的位置贡献值 \(-1\),将当前数的位置的贡献值 \(+1\)

对于一段区间 \([l,r]\),答案为 \(sum(r)-sum(l-1)\)

【code】

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int n,m,ask_r,prev,pos;
int vis[1000005],a[1000005],t[1000005],ans[1000005];
struct A{
	int l,r,num;
}ask[1000005];
bool cmp(A x,A y){
	return x.r<y.r;
}
int find(int pos){
	ask_r=ask[pos++].r;
	while(ask_r==ask[pos].r) pos++;
	return pos-1;
}
void add(int x,int y){
	for(;x<=n;x+=(x&-x)) t[x]+=y;
	return;
} 
int sum(int x){
	int su=0;
	for(;x;x-=(x&-x)) su+=t[x];
	return su;
}
void replace(){
	for(int i=ask[prev].r+1;i<=ask_r;i++){
		if(vis[a[i]]!=0) add(vis[a[i]],-1);
		add(i,1);
		vis[a[i]]=i;
	}
	for(int i=prev+1;i<=pos;i++) ans[ask[i].num]=sum(ask[i].r)-sum(ask[i].l-1);
	return;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	scanf("%d",&m);
	for(int i=1;i<=m;i++) scanf("%d%d",&ask[i].l,&ask[i].r),ask[i].num=i;
	sort(ask+1,ask+m+1,cmp);
	while(1){
		if(pos==m) break;
		prev=pos;
		pos=find(pos+1);
		replace();
	}
	for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
	return 0;
}

\(summary\)

此题不再以数据范围为下标,而是以位置为下标。对于树状数组的应用更加灵活。在想到以最右端的贝壳为有价值的贡献时,对应到树状数组的操作就可以是上一个重复的数的位置的贡献值 \(-1\),当前数的位置的贡献值 \(+1\)。然后用前缀和统计区间内的个数。算进一步的开阔思维。

【解法二】 可持久化线段树

可持久化线段树简介(主席树)

可持久化线段树本质为记录每个时段线段树的状态。

P3834 【模板】可持久化线段树 2

思想剖析

如题,给定 \(n\) 个整数构成的序列 \(a\),将对于指定的闭区间 \([l, r]\) 查询其区间内的第 \(k\) 小值。

以此题为例,线段树以元素种类从小到大排序作为叶子结点,阶段划分为 \([1 \sim i] (1 \le i \le n)\)

线段树结点权值表示为线段树结点表示的区间(这里的区间指的是按从小到大排序的元素种类作为叶子结点,往上构建的区间,而不是阶段表示的区间)所包含的元素个数。(再形象一点,可以在这边类比在桶排序数组上建立一个线段树,其功能为统计区间和。)

类比前缀和的思想,可以用线段树 \([1,r]\) 与线段树 \([1,l-1]\) 对应的结点相减,得出区间 \([l,r]\) 之间的元素所对应得线段树。(当然,在实际操作中不需要将整颗树都算出来,计算相关区间即可)。

由于我们已经按照元素种类将其排序好,因此只需要在得到的区间线段树中查询第 \(k\) 个元素即可。(这里线段树的优势在于尽管它空间大,但是建树和查询操作都是 \(logn\) 的。个人感觉可以当成记录所有阶段的桶排数组,然后从头到尾查询第 \(k\) 个元素,线段树作为一个工具优化时间,负优化空间。)

操作补充

  • 由于要开很多颗线段树,因此在存的时候采用动态开点存线段树。建立一个结构体 (\(sum\)权值,\(l\)左子区间对应的序号,\(r\)右子区间对应的序号),存的时候类似于链表,记录树根就可以遍历整棵树。再具体一点就是 \(lxj\) 讲过,大家意会就好,看到代码就懂了。

  • 我们在建立新一颗线段树时,只需要把大部分重复的节点指向上一颗对应节点,需要更新的数据新建节点储存。

【code】

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#define max_num 200005
using namespace std;
int n,m,a[max_num],rec[max_num],cnt,size;
int root[max_num];
struct node{
	int sum,l,r;
	#define sum(x) t[x].sum
	#define l(x) t[x].l
	#define r(x) t[x].r
}t[max_num<<5];
int find(int x){
	int l=1,r=size;
	while(l<=r){
		int mid=(l+r)/2;
		if(rec[mid]==x) return mid;
		if(x<rec[mid]) r=mid-1;
		else l=mid+1;
	}
}
int update(int pre,int pl,int pr,int x){
	//pre表示该节点在前一颗线段树中对应的节点 
	int rt=++cnt;
	l(rt)=l(pre);
	r(rt)=r(pre);
	sum(rt)=sum(pre)+1;
	if(pl<pr){
		int mid=(pl+pr)/2;
		if(x<=mid) l(rt)=update(l(pre),pl,mid,x);
	 	else r(rt)=update(r(pre),mid+1,pr,x);
	}
	return rt;
}
int query(int l,int r,int pl,int pr,int k){
	// l,r 表示此时对应节点的序号 
	if(pl==pr) return pl;
	int x=sum(l(r))-sum(l(l));//计算左子树的元素个数。
	int mid=(pl+pr)/2;
	if(k<=x) return query(l(l),l(r),pl,mid,k);
	else return query(r(l),r(r),mid+1,pr,k-x); 
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),rec[i]=a[i];
	sort(rec+1,rec+n+1);
	for(int i=1;i<=n;i++){
		if(rec[i]==rec[i+1]) continue;
		rec[++size]=rec[i];
	}
	for(int i=1;i<=n;i++){
		int pos=find(a[i]);
		root[i]=update(root[i-1],1,size,pos);
	}
	for(int i=1;i<=m;i++){
		int l,r,k;
		scanf("%d%d%d",&l,&r,&k);
		cout<<rec[query(root[l-1],root[r],1,size,k)]<<endl;
	}
	return 0;
}

回归正题

与树状数组结构类似,我们同样以位置作为叶子节点,阶段划分为 \([i \sim n] (1 \le i \le n)\) ,对于从端点 \(i\) 到结尾 \(n\) 这段区间,初始时每个位置对于区间不同种类的个数贡献都为 \(1\),如若有重复的元素,只统计最左端的元素的贡献。(至于为什么后面解释。)因此 \(i\) 需要从右往左也就是下标从大到小进行遍历,这样更新时只需要删去元素 \(a_i\)\(i\) 右边第一次出现的位置对此时区间的贡献,再加上位置 \(i\) 对区间的贡献即可。

在查询区间 \([l,r]\) 时,我们只需要在线段树 \([l,n]\) 上求 \([l,r]\) 的和即可。(由于在处理时相同元素只统计了最左端的,因此不会重复,为什么可以类比树状数组解法。)

补充一点:在删除重复元素时,不能直接遍历到节点就删,要判断一下是上一颗树的节点还是这一颗的,虽然查询起来是等效的,但是如若指向上一颗树的节点,删减操作相当于同时对上一颗树进行,不符合定义,会错。此时需要新建节点。

【code】

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,m,cnt,rec[1000005],ans_sum,root[1000005],a[1000005],rooot,pos;
struct node{
	int sum,l,r;
	#define sum(x) t[x].sum
	#define l(x) t[x].l
	#define r(x) t[x].r
}t[1000005<<5];
int update(int prev,int pl,int pr){
	int rt=++cnt;
	l(rt)=l(prev);
	r(rt)=r(prev);
	sum(rt)=sum(prev)+1;
	if(pl<pr){
		int mid=(pl+pr)/2;
		if(pos<=mid) l(rt)=update(l(prev),pl,mid);
		else r(rt)=update(r(prev),mid+1,pr);
	}
	return rt;
}
int change(int rt,int pl,int pr){
	if(rt<rooot){
		int tt=++cnt;
		sum(tt)=sum(rt)-1;
		l(tt)=l(rt);
		r(tt)=r(rt);
		if(pl==pr) return tt;
		int mid=(pl+pr)/2;
		if(pos<=mid) l(tt)=change(l(rt),pl,mid);
		else r(tt)=change(r(rt),mid+1,pr);
		return tt;		
	}	
	else{
		sum(rt)--;
		if(pl==pr) return rt;
		int mid=(pl+pr)/2;
		if(pos<=mid) l(rt)=change(l(rt),pl,mid);
		else r(rt)=change(r(rt),mid+1,pr);
		return rt;
	}
}
void ask(int rt,int l,int r,int pl,int pr){
	if(l<=pl&&r>=pr){	
		ans_sum+=sum(rt);
		return ;
	}
	int mid=(pl+pr)/2;
	if(l<=mid) ask(l(rt),l,r,pl,mid);
	if(r>mid) ask(r(rt),l,r,mid+1,pr);
}
int main(){
	int l,r;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=n;i>=1;i--){
		pos=i;
		rooot=root[i]=update(root[i+1],1,n);
		
		if(rec[a[i]]>0)	pos=rec[a[i]],change(root[i],1,n);
		rec[a[i]]=i;
	}
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&l,&r);
		ans_sum=0;
		ask(root[l],l,r,1,n);
		printf("%d\n",ans_sum);
	}
	return 0;
}

最后的最后

在书上有看到莫队解法的,但由于现在数据超强且还在开线段树,所以先留个坑。以后开莫队的时候再回来写个部分分。

update P2184 贪婪大陆

同是求区间内不同种类,这一题与前一题不同,这一题给出的并不是一串,而是同一种类一个区间一个区间的给,但求得同样都是某一区间内贝壳的种类数,因此这里拿出来一起讲。

【思路分析】

容易发现对于给定的区间,求与其有交集的区间的个数。将 \(L\) 记为区间头,\(R\) 记为区间尾,\(L\) 一定在给定区间的 \(R\) (包含 \(R\))的前面,但不被给定区间包含的区间,\(R\) ,一定在给定区间的 \(L\) (不包含 \(L\))的前面。

因此只需要用两个树状数组统计头尾,用 \(R\) 以前所有的头减去 \(L\) 以前所有的尾即可。

【code】

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,m,q;
int h[100005],t[100005];
void add(int *a,int x){
	for(;x<=n;x+=(x&-x)) a[x]+=1;
	return;
}
int sum(int *a,int x){
	int cnt=0;
	for(;x;x-=(x&-x)) cnt+=a[x];
	return cnt;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d",&q);
		int l,r;
		if(q==1){
			scanf("%d%d",&l,&r);
			add(h,l);
			add(t,r);
		}
		else{
			scanf("%d%d",&l,&r);
			cout<<sum(h,r)-sum(t,l-1)<<endl;
		}
	}
	return 0;
}
posted @ 2023-04-08 22:00  k_stefani  阅读(63)  评论(0编辑  收藏  举报