P4688 [Ynoi2016] 掉进兔子洞

发布时间 2023-04-01 16:20:23作者: Aisaka_Taiga

RE了大约12次以后,SoN3ri告诉我是bitset开小了。

image

那你为什么全RE了啊(?

题意是给你一个长度为 \(n\) 的序列,一共 \(m\) 次询问,每次询问包含三个区间,求三个区间内相同的数去掉后剩下的数的个数。

做完了小清新人渣的本愿,看啥都是bitset+莫队,这题我也是一开始打了一个莫队+bitset,但是很不幸我因为开小了RE了12次,但这都不重要了,来看一下如何做这道题目。

首先看到值域很大,所以我们先离散化,然后对于每一个询问,我们都开一个bitset sum[N],来存放每一次询问中重复的元素的个数,我们发现题目解释里面举得例子,要求不是每一个区间去重后再进行查询,也就是说,我们不能在离散化的时候去重,这样我们在添加和删除一个值对答案的贡献的时候,我们就可以直接用第 x+cnt[x] 位来表示当前点是否重合。在进行对于答案的计算时,我们假设一开始所有的数都是不重合的,然后把 sum[i] 给全赋成1(因为后面是要进行取与运算的),表示一开始都重合,然后三个区间和 sum[i] 取与,得到的里面1的个数就是重合的元素的个数。

然后我们就可以写出下面的代码:

#include<bits/stdc++.h>
#define N 100100
using namespace std;
int n,m,kc,a[N],bl[N],ans[N],cnt[N];
bitset<N>sum[N],now;
struct sb{int l,r,id;}e[N];
inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}
inline int cmp(sb a,sb b){if(bl[a.l]==bl[b.l])return a.r<b.r;return a.l<b.l;}
inline void add(int x){now.set(x+cnt[x]);cnt[x]++;}
inline void del(int x){cnt[x]--;now.reset(x+cnt[x]);}
signed main()
{
	n=read();m=read();
	kc=sqrt(n);
	for(int i=1;i<=n;i++)
	  a[i]=read(),bl[i]=(i-1)/kc+1;
	int tmp[N];
	memcpy(tmp,a,sizeof(a));
	sort(tmp+1,tmp+n+1);
	for(int i=1;i<=n;i++)
	  a[i]=lower_bound(tmp+1,tmp+n+1,a[i])-tmp;
	int tot=0;
	now.reset();
	for(int i=1;i<=m;i++)
	{
		ans[i]=0;
		sum[i].set();
		for(int j=1;j<=3;j++)
		{
			e[++tot].l=read();e[tot].r=read();
			e[tot].id=i;
			ans[i]+=e[tot].r-e[tot].l+1;
		}
	}
	sort(e+1,e+tot+1,cmp);
	int nl=1,nr=0;
	for(int i=1;i<=tot;i++)
	{
		while(nl>e[i].l)add(a[--nl]);
		while(nr<e[i].r)add(a[++nr]);
		while(nl<e[i].l)del(a[nl++]);
		while(nr>e[i].r)del(a[nr--]);
		sum[e[i].id]&=now;
	}
	for(int i=1;i<=m;i++)
		cout<<ans[i]-(int)sum[i].count()*3<<endl;
	return 0;
}

然后我就从RE变成了:
image

开这么多不MLE才怪。。。。

然后稍加思索,我们可以把询问“分块”处理,把他分成几组小的分别处理就好了。
时限三秒放心memset。

#include<bits/stdc++.h>
#define N 100100
#define M 25100
using namespace std;
int n,m,kc,a[N],bl[N],ans[N],cnt[N];
bitset<N>sum[M],now;
struct sb{int l,r,id;}e[N];
inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}
inline int cmp(sb a,sb b){if(bl[a.l]==bl[b.l])return a.r<b.r;return a.l<b.l;}
inline void add(int x){now.set(x+cnt[x]);cnt[x]++;}//标记当前点的值出现 
inline void del(int x){cnt[x]--;now.reset(x+cnt[x]);}//标记当前点的值去除 
inline void solve()
{
	if(m==0)return ; //如果之前就已经全完成了就直接退出 
	int tot=0,i=1;
	now.reset();//清空now数组 
	for(i=1;i<=M-10;i++)//一次最多进行25000次查询操作 
	{
		if(m==0)break;//如果进行完了就退出 
		m--;
		ans[i]=0;//清空当前询问的答案 
		sum[i].set();//当前询问的集合全设为1 
		for(int j=1;j<=3;j++)//三个区间 
		{
			e[++tot].l=read();e[tot].r=read();//输入区间信息 
			e[tot].id=i;//标号 
			ans[i]+=e[tot].r-e[tot].l+1;//累加当前点的答案,假设一开始全不重合 
		}
	}
	sort(e+1,e+tot+1,cmp);//询问排序 
	int nl=1,nr=0,t=i-1;//t是当前询问的个数 
	for(int i=1;i<=tot;i++)
	{
		while(nl>e[i].l)add(a[--nl]);//正常莫队 
		while(nr<e[i].r)add(a[++nr]);
		while(nl<e[i].l)del(a[nl++]);
		while(nr>e[i].r)del(a[nr--]);
		sum[e[i].id]&=now;//当前序列内的元素的数量,三个区间都进行过&操作后sum里面存放的就是当前重合的数的离散化后的值 
	}
//	cout<<"T:"<<t<<endl;
	for(int i=1;i<=t;i++)
		cout<<ans[i]-(int)sum[i].count()*3<<endl;//减去重合的数的数量输出答案 
}
signed main()
{
	n=read();m=read();
	kc=sqrt(n);
	for(int i=1;i<=n;i++)
	  a[i]=read(),bl[i]=(i-1)/kc+1;
	int tmp[N];
	memcpy(tmp,a,sizeof(a));//复制数组 
	sort(tmp+1,tmp+n+1);//排序 
	for(int i=1;i<=n;i++)//离散化 
	  a[i]=lower_bound(tmp+1,tmp+n+1,a[i])-tmp;//直接在原数组上修改 
	for(int i=1;i<=4;i++)//分四次进行 
	{
		solve();
		for(int i=1;i<=n;i++)
		  cnt[i]=0; //每次都要清空cnt数组 
	}
	return 0;
}