莫队分块

莫队算法

用于解决一类区间询问问题的办法

对于一段给定的数据序列,有 \(m\) 次询问,询问的区间 \([l,r]\) 如果能够在 \(O(1)\) 的时间复杂度内将区间拓展到 \([l\pm1,r\pm 1]\) ,那么我们就能够在 \(O(n\sqrt n)\) 的时间复杂度内计算所有询问的答案

将这段数据序列均匀划分为 \(\sqrt n\) 个区域,对所有的询问离线后按照 \(l\) 块的编号,\(r\) 块的编号进行双关键字排序


以洛谷 P2709 为例:

题目描述

小B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求:$$\sum\limits_{i=1}^k c_i^2$$ ,其中 \(c_i\) 表示数字 \(i\)\([l,r]\) 中的出现次数。

struct que{
	int l,r,id;
};

void solve(){
	int n,m,k;
	cin>>n>>m>>k;
	vector<int> a(n+1);
	for (int i=1;i<=n;i++) cin>>a[i];
	vector<que> q(m);
	for (int i=0;i<m;i++) cin>>q[i].l>>q[i].r,q[i].id=i;
	int B=sqrt(n);//分块大小
	vector<int> cnt(n+1);
	LL sum=0;
	
	auto cmp=[&](que x,que y){
		if (x.l/B==y.l/B) return x.r<y.r;
		return x.l<y.l;
	};//双关键字排序
	
	auto add=[&](int x){
		sum-=cnt[a[x]]*cnt[a[x]];
		cnt[a[x]]++;
		sum+=cnt[a[x]]*cnt[a[x]];
		
	};
	auto del=[&](int x){
		sum-=cnt[a[x]]*cnt[a[x]];
		cnt[a[x]]--;
		sum+=cnt[a[x]]*cnt[a[x]];
	};
	sort(q.begin(),q.end(),cmp);
	vector<int> ans(m);
	for (int i=0,l=1,r=0;i<m;i++){
		while (l>q[i].l) add(--l);
		while (r<q[i].r) add(++r);
		while (l<q[i].l) del(l++);
		while (r>q[i].r) del(r--);
		ans[q[i].id]=sum;
	}
	for (auto x:ans) cout<<x<<endl;
}

特别的,朴素的莫队算法常数比较大,原因有一部分是因为在相同块内的 \(r\) 指针都是单调排列的,在相邻块间移动时 \(r\) 指针前后会重复移动:

image-20250806224156020

解决办法时将奇数块的内的 \(r\) 按照升序排列,偶数块内的 \(r\) 按照降序排列:

image-20250806224259805

auto cmp=[&](que x,que y){
	if (x.l/B==y.l/B) return (x.l/B&1)?x.r<y.r:x.r>y.r;
	return x.l<y.l;
};//双关键字排序

带修改莫队

一般情况下,对于区间查询问题莫队算法可以有良好的时间复杂度,但是如果需要执行区间修改操作,那么普通的莫队算法就不能维护出区间的信息

我们对所有操作都记录下它的时间戳,按照三关键字进行排序后,两个询问之间的时间差为 \(tim_i-tim_j\)

只要能够在 时间复杂度可接受的(\(k=O(1),O(\log n)\) 范围内维护出这段时间差中的贡献,那么就可以做到 \(O(n^{\frac{5}{3}}\times k)\) 的时间复杂度回答


以洛谷 P1903 为例

题目描述

墨墨购买了一套 \(N\) 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:

  1. \(Q\ L\ R\) 代表询问你从第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。

  2. \(R\ P\ C\) 把第 \(P\) 支画笔替换为颜色 \(C\)

为了满足墨墨的要求,你知道你需要干什么了吗?

struct que{
	int l,r,id,tim;
};

void solve(){
	int n,m;
	cin>>n>>m;
	vector<int> a(n+1);
	for (int i=1;i<=n;i++) cin>>a[i];
	vector<que> q(m+1);
	vector<pair<int,int>> R(m+1);
	int id=0,tim=0;
	for (int i=0;i<m;i++){
		char c;
		int l,r;
		cin>>c>>l>>r;
		if (c=='Q') q[++id]={l,r,id,tim};
		else R[++tim]={l,r};
	}
	
	int B=pow(n,0.666);
	
	auto cmp=[&](que x,que y){
		if (x.l/B!=y.l/B) return x.l<y.l;
		if (x.r/B!=y.r/B) return (x.l/B&1)?x.r<y.r:x.r>y.r;
		return x.tim<y.tim;
	};
	
	sort(q.begin()+1,q.begin()+id,cmp);
	
	vector<int> mp(1000010);
	int sum=0;
	vector<int> ans(id+1);
	
	auto add=[&](int x){
		if (++mp[x]==1) sum++;
	};
	
	auto del=[&](int x){
		if (--mp[x]==0) sum--;
	};
	
	for (int i=1,l=1,r=0,tim=0;i<=id;i++){
		while (l>q[i].l) add(a[--l]);
		while (r<q[i].r) add(a[++r]);
		while (l<q[i].l) del(a[l++]);
		while (r>q[i].r) del(a[r--]);
		while (tim<q[i].tim){
			tim++;
			int p=R[tim].first;
			if (l<=p&&p<=r) del(a[p]),add(R[tim].second);
			swap(a[p],R[tim].second);
		}
		while (tim>q[i].tim){
			int p=R[tim].first;
			if (l<=p&&p<=r) del(a[p]),add(R[tim].second);
			swap(a[p],R[tim].second);
			tim--;
		}
		ans[q[i].id]=sum;
	}
	for (int i=1;i<=id;i++) cout<<ans[i]<<endl;
}

可以证明这种情况下将分块大小设置为 \(n^{\frac{2}{3}}\) 是最优的,对于每个新扩展进来的元素,我们可以 \(O(1)\) 的统计是不是一个没有出现过的元素,所以总的时间复杂度为 \(O(n^{\frac{5}{3}})\)

对于一段当前询问的区间以及它的时间戳 \(tim_i\) ,维护出当前时间戳 \(x\) 到这段时间中修改操作的贡献

while (tim<q[i].tim){
	tim++;
	int p=R[tim].first;
	if (l<=p&&p<=r) del(a[p]),add(R[tim].second);
	swap(a[p],R[tim].second);
}
while (tim>q[i].tim){
	int p=R[tim].first;
	if (l<=p&&p<=r) del(a[p]),add(R[tim].second);
	swap(a[p],R[tim].second);
	tim--;
}
posted @ 2025-08-08 20:34  才瓯  阅读(6)  评论(0)    收藏  举报