牛客网暑期ACM多校训练营(第一场)J Different Integers(莫队/主席树)(求区间不同数的个数)

题目链接

题意:

给你一个长度为n的序列,有q次询问

每次询问l,r问你,a1,a2,a3,......al,ar,ar+1,.....an中有多少个不同的数

 

解析:

一开始我是用莫队做的,正的反的都做了一遍

正的是遍历询问区间的时候l=0,r=n+1,是从两端往里缩的

用vis[i]记录的是当前[1,l]+[r,n]中i的个数,那么转移的时候,只要vis[i]从0变到1就ans++,vis[i]从1变到0就ans--

#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<algorithm>
using namespace std;
 
typedef long long int ll;
 
const int MAXN = 1e5+100;
 
int n,res,m;
 
int ans;
 
int pos[MAXN];  //i位置是属于第pos[i]块的,用于对询问进行排序
 
int vis[MAXN];
 
int gg[MAXN];
 
 
 
int a[MAXN];
 
struct node{
 
    int l;
 
    int r;
 
    int id;
 
    int ans;
 
}q[MAXN];
 
 
 
bool cmp(node a,node b)
 
{
 
    if(pos[a.l]==pos[b.l]) return a.r>b.r;
 
    return a.l<b.l;
 
}
 
 
 
bool cmp1(node a,node b)
 
{
 
    return a.id<b.id;
 
}
 
 
 
inline void updateplus(int x)
{
    vis[x]+=1;
    /*if(vis[x]==1)
        ans++;*/
    ans+=(vis[x]==1?1:0);
}
 
inline void updateminus(int x)
{
    vis[x]-=1;
    /*if(vis[x]==0)
        ans--;*/
    ans-=(vis[x]==0?1:0);
}
 
 
 
 
inline void solve()
{
 
    int i,l,r;
    l=0;r=n+1;
 
    for(i=1;i<=m;i++)
    {
        while(l<q[i].l) updateplus(a[l+1]),l++;  //将l向右移至q[i].l
        while(l>q[i].l) updateminus(a[l]),l--;   //将l向右移至q[i].l
        while(r>q[i].r) updateplus(a[r-1]),r--;     //将r左移至q[i].r
        while(r<q[i].r) updateminus(a[r]),r++;    //将r/右移至q[i].r
        gg[q[i].id]=ans;
    }
 
}
 
 
 
int main()
{
 
    int k;
 
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        int block=sqrt(n);
        ans=0;
        for(int i=0;i<=n;i++) vis[i]=0;
        res=0;
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
            pos[i]=(i-1)/block;
        }
        a[n+1]=0;
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d",&q[i].l,&q[i].r);
            q[i].id=i;
        }
        sort(q+1,q+m+1,cmp);  //按照块来排序
        solve();
        //sort(q+1,q+m+1,cmp1);
        for(int i=1;i<=m;i++)
        {
            printf("%d\n",gg[i]);
        }
    }
    return 0;
}

反的话我是vis[i]记录除[l,r]外i的个数,那么我们就可以从l=1,r=0开始,按普通的莫队来扫,都逐渐往右扫。

ans表示的是[l,r]内不同数的个数。一开始vis需要预处理出来,因为一开始区间是空的。当vis[i]从1变成0,ans++

vis[i]从0变成1,ans--。这样每次询问答案就是总的不同数的个数-ans

#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<algorithm>
using namespace std;
 
typedef long long int ll;
 
 
 
const int MAXN = 1e5+100;
 
int n,res,m;
int ans;
 
int pos[MAXN];  //i位置是属于第pos[i]块的,用于对询问进行排序
 
int vis[MAXN];
 
 
 
int a[MAXN];
 
struct node{
    int l;
    int r;
    int id;
    int ans;
 
}q[MAXN];
 
 
 
bool cmp(node a,node b)
 
{
 
    if(pos[a.l]==pos[b.l]) return a.r<b.r;
 
    return a.l<b.l;
 
}
 
 
 
bool cmp1(node a,node b)
{
    return a.id<b.id;
}
 
void update1(int x)
{
    vis[x]+=1;
    /*if(add>0&&vis[x]==1)
        ans--;*/
    ans-=(vis[x]==1?1:0);
}
 
void update2(int x)
{
    vis[x]-=1;
    /*if(add<0&&vis[x]==0)
        ans++;
    else if(add>0&&vis[x]==1)
        ans--;*/
    ans+=(vis[x]==0?1:0);
}
 
void solve()
{
    int i,l,r;
    l=1;r=0;
    ans=0;
    for(i=1;i<=m;i++)
    {
        if(q[i].ans!=-1) continue;
        while(l<q[i].l) update1(a[l]),l++;  //将l向右移至q[i].l
        while(l>q[i].l) update2(a[l-1]),l--;   //将l向左移至q[i].l
        while(r>q[i].r) update1(a[r]),r--;     //将r左移至q[i].r
        while(r<q[i].r) update2(a[r+1]),r++;    //将r右移至q[i].r
        q[i].ans=res-ans;
    }
 
}
 
int main()
{
    int k;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        int block=sqrt(n);
 
        //for(int i=0;i<=n;i++) vis[i]=0;
        memset(vis,0,sizeof(vis));
        res=0;
        for(int i=1;i<=n;i++)
        {
 
            scanf("%d",&a[i]);
            pos[i]=(i-1)/block;
            if(vis[a[i]]==0) res++;
            vis[a[i]]++;
        }
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d",&q[i].l,&q[i].r);
            q[i].ans=-1;
            //if(q[i].l>q[i].r) swap(q[i].l,q[i].r);
            if(q[i].l==q[i].r||q[i].l+1==q[i].r||q[i].l>q[i].r) q[i].ans=res;
 
            q[i].l++;
            q[i].r--;
            q[i].id=i;
        }
        sort(q+1,q+m+1,cmp);  //按照块来排序
        solve();
        sort(q+1,q+m+1,cmp1);
        for(int i=1;i<=m;i++)
        {
            printf("%d\n",q[i].ans);
        }
    }
    return 0;
}


主席树rt[i]维护的是a[1..i]的不同的数的最后一次出现的位置
我们先将a,扩展成a[1...2n](a[1..n]=a[n+1...2n])
然后按照下标插入主席树,插入a[i]时,若a[i]是第一次出现,那么就将主席树对应i位置值+1
否则,就要在rt[i]这棵树内将a[i]原来的位置-1,再把i的位置+1(维护最后出现的位置)

那么对于查询[l,r],就是查询[r,l+n],我们只需要在rt[l+n]这棵树中,查询[r,l+n]这个区间的不同数的个数。其实只需查询r位置的叶子节点,然后回溯的时候把右子树的值都加上就可以了。
这样做的原因是在[r,l+n]区间内的不同的数,在[①,l+n]最后出现的位置一定是在[r,l+n]中的
所以在[①...l+n]维护最后出现的位置一定可以查询出[k,l+n](k<=l+n)内不同数的个数

/*********分割线***********/

这里的主席树不具有前缀的性质,这里这样做主要是为了在线查询。

其实如果将询问排序,按l+n(右边界值)从小到大排序,那么其实只需要一棵线段树就够了

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

 

const int MAXN = 1e5+5;

//const int MAXM = (1e5+5)*15;

 

typedef struct node

{

    int x;

    int id;

}node;

 

int a[MAXN];

int ran[MAXN];

int rt[MAXN*20],ls[MAXN*80],rs[MAXN*80],tot,sz;

int sum[MAXN*80];
int vis[MAXN];

 

bool cmp(node a,node b)

{

    return a.x<b.x;

}

 

void build(int& root,int l,int r)

{

    root=++tot;

    sum[root]=0;

    if(l==r) return;

    int mid=(l+r)>>1;

    build(ls[root],l,mid);

    build(rs[root],mid+1,r);

 

}

 

void update(int& root,int l,int r,int last,int x,int val)
{
    root=++tot;
    ls[root]=ls[last];
    rs[root]=rs[last];
    sum[root]=sum[last]+val;
    if(l==r) return ;
    int mid=(l+r)>>1;
    if(mid>=x) update(ls[root],l,mid,ls[last],x,val);
	else update(rs[root],mid+1,r,rs[last],x,val);

}

 

int query(int tt,int l,int r,int x)
{
	if(l==r)
	{
		return sum[tt];
	}
	int mid=(l+r)>>1;
	if(mid>=x)
		return query(ls[tt],l,mid,x)+sum[rs[tt]];
	else
		return query(rs[tt],mid+1,r,x);
    

}

 

int main()

{

    int t;

    int n,m;


    while(~scanf("%d%d",&n,&m))
    {

        for(int i=1;i<=n;i++) scanf("%d",&a[i]),vis[i]=0;

        tot=0;

        //sort(ran+1,ran+1+n);

        //sz=unique(ran+1,ran+1+n)-(ran+1);

        

        //for(int i=1;i<=n;i++) a[i]=lower_bound(ran+1,ran+1+sz,a[i])-ran;
		sz=n;
		build(rt[0],1,sz);
        for(int i=1;i<=n;i++) 
		{
			int x=a[i];
			if(!vis[x])
				update(rt[i],1,sz,rt[i-1],i,1);
			else
			{
				int t;
				update(t,1,sz,rt[i-1],vis[x],-1);
				update(rt[i],1,sz,t,i,1);
			}
			vis[x]=i;
		}

		for(int i=n+1;i<=2*n;i++) 
		{
			int x=a[i-n];
			if(!vis[x])
				update(rt[i],1,sz,rt[i-1],i,1);
			else
			{
				int t;
				update(t,1,sz,rt[i-1],vis[x],-1);
				update(rt[i],1,sz,t,i,1);
			}
			vis[x]=i;
		}
		//for(int i=n+1;i<=2*n;i++) update(rt[i],1,sz,rt[i-1],a[i-n]);

        for(int i=0;i<m;i++)
        {

            int l,r;

            scanf("%d%d",&l,&r);
			int ind=query(rt[l+n],1,sz,r);
			printf("%d\n",ind);
        }

    }

    return 0;

}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值