离线树状数组&&线段树 (对pre数组的利用)

https://ac.nowcoder.com/acm/problem/20325

题目大意:给定数组a, 每次询问【l,r】中有多少种不同的数。

思路:预处理出来每个数的位置pos[a[i]], 和上一次a[i] 出现的位置pre[i],对于每次询问,将其离线,也就是将询问先都储存先来,按照右端点进行排序,从前往后遍历,如果这个数是第一次出现,就将其加入树状数组中,如果不是第一次出现(其pre[i]不为0),则将其前一次出现的位置删去。

code:

#include<bits/stdc++.h>

using namespace std;

const int N=2e6+10;

struct node{
	int l,r,id;
	bool operator< (const node &t) const
	{
		return r<t.r;
	}
}q[N];
int a[N],tr[N],ans[N];
int pre[N],pos[N]; 
int n,m;
int lowbit(int x)
{
	return x&-x;
}
int sum(int x)
{
	int ans=0;
	for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
	return ans;
}
void add(int x,int c)
{
	for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++) {
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		pre[i]=pos[a[i]];
		pos[a[i]]=i;
	}
	cin>>m;
	for(int i=1;i<=m;i++){
		int l,r;
		cin>>l>>r;
		q[i]={l,r,i};
	}
	sort(q+1,q+1+m);
	int t=1;
	for(int i=1;i<=m;i++){
		while(t<=q[i].r)
		{
            if(pre[t]) add(pre[t],-1);//下标要为1
			add(t,1);
			t++;
		}
		ans[q[i].id]=sum(q[i].r)-sum(q[i].l-1);
	}
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<endl;
	}
    
}
 

https://ac.nowcoder.com/acm/problem/20545

本题与上一题几乎相同,唯一变的是本题要求的是出现两次及以上的数的种数,与上题类似,关键是预处理出pre数组。

思路:要求至少出现两次的数的个数,即pre[i]不为0,因此对于每个数,如果它的pre不为0,我们就将其加入到树状数组中,题目要求的是出现的种类数,显然大于2次的没有意义,那么关键是如何把这部分的贡献去掉呢?这里的思想很像KMP中的next数组,对于a[i], 其前一次出现位置为pre, 则其前两次出现的位置为pre[pre], 因此我们可以对于每个a[i], 如果前面出现了两次,就将其前两次出现的位置的贡献减去。

code:

#include<bits/stdc++.h>

using namespace std;

const int N=4e6+10;

struct node{
	int l,r,id;
	bool operator< (const node &t) const
	{
		return r<t.r;
	}
}q[N];
int a[N],tr[N],ans[N];
int pre[N],pos[N]; 
int n,m,c;
int lowbit(int x)
{
	return x&-x;
}
int sum(int x)
{
	int ans=0;
	for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
	return ans;
}
void add(int x,int c)
{
	for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
signed main()
{
    
	//cin>>n>>c>>m;
    scanf("%d%d%d",&n,&c,&m);
	for(int i=1;i<=n;i++) {
		//cin>>a[i];
        scanf("%d",&a[i]);
	}
	for(int i=1;i<=n;i++){
		pre[i]=pos[a[i]];
		pos[a[i]]=i;
	}
	
	for(int i=1;i<=m;i++){
		int l,r;
		//cin>>l>>r;
        scanf("%d%d",&l,&r);
		q[i]={l,r,i};
	}
	sort(q+1,q+1+m);
	int t=1;
	for(int i=1;i<=m;i++){
		while(t<=q[i].r)
		{   
            if(pre[pre[t]]) add(pre[pre[t]],-1);//下标要为1
			if(pre[t]) add(pre[t],1);
			t++;
		}
		ans[q[i].id]=sum(q[i].r)-sum(q[i].l-1);
	}
	for(int i=1;i<=m;i++){
		//cout<<ans[i]<<endl;
        printf("%d\n",ans[i]);
	}
    return 0;
}

https://ac.nowcoder.com/acm/problem/14601

这个题目的想法肯定是对于每一个右端点 r,去找对应的左端点 l, 但是这个l是很难找的。

换个角度思考,第i个位置来一个颜色C[i]。

我们采用区间更新+区间查询求max的方法。

如果这个颜色是第一次出现,那么对于其前面的所有节点都累加 W[C[i]], 价值最大的节点即为我们要找的l。

如果这个颜色不是第一次出现,那么记录其前一次出现的位置pre[C[i]]和前前一次出现的位置pre2[C[i]],

他对[pre2[C[i]]+1, pre[C[i]]]的贡献为 -C[i], 因为这段区间之前加过了C[i],现在要减掉。

对 [pre[C[i]]+1, i] 的贡献为 C[i], 对这段区间进行更新即可。

最终只要对 1~i位置进行单点最大值查询即可找到 1~i 的最大贡献。

code:

#include<bits/stdc++.h>

using namespace std;
#define int long long
const int N=1e5+10;

struct node{
	int l,r,v,add;
}tr[N<<2];
int c[N],w[N],pos[N],pre[N];

void pushup(int u)
{
	tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
}

void pushdown(int u)
{
	if(tr[u].add)
	{
		int d=tr[u].add;
		tr[u<<1].add+=d; tr[u<<1].v+=d;
		tr[u<<1|1].add+=d; tr[u<<1|1].v+=d;
		tr[u].add=0;
	}
}
void build(int u,int l,int r)
{
	if(l==r){
		tr[u]={l,l,0,0};
	}
	else {
		tr[u]={l,r};
		int mid=l+r>>1;
		build(u<<1,l,mid); build(u<<1|1,mid+1,r);
		pushup(u);
	}
}
void modify(int u,int l,int r,int v)
{
	if(tr[u].l>=l&&tr[u].r<=r){
		tr[u].add+=v; tr[u].v+=v;
	}
	else {
		pushdown(u);
		int mid=tr[u].l+tr[u].r>>1;
		if(l<=mid) modify(u<<1,l,r,v);
		if(r>mid) modify(u<<1|1,l,r,v);
		pushup(u);
	}
}
signed main()
{
	int n,m;
	while(cin>>n>>m)
	{
		for(int i=1;i<=n;i++) cin>>c[i];
		for(int i=1;i<=m;i++) cin>>w[i];
		memset(pos,0,sizeof pos);
		memset(pre,0,sizeof pre);
		for(int i=1;i<=n;i++){
			pre[i]=pos[c[i]];
			pos[c[i]]=i;
		}
		build(1,1,n);
		int ans=0;
		for(int i=1;i<=n;i++){
			modify(1,pre[i]+1,i,w[c[i]]);
			if(pre[i]) modify(1,pre[pre[i]]+1,pre[i],-w[c[i]]);
            //注意不要越界
			ans=max(ans,tr[1].v);
		}
		cout<<ans<<endl;
	}
	return 0;
} 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值