浅谈分块 区间众数

前言

分块大法好(

本文直接讲例题了 qwq



P4168 [Violet]蒲公英

题目链接:P4168 [Violet]蒲公英

题意: 找到区间内编号最小的众数,强制在线

解法一

直接分块

设块长为 l e n len len ,块的总数为 t t t

我们先预处理出所有以块的端点为端点的区间众数和众数的出现次数

或者说就是把每个连续块组成的区间都算出编号最小众数和它的出现次数

别忘了离散化,不然MLE咯

总共有 t 2 t^2 t2 个这样的区间,每次处理 O ( n ) O(n) O(n) ,则预处理时间复杂度为 O ( n t 2 ) O(nt^2) O(nt2)

暴力查询时可以在预处理过的数组基础上临时更新数组,以统计答案,再复原数组

总时间复杂度 O ( n t 2 + m n / t ) O(nt^2+mn/t) O(nt2+mn/t)

假设 m m m n n n 数量级相同,解 n t 2 = m n / t nt^2=mn/t nt2=mn/t t ≈ n 3 t \approx \sqrt[3]{n} t3n

则时间复杂度可以控制在 O ( n 5 / 3 ) O(n^{5/3}) O(n5/3)

空间复杂度比较大, O ( n t 2 ) O(nt^2) O(nt2) (注:预处理所需)

代码如下

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define MAXN (int)(5e4+5)
#define MAXM (int)(45)
#define pc(a) putchar(a)
template<typename T>inline void read(T &k)
{
	char ch=getchar();T x=0,f=1;
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	k=x*f;
}
template<typename T>void write(T k)
{
	if(k<0){pc('-');k=-k;}
	if(k>9)write(k/10);
	pc(k%10+'0');
}
int n,m,Q,len,last,t;
int id[MAXN],L[MAXN],R[MAXN],c[MAXM][MAXM][MAXN]; // c[i][j][a[k]] 表示块i~j中a[k]的出现次数
int b[MAXN],f[MAXM][MAXM][2],now[2],a[MAXN]; // f[i][j][0/1]分别记录次数和众数
void init()
{
    // 预处理
	sort(b+1,b+1+n);
	m=unique(b+1,b+1+n)-b-1;
	for(int i=1; i<=n; i++)
		a[i]=lower_bound(b+1,b+1+m,a[i])-b; // 离散化
	t=pow((double)n,(double)1/3); // 块的数量
	len=t?n/t:n; // 块的长度
	for(int i=1; i<=t; i++)
	{
		L[i]=(i-1)*len+1;
		R[i]=i*len;
	}
	if(R[t]<n)
	{
		L[t+1]=R[t]+1;
		R[++t]=n;
	}
	for(int i=1; i<=t; i++)
		for(int j=L[i]; j<=R[i]; j++)
			id[j]=i;
	for(int i=1; i<=t; i++)
		for(int j=i; j<=t; j++)
		{
			for(int k=L[i]; k<=R[j]; k++)
				++c[i][j][a[k]]; // 统计答案
			for(int k=1; k<=m; k++)
			{
				if(c[i][j][k]>f[i][j][0]) // 更新答案
				{
					f[i][j][0]=c[i][j][k];
					f[i][j][1]=k;
				}
			}
		}
}
void proc(int x,int y,int k)
{
	++c[x][y][k]; // 临时修改
	if(c[x][y][k]>now[0]||c[x][y][k]==now[0]&&k时间<now[1])
	{
		now[0]=c[x][y][k]; // 更新答案
		now[1]=k;
	}
}
void solve(int l,int r)
{
	l=(l+last-1)%n+1; // 题目要求
	r=(r+last-1)%n+1;
	if(l>r)swap(l,r); // 题目要求

	int p=id[l],q=id[r],x=0,y=0;
	if(p+1<=q-1)x=p+1,y=q-1;
	memcpy(now,f[x][y],sizeof(now));
	if(p==q)
	{
		for(int i=l; i<=r; i++)proc(x,y,a[i]); // 临时修改+更新答案
		for(int i=l; i<=r; i++)--c[x][y][a[i]]; // 改回来
	}else
	{
		for(int i=l; i<=R[p]; i++)proc(x,y,a[i]);
		for(int i=L[q]; i<=r; i++)proc(x,y,a[i]);
		for(int i=l; i<=R[p]; i++)--c[x][y][a[i]];
		for(int i=L[q]; i<=r; i++)--c[x][y][a[i]];
	}
	write(last=b[now[1]]);pc('\n');
}
signed main()
{
	read(n);read(Q);
	for(int i=1; i<=n; i++)
		read(a[i]),b[i]=a[i];
	init();
	while(Q--)
	{
		int l,r;
		read(l);read(r);
		solve(l,r);
	}
	return 0;
}

解法二

还是分块,只不过维护的东西不一样

我们发现,解法一的空间就多在了次数统计这个东西上

次数统计?不是可以二分吗?

对每个数值建个vector保存它出现的下标,出现次数就是upper_bd-lower_bd

这样我们只要预处理众数是谁就行了

暴力查询结果和众数的结果比一下就行了

别忘了离散化哦

有点时间换空间的感觉,但是还是很快

时间复杂度 O ( n t + m n / t × log ⁡ n ) O(nt+mn/t\times \log n) O(nt+mn/t×logn)

假设 m m m n n n 数量级相同,还是解方程, t ≈ n log ⁡ n t \approx \sqrt{n\log n} tnlogn

则时间复杂度可控制在 O ( n n log ⁡ n ) O(n\sqrt{n\log n}) O(nnlogn )

空间复杂度 O ( t 2 ) O(t^2) O(t2) ,不错

代码如下

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define MAXN (int)(5e4+5)
#define MAXM (int)(888)
#define pc(a) putchar(a)
template<typename T>inline void read(T &k)
{
	char ch=getchar();T x=0,f=1;
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	k=x*f;
}
template<typename T>void write(T k)
{
	if(k<0){pc('-');k=-k;}
	if(k>9)write(k/10);
	pc(k%10+'0');
}
int n,m,Q,last,t,len,c[MAXN],id[MAXN];
int a[MAXN],b[MAXN],L[MAXM],R[MAXM],f[MAXM][MAXM];
vector<int> vec[MAXN];
int find(int k,int l,int r)
{
	return upper_bound(vec[k].begin(),vec[k].end(),r)
	-lower_bound(vec[k].begin(),vec[k].end(),l);
} // 出现次数
void proc(int k,int l,int r,int &ans,int &cnt)
{
	int res=find(k,l,r);
	if(res>cnt||res==cnt&&k<ans)
	{
		cnt=res;
		ans=k;
	} // 更新答案
}
void init()
{
	sort(b+1,b+1+n);
	m=unique(b+1,b+1+n)-b-1;
	for(int i=1; i<=n; i++)
	{
		a[i]=lower_bound(b+1,b+1+m,a[i])-b;
		vec[a[i]].push_back(i); // 记录下标
	}
	t=sqrt(log(n)/log(2)*n);
	len=t?n/t:n;
	for(int i=1; i<=t; i++)
	{
		L[i]=(i-1)*len+1;
		R[i]=i*len;
	}
	if(R[t]<n)
	{
		L[t+1]=R[t]+1;
		R[++t]=n;
	}
	for(int i=1; i<=t; i++)
		for(int j=L[i]; j<=R[i]; j++)
			id[j]=i;
	for(int i=1; i<=t; i++)
	{
		memset(c,0,sizeof(c)); // 统计次数
		int cnt=0,ans=0;
		for(int j=L[i]; j<=n; j++)
		{
			++c[a[j]];
			if(c[a[j]]>cnt||c[a[j]]==cnt&&a[j]<ans)
			{
				cnt=c[a[j]];
				ans=a[j];
			}
			f[i][id[j]]=ans; // 众数
		}
	}
}
void solve(int l,int r)
{
	l=(l+last-1)%n+1;
	r=(r+last-1)%n+1;
	if(l>r)swap(l,r);
	int p=id[l],q=id[r],x=0,y=0,ans=0,cnt=0;
	if(p+1<=q-1)x=p+1,y=q-1;
	if(p==q)
	{
		for(int i=l; i<=r; i++)
			proc(a[i],l,r,ans,cnt);
	}else
	{
		for(int i=l; i<=R[p]; i++)proc(a[i],l,r,ans,cnt);
		for(int i=L[q]; i<=r; i++)proc(a[i],l,r,ans,cnt);
	}
	if(f[x][y])proc(f[x][y],l,r,ans,cnt);
	write(last=b[ans]);pc('\n');
}
signed main()
{
	read(n);read(Q);
	for(int i=1; i<=n; i++)
		read(a[i]),b[i]=a[i];
	init();
	while(Q--)
	{
		int l,r;
		read(l);read(r);
		solve(l,r);
	}
	return 0;
}

然后我们就想到

如果只需要知道众数的出现次数,有没有什么好的办法呢?


[Ynoi2019 模拟赛] Yuno loves sqrt technology III

题目链接:[Ynoi2019 模拟赛] Yuno loves sqrt technology III

题意:区间众数出现次数,强制在线

类似于上一题的解法二,这题 t = n t=\sqrt{n} t=n 即可

不同的是,我们要预处理出连续块的众数出现次数

然后还是每个数值建一个vector,统计出现的下标

再记录每个存的下标在vector中的下标 a x i ax_i axi

显然非暴力查询的区域,答案 r e s = f [ x ] [ y ] res=f[x][y] res=f[x][y]

因为边界数最多对答案有 2 n 2\sqrt{n} 2n 的贡献

所以暴力查询时,对于每个左侧元素 k k k ,我们只要找到 k k k 的vector中 a x k + r e s ax_k+res axk+res 的位置的元素,看它是不是小于 r r r ,如果小于 r r r ,说明它一定更接近最终答案且在该区间内,更新 r e s res res

对于每个右侧元素 k k k ,我们只要找到 k k k 的vector中 a x k − r e s ax_k-res axkres 的位置的元素,看它是不是大于 l l l ,原理同上

最终答案即为 r e s res res

时间复杂度 O ( ( n + m ) n ) O((n+m)\sqrt{n}) O((n+m)n )

空间复杂度 O ( n ) O(n) O(n)

代码如下

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define MAXN (int)(5e5+5)
#define MAXM (int)(888)
#define pc(a) putchar(a)
template<typename T>inline void read(T &k)
{
	char ch=getchar();T x=0,f=1;
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	k=x*f;
}
template<typename T>void write(T k)
{
	if(k<0){pc('-');k=-k;}
	if(k>9)write(k/10);
	pc(k%10+'0');
}
int n,m,Q,last,t,len,c[MAXN],id[MAXN],cnt[MAXN];
int a[MAXN],b[MAXN],L[MAXM],R[MAXM],f[MAXM][MAXM],ax[MAXN];
vector<int> vec[MAXN];
void init()
{
	sort(b+1,b+1+n);
	m=unique(b+1,b+1+n)-b-1;
	for(int i=1; i<=n; i++)
	{
		a[i]=lower_bound(b+1,b+1+m,a[i])-b; // 离散化
		vec[a[i]].push_back(i); // 下标
		ax[i]=vec[a[i]].size()-1; // 每个下标在vector中的下标
	}
	t=sqrt(n);
	len=t?n/t:n;
	for(int i=1; i<=t; i++)
	{
		L[i]=(i-1)*len+1;
		R[i]=i*len;
	}
	if(R[t]<n)
	{
		L[t+1]=R[t]+1;
		R[++t]=n;
	}
	for(int i=1; i<=t; i++)
		for(int j=L[i]; j<=R[i]; j++)
			id[j]=i;
	for(int i=1; i<=t; i++)
	{
		memset(c,0,sizeof(c));
		for(int j=i; j<=t; j++)
		{
			f[i][j]=f[i][j-1];
			for(int k=L[j]; k<=R[j]; k++)
				f[i][j]=max(++c[a[k]],f[i][j]); 
				// 统计次数即可(它又不要知道众数是谁咯 qwq)
		}
	}
}
void solve(int l,int r)
{
	l^=last;r^=last; // 题目要求
	if(l>r)swap(l,r);
	int p=id[l],q=id[r],x=0,y=0,res=0;
	if(p+1<=q-1)x=p+1,y=q-1;
	if(p==q)
	{
		for(int i=l; i<=r; i++)
			cnt[a[i]]=0;
		for(int i=l; i<=r; i++)
			res=max(++cnt[a[i]],res);
	}else
	{
		res=f[x][y];
		for(int i=l; i<=R[p]; i++)
		{
			int it=ax[i]+res;
			while(it<vec[a[i]].size()&&vec[a[i]][it]<=r)
				{++res;++it;} // 暴力扩展+更新答案
		}

		for(int i=L[q]; i<=r; i++)
		{
			int it=ax[i]-res;
			while(it>=0&&vec[a[i]][it]>=l)
				{++res;--it;}
		}
	}
	write(last=res);pc('\n');
}
signed main()
{
	read(n);read(Q);
	for(int i=1; i<=n; i++)
		read(a[i]),b[i]=a[i];
	init();
	while(Q--)
	{
		int l,r;
		read(l);read(r);
		solve(l,r);
	}
	return 0;
}

总结

本文简单讲了下区间众数的分块解法

转载请说明出处

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值