刷题记录:牛客NC20545[HEOI2012]采花

文章讨论了一种求区间内特定条件的花的种类数的问题,主要涉及数据结构主席树和离线树状数组的使用。由于数据规模和空间限制,主席树在某些测试用例中无法通过,而离线树状数组则提供了解决方案。文章详细解释了两种方法的思路和实现,并提供了代码示例。
摘要由CSDN通过智能技术生成

传送门:牛客

题目描述:

题目较长,此处暂略
输入:
5 3 5
1 2 2 3 1
1 5
1 2
2 2
2 3
3 5
输出:
2
0
0
1
0

总结一下题意,就是求区间 [ l , r ] [l,r] [l,r]出现次数大于1的花的种类数.

考虑使用主席树或者离线树状数组的方法来解决.由于数据加强的原因,导致主席树在本题中是不能完美通过的,洛谷上可以得133分(T了两个点),在牛客上可以过一般数据(MLE+T).因为牛客上的空间甚至只有可怜的256MB,导致我们的主席树在牛客上被卡的死死的.虽然但是,主席树的解法还是可以了解一下的.

首先是主席树解法:

考虑用 l a s t 1 [ i ] last1[i] last1[i]来记录每一个位置的花上一次出现的位置,那么对于本题来说,我们需要记录区间内出现次数大于1,这就意味着花上一次出现的位置要在我们的区间 [ l , r ] [l,r] [l,r]里面就可以了.所以此时需要计算的是就是: ∑ l r ( l a s t 1 [ i ] > = l ) \sum_{l}^r{(last1[i]>=l)} lr(last1[i]>=l)为主席树可以解决的经典题目之一.
当然此时就有人要有疑问了,上面的解法显然是有一个问题的,就是我们区间里的花的种类是可能重复的,也就是当我们区间里有三朵同样的花时,此时我们直接使用上述 l a s t 1 last1 last1会发现会被重复计算一次.那我们要怎么解决这个问题呢,我们可以在第三朵相同的花出现的时候将前一朵花的贡献删除掉.也就是意味着当我们的第三朵花在区间里面时,前面的花的贡献因为重复了所以可以直接删除掉.
因为对于我们的主席树来说,我们最终的答案是有首尾两个相减得到的,所以我们只在末尾出更改只影响到了包括这三朵花的区间,而对于这样的区间来说,此时显然我们只需要第三朵花的贡献即可.对于不完全包括这三朵花的区间,我们发现要么此前的花本身就没有贡献,删掉也无所谓;要么三朵花都没有贡献,所以删掉不影响这些区间,所以这么做就巧妙的解决了这个问题.

在搞清楚上述关键点之后,直接使用主席树来解决即可:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define root 1,n,1
#define ls rt<<1
#define rs rt<<1|1
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {
	ll x=0,w=1;char ch=getchar();
	for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;
	for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
	return x*w;
}
#define maxn 2000100
const double eps=1e-8;
#define	int_INF 0x3f3f3f3f
#define ll_INF 0x3f3f3f3f3f3f3f3f
struct PerSegment_tree{
	int sum,lnum,rnum;
}tree[maxn<<5];int cnt=0;int RT[maxn];
int build(int l,int r) {
	int p=++cnt;tree[p].sum=0;
	if(l==r) {
		return p;
	}
	int mid=(l+r)>>1;
	tree[p].lnum=build(l,mid);
	tree[p].rnum=build(mid+1,r);
	return p;
}
int update(int per,int l,int r,int pos,int val) {
	if(pos==0) return per;
	int p=++cnt;
	tree[p]=tree[per];tree[p].sum+=val;
	if(l==r) return p;
	int mid=(l+r)>>1;
	if(pos<=mid) tree[p].lnum=update(tree[per].lnum,l,mid,pos,val);
	else tree[p].rnum=update(tree[per].rnum,mid+1,r,pos,val);
	return p;
}
int query(int per,int now,int l,int r,int k) {
	if(l==r) return tree[now].sum-tree[per].sum;
	int mid=(l+r)>>1;
	if(k>mid) {
		return query(tree[per].rnum,tree[now].rnum,mid+1,r,k);
	}
	else {
		int s=tree[tree[now].rnum].sum-tree[tree[per].rnum].sum;
		return s+query(tree[per].lnum,tree[now].lnum,l,mid,k); 
	}
}
int a[maxn];
int n,c,m;int last1[maxn],last2[maxn];
int main() {
	n=read();c=read();m=read();
	RT[0]=build(1,c);
	for(int i=1;i<=n;i++) {
		a[i]=read();
		RT[i]=update(RT[i-1],1,c,last1[a[i]],-1);
		RT[i]=update(RT[i],1,c,last2[a[i]],1);
		last1[a[i]]=last2[a[i]];
		last2[a[i]]=i;
	}
	for(int i=1;i<=m;i++) {
		int l=read(),r=read();
		printf("%d\n",query(RT[l-1],RT[r],1,c,l));
	}
	return 0;
}

方法二:离线树状数组

我们可以将所有的询问区间进行一个排序(按区间右端点从小到大排).那么此时我们的关注点就是所有小于当前区间的右端点的花了.

对于所有小于当前区间右端点的花来说,假设此时我们先后出现了两朵颜色相同的花,此时我们需要将第一朵加入我们的贡献.因为只有当我们的两朵都在区间内的时候,我们此时才有贡献,所以算倒数第二朵的.假设我们此时先后出现了三朵颜色相同的花,注意,此时无论我们的左端点时如何,显然我们应该删除第一朵花原本的贡献,因为无论我们的左端点所在的情况如何,越靠近右端点的花显然是越有可能贡献的(这个可以仔细理解一下).

并且因为我们保证了越靠近后面的花的贡献,所以此时我们的询问区间从小到大排的正确性也就是可以保证了.

讲一讲离线做法的精华所在:直接做这道题是不好做的,但是假设此时我们改变一下此题,假设我们此时的问题是计算区间 [ l , n ] [l,n] [l,n]的花的贡献,我们是不是就可以使用上面的方法来做了,但是此时我们一旦使用了上面的方法,那么对于 n n n之前的所有区间就会产生影响.因为我们删除了前面的花的贡献.但是此时我们进行排序了,就保证后面区间都是在当前区间后面的,对于后面的区间来说,假设这区间的左端点在我们当前区间左端点的左边,那么没关系,因为当前区间左端点的贡献已经记录在区间里了.假设这区间的左端点在当前区间左端点右边,那么也没关系

然后使用树状数组解决即可:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define root 1,n,1
#define ls rt<<1
#define rs rt<<1|1
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {
	ll x=0,w=1;char ch=getchar();
	for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;
	for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
	return x*w;
}
#define maxn 2001000
const double eps=1e-8;
#define	int_INF 0x3f3f3f3f
#define ll_INF 0x3f3f3f3f3f3f3f3f
inline int lowbit(int x) {
	return x&(~x+1);
}
int tree[maxn];int n,m,c;int a[maxn];
void Add(int pos,int val) {
	while(pos<=n) {
		tree[pos]+=val;
		pos+=lowbit(pos);
	}
}
int query(int pos) {
	int ans=0;
	while(pos) {
		ans+=tree[pos];
		pos-=lowbit(pos);
	}
	return ans;
}
struct Ask{
	int l,r,id;
}ask[maxn];
bool cmp(Ask aa,Ask bb) {
	return aa.r<bb.r;
}
int last1[maxn];int last2[maxn];int ans[maxn];
int main() {
	n=read();c=read();m=read();
	for(int i=1;i<=n;i++) {
		a[i]=read();
	}
	for(int i=1;i<=m;i++) {
		ask[i].l=read();ask[i].r=read();ask[i].id=i;
	}
	sort(ask+1,ask+m+1,cmp);
	int pos=1;
	for(int i=1;i<=m;i++) {
		while(pos<=n&&pos<=ask[i].r) {
			if(last1[a[pos]]==0) {
				last1[a[pos]]=pos;
			}
			else {
				if(last2[a[pos]]==0) {
					Add(last1[a[pos]],1);
					last2[a[pos]]=pos;
				}
				else {
					Add(last1[a[pos]],-1);
					Add(last2[a[pos]],1);
					last1[a[pos]]=last2[a[pos]];
					last2[a[pos]]=pos;
				}
			}
			pos++;
		}
		ans[ask[i].id]=query(ask[i].r)-query(ask[i].l-1);
	}
	for(int i=1;i<=m;i++) {
		printf("%d\n",ans[i]);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值