莫队+值域分块

P3834 【模板】可持久化线段树 2

虽然是可持久化线段树的题,但是也可以用莫队+值域分块来做
这个题类似于之前将到的由乃打扑克但是用之前的做法无法通过这道题。
静态查询第k小,想到另一种做法
先把所有的数离散化,值域就只到 \(2e5\)
用莫队来处理查询,接下来考虑 \(SUB\) 删除,和 \(ADD\) 加入的写法

可以放 \(2e5\) 个桶,记录每种数的出现数量,在加入和删除函数里需要把操作的元素的出现次数修改

查询的答案可以从左到右扫,不停的减去桶内的数,减不了就是找到答案(桶内的值不是对于整个序列,只是对于操作区间

但是发现这样的时间不够
可以用分块优化,把若干个桶划分成一个分块,分块记录管辖的所有桶内的值的和

这样查询时可以一块一块的跳跃,每次减去整个块内的值,然后跳到下一个块。
如果减不了再到块的内部查询,不停减去块内元素,直到找到答案,减不了
特殊情况:全部减完还有,返回剩下数值+1

注意,分块优化需要修改 \(SUB\)\(ADD\) 函数,在加入元素时,这个元素的桶数值增加,桶所处的分块的值也要加,删除元素也一样

#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
const int N=2e5+10;
int n,m,num;
struct node{int l,r,k,id;}q[N];
int len;
bool cmp(node A,node B){
	if(A.l/len==B.l/len)
		return A.r>B.r;
	return A.l<B.l;
}
int ans[N],sum[N],cnt[N];
int a[N],b[N];
map<int,int>f;
void add(int x,int val){
	int bl=x/num;
	cnt[x]+=val;
	sum[bl]+=val;
	return;
}
int rk(int k){
	for(int i=0;i<=num;i++){
		if(k<=sum[i]){
			for(int j=i*num;j<=(i+1)*num;j++){
				if(k<=cnt[j])return j;
				else k-=cnt[j];
			}
		}
		else k-=sum[i];
	}
	return k+1;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>m;
	len=sqrt(n);
	num=(n+len-1)/len;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	int ijk=unique(b+1,b+1+n)-(b+1);
	for(int i=1;i<=n;i++){
		int x=lower_bound(b+1,b+ijk+1,a[i])-b;
		f[x]=a[i];
		a[i]=x;
	}
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r>>q[i].k;
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		while(l>q[i].l){
			l--;
			add(a[l],1);
		}
		while(r<q[i].r){
			r++;
			add(a[r],1);
		}
		while(l<q[i].l){
			add(a[l],-1);
			l++;
		}
		while(r>q[i].r){
			add(a[r],-1);
			r--;
		}
		ans[q[i].id]=f[rk(q[i].k)];
	}
	for(int i=1;i<=m;i++)
		cout<<ans[i]<<'\n';
	return 0;
}


P4137 Rmq Problem / mex

可以用和上题类似的方法把查询用莫队处理,加减函数用桶

答案就从左到右扫,找到第一个没有值的桶,就是 \(mex\)
直接扫不行,就套上分块,分块的记录\(siz\)表示管辖的范围内有多少个桶是有值的
找答案就从左往右扫,如果一个分块的 \(siz\) 等于它的长度就说明整个分块的桶都是有值的,\(mex\) 就不在这个分块的管辖范围内 ,直接去下一个分块找

不然就在分块内遍历,找到第一个没有值的桶
特殊的: 如果从头到尾都没有找到就是最后一个分块的右边界\(+1\)

加减函数要对操作的元素修改,还要维护 \(siz\)

  • 加入元素: 如果加入之后这个值的出现次数是 \(1\) 次,那么这个桶就是从没有值变成了有值,所处分块的 \(siz\) \(+\) \(1\)
  • 删除元素: 如果删除之后的元素的值的出现次数变成了 \(0\) 次,说明这个值从有值变成了没有值,所处分块的 \(siz\) \(-\) \(1\)
#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
const int N=2e5+10;
int n,m,len,num,qlen;
struct node{int l,r,ans,id;}q[N];
int a[N],siz[N],mp[N],pos[N];
int L[N],R[N],P[N];
bool cmp(node A,node B){
	if(A.l/qlen!=B.l/qlen)return A.l<B.l;
	return A.r<B.r;
}
bool out(node A,node B){return A.id<B.id;}
void update(int x,int val){
	int pos=P[a[x]];
	mp[a[x]]+=val;
	if(val==-1&&mp[a[x]]==0)siz[pos]--;
	if(val==1&&mp[a[x]]==1)siz[pos]++;
}
int query(){
	for(int i=1;i<=num;i++){
		if(siz[i]==R[i]-L[i]+1)continue;
		for(int j=L[i];j<=R[i];j++){
			if(mp[j]==0)return j;
		}
	}
	return R[num]+1;
}
void init(){
	int MAX=2e5;
	len=sqrt(MAX);
	num=(MAX+len-1)/len;
	L[1]=0,R[1]=len-1;
	for(int i=2;i<=num;i++){
		L[i]=R[i-1]+1;
		R[i]=L[i]+len-1;
	}
	if(R[num]>MAX)R[num]=MAX;
	for(int i=1;i<=num;i++){
		for(int j=L[i];j<=R[i];j++){
			P[j]=i;
		}
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>m;
	qlen=sqrt(n);
	init();
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		while(l<q[i].l){update(l,-1);l++;}
		while(l>q[i].l){l--;update(l,1);}
		while(r<q[i].r){r++;update(r,1);}
		while(r>q[i].r){update(r,-1);r--;}
		q[i].ans=query();
	}
	sort(q+1,q+m+1,out);
	for(int i=1;i<=m;i++)cout<<q[i].ans<<'\n';
	return 0;
}


P3709 大爷的字符串题

题意有些抽象,是说每一次查询一个区间
对区间操作,需要按照任意顺序往集合内放值,如果放入前集合为空\(rp--\),如果有任意元素\(\ge\) 放入元素就清空集合,\(rp--\)
需要用最优的顺序放入,是的最后 \(rp\) 最多

首先无论怎样,一定希望放入的元素是按照升序的,思考发现需要排成若干条严格上升的序列,最后答案就是最少得严格上升序列数

因此问题转化为划分元素为若干序列,求最少得严格上升序列数,可以发现这个其实就是这些数的众数,即区间内出现次数最多的值的出现次数,因为没有这个值的每一次出现必然会形成一个新的序列,因为它出现最多没有值可以带它

最终只要求区间众数

莫队处理查询,桶维护每一个值的出现次数
加减操作是处理每一个值的出现次数,同时更新众数
但是这样维护不了区间众数,例如如果同时出现多个众数

\[1\;1\;2\;2\;3\;3 \]

如果由于莫队移动删掉了一个 \(1\) 就维护不了众数了,按照原来的方法会先记录最多的出现次数:\(2\)

删去的 \(1\) 出现了 \(2\) 次,所以会把区间众数 \(-1\),但这时不对的,因为还有 \(2\)\(3\) 的出现次数也是 \(2\)

用刚刚的方法错误是因为众数可能有多个,那从根本上解决问题,再开一个桶,这个桶 \(cnt[x]\) 记录的是出现次数为x的数的个数

这样在更新一个数的出现次数时,先把原来出现次数的 \(cnt\) 更新,自己更新,再重新更新 \(cnt\),同时自己更新的时候处理众数

  • 加入元素: 在加入后众数的出现次数和加入的元素的出现次数取最大值
  • 删除元素: 删除时如果删除的数的出现次数时众数的出现次数,而且删完之后,\(cnt[众数]\)\(0\) 说明这是最后一个当前众数,那么众数就要\(-1\),因为这个元素删完之后它的出现次数减少了,但是它删完之后一定还是出现次数最多的(至少没有比它出现次数更多的

每一次的答案就是当前的众数了

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int st;
int n,m,qlen;
struct node{int l,r,id,ans;}q[N];
int a[N],b[N];
map<int,int>f;
int mp[N],cnt[N],tot;
int ed;
bool cmp(node A,node B){
	if(A.l/qlen!=B.l/qlen)return A.l<B.l;
	return A.r<B.r;
}
bool out(node A,node B){
	return A.id<B.id;
}
void SUB(int x){
	cnt[mp[a[x]]]--;
	mp[a[x]]--;
	cnt[mp[a[x]]]++;
	if(cnt[tot]==0)tot--;
}
void ADD(int x){
	cnt[mp[a[x]]]--;
	mp[a[x]]++;
	cnt[mp[a[x]]]++;
	tot=max(tot,mp[a[x]]);
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	cin>>n>>m;
	qlen=sqrt(n);
	for(int i=1;i<=n;i++){
		cin>>a[i];
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	int ijk=unique(b+1,b+1+n)-(b+1);
	for(int i=1;i<=n;i++){
		int x=lower_bound(b+1,b+ijk+1,a[i])-b;
		f[x]=a[i];
		a[i]=x;
	}
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		while(l<q[i].l)SUB(l++);
		while(l>q[i].l)ADD(--l);
		while(r<q[i].r)ADD(++r);
		while(r>q[i].r)SUB(r--);
//		cerr<<l<<' '<<r<<'\n';
		q[i].ans=tot;		
	}
	sort(q+1,q+m+1,out);
	for(int i=1;i<=m;i++)
		cout<<-q[i].ans<<'\n';
	return 0;
}
posted @ 2025-08-06 11:39  wmq2012  阅读(13)  评论(0)    收藏  举报