朴素莫队

朴素莫队


莫队算法简介

莫队算法是由莫涛提出的算法。在莫涛提出莫队算法之前,莫队算法已经在 \(Codeforces\) 的高手圈里小范围流传,但是莫涛是第一个对莫队算法进行详细归纳总结的人。莫涛提出莫队算法时,只分析了普通莫队算法,但是经过 $OIer $和 \(ACMer\)的集体智慧改造,莫队有了多种扩展版本。

莫队算法可以解决一类离线区间询问问题(尽管在一些毒瘤\(OIer\)的改造下,已经有了在线莫队),适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。


算法讲解

莫队是一种基于分块的离线算法,通过动态调整区间范围来快速维护答案

我们将一个序列分割成\(\sqrt{n}\)快,每块长为$ \sqrt{n} $,对于每个询问区间[l,r],我们按照每个区间的l所在区块地址从小到大排序,对于在边界在同一区间内的区间,按照右边界r 从下到大排序,然后维护莫队区间

将这次询问的区间扩展或缩小到下一个区间,总时间复杂度最坏情况下为为\(O((N+Q)\sqrt{N})\),\(Q\)为询问数

简单证明一下时间复杂度,我们考虑为最坏情况,每个左边界的最坏情况为每个左边界所在区块都不同,且同一个区块内的左边界距离最远,那么此时若要遍历到每一个左边界,从一个左边界去往下一个左边界的时间为\(O(\sqrt{N})\),总时间复杂度为\(O(Q\sqrt{N})\) 。右边界的最坏情况为\(O(N(\sqrt{N}))\),所以总时间复杂度为\(O((N+Q)\sqrt{N})\)。

为什么右边界移动的总体时间复杂度为\(O(N(\sqrt{N}))\)呢?

考虑最坏数据,我们为了能吃满移动,将每个分块的左右边界上都放上一个查询区间的左区间,对于它们的右区间,将其放在l+1与n的位置上能够让右区间移动次数最多,因为右区间是降序排序,遍历一遍最多也就\(O(N)\),二这种数据能够在最少的询问次数下吃满这个\(O(N)\),但是由于分块最多\(O(\sqrt{N})\)个,所以最多时间复杂度也就\(O(N\sqrt{N})\)。

然后其实莫队算法就没了,在刚才的时间复杂度证明过程中其实已经展现了莫队实现过程。

总接就是通过维护一个莫队区间来快速通过这个询问的答案转移掉另一个询问的答案,这也要求了莫队算法对题目要求可离线,并且答案区间要求可加可减。


例题讲解

通过例题展现一下莫队的实现流程

P1494 国家集训队 小 Z 的袜子 - 洛谷

莫队纯板子题

对于所有询问区间按照左区间所在区块排序,同一区块内再按照右区间从小到大排序。(其实右区间这里比较不确定,无论怎么排序都有可能会被卡,所以可以自己决定,或者学习一些例如奇偶排序的技巧)

然后左右移动莫队区间维护答案即可

示例代码: (马蜂应该还行)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5e4+10;

ll n,m,block,same;
ll color[N],cnt[N];
ll ans1[N],ans2[N];

struct node{
	ll l,r,_id;
}q[N];

void add(ll pos){//扩展区间
	same+=cnt[color[pos]];
	cnt[color[pos]]++;
}

void remove(ll pos){//缩减区间
	cnt[color[pos]]--;
	same-=cnt[color[pos]];
}

int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>m;
	block = sqrt(n);
	
	for(int i=1;i<=n;i++){
		cin>>color[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,[](node t1,node t2){//排序
		ll len1=t1.l/block;//按照左边界所在区块排序
		ll len2=t2.l/block;//利用len1,len2表示
		if(len1==len2)return t1.r<t2.r;//让右边界从小到大排序
		else return len1<len2;
	});
	
	ll L=1,R=0;
	
	for(int i=1;i<=n;i++){
		ll l=q[i].l,r=q[i].r;
		while(L > l)add(--L);//这里注意要先减或者先加左右边界
		while(R < r)add(++R);//因为这里增加新的方案数时要按照新区见的大小增加
		while(L < l)remove(L++);//缩减区间时同样注意先减后再自增
		while(R > r)remove(R--);
		
		ll len = r - l + 1;
		ll sum = 1ll*len*(len-1)/2;
		if(sum==0){
			ans1[q[i]._id]=0;
			ans2[q[i]._id]=1;
		}
		else{
			ll g=__gcd(sum,same);
			ans1[q[i]._id]=same/g;
			ans2[q[i]._id]=sum/g;
		}
	}
	for(int i=1;i<=m;i++){
		cout<<ans1[i]<<"/"<<ans2[i]<<'\n';
	}
	return 0;
}
posted @ 2025-07-27 19:33  Accepted_wyr  阅读(51)  评论(0)    收藏  举报