莫队

莫队入门

莫队是一种离线算法,适用性及其广泛。
只要就是用区间 \([l,r]\)\(O(1)\) 扩展到 \([l+1,r],[l-1,r],[l,r+1],[l,r-1]\),继续扩展知道将区间移动至下一个要查询的区间。最优可以再 \(O(n\sqrt{n})\) 的时间内完成。

实现

将询问离线,把左端点分成很多块,以左端点所在块的编号为第一关键字,右端点为第二关键字从小到大排序,再暴力扩展求出所有询问的答案。
这是最基础的莫队。

时间复杂度证明

考虑最坏情况,每一次移动左指针移动 \(\sqrt{n}\) 次,右指针最多移动 \(n\) 次,也就是一共 \(n+\sqrt{n}\) 次。总共只有 \(\frac{n}{\sqrt{n}}=\sqrt{n}\) 个块,最坏就是 \(O(n\sqrt{n})\)
\(tips:\) 使用奇奇怪怪的块长可能有意想不到的效果。

P2709 小B的询问 /【模板】莫队

最模板的,注意是每个数的平方的和,所以不能用线段树前缀和等。

点击查看代码
//加点、删点
void add(int x){
	t[f[x]]++;
	if(t[f[x]]==1) res++;
	else res+=(t[f[x]]*t[f[x]])-(t[f[x]]-1)*(t[f[x]]-1);
}
void del(int x){
	t[f[x]]--;
	if(t[f[x]]==0) res--;
	else res=res+(t[f[x]]*t[f[x]])-(t[f[x]]+1)*(t[f[x]]+1);
}
//扩展
while(l<b[i].l)del(l++);
while(l>b[i].l)add(--l);
while(r<b[i].r)add(++r);
while(r>b[i].r)del(r--);

奇偶排序优化

一般很容易遇到右指针先跑到序列最后面再跑回前面的情况,这样就很慢。
奇偶排序的思想就是奇数块左端点从小到大排序,偶数块从大到小排序,大大减少了右指针的移动次数。当然反过来也可以。
不过因为通常是先处理奇数块的询问再处理偶数块的,所以我不建议反过来。
这样可以让程序快 \(30\%\) 左右。

莫队进阶

带修莫队

与普通莫队类似,加一个时间轴,和左右端点一样扩展。

P1903 [国家集训队] 数颜色 / 维护队列 /【模板】带修莫队

点击查看代码
//修改部分
while(last<md[i].t){
	last++;
	if(rr[last].l>=l&&rr[last].l<=r)add(rr[last].r) , del(a[rr[last].l]);
	swap(a[rr[last].l],rr[last].r);
}
while(last>md[i].t){
	if(rr[last].l>=l&&rr[last].l<=r)add(rr[last].r) , del(a[rr[last].l]);
	swap(a[rr[last].l],rr[last].r);
	last--;
}

二维莫队

顾名思义,覆盖的是一个二维矩阵,也就是要用 \(4\) 个指针。
每一次移动修改一行或一列,其余基本不变。

P1527 [国家集训队] 矩阵乘法

点击查看代码
//扩展部分
while(l<q[i].y1){
	for(int i=u;i<=d;i++)cnt[a[i][l]]-- , blocksum[numid[a[i][l]]]--;
	l++;
}
while(l>q[i].y1){
	l--;
	for(int i=u;i<=d;i++)cnt[a[i][l]]++ , blocksum[numid[a[i][l]]]++;
}
while(r<q[i].y2){
	r++;
	for(int i=u;i<=d;i++)cnt[a[i][r]]++ , blocksum[numid[a[i][r]]]++;
}
while(r>q[i].y2){
	for(int i=u;i<=d;i++)cnt[a[i][r]]-- , blocksum[numid[a[i][r]]]--;
	r--;
}
while(u>q[i].x1){
	u--;
	for(int i=l;i<=r;i++)cnt[a[u][i]]++ , blocksum[numid[a[u][i]]]++;
}
while(u<q[i].x1){
	for(int i=l;i<=r;i++)cnt[a[u][i]]-- , blocksum[numid[a[u][i]]]--;
	u++;
}
while(d<q[i].x2){
	d++;
	for(int i=l;i<=r;i++)cnt[a[d][i]]++ , blocksum[numid[a[d][i]]]++;
}
while(d>q[i].x2){
	for(int i=l;i<=r;i++)cnt[a[d][i]]-- , blocksum[numid[a[d][i]]]--;
	d--;
}

回滚莫队

回滚莫队的思想主要是正难则反。说人话就是删除或添加操作很难实现时用更多的另一种操作代替。
具体看实现:

  1. 排序
  2. 遍历所有询问:
    a. 如果两个端点在同一块内,直接暴力修改,时间复杂度 \(O(\sqrt{n})\) 没问题。
    b. 如果与上一次询问的左端点不在同一块内,则将左指针拉到当前这个块的右端点(尽量让两个指针往自己的正方向走),清空上次处理留下的数据(因为左指针不在那个位置数据就没用了,相当于莫队重新开始)。
    c. 继续处理,和正常莫队基本一样只不过你不用写删除。
    d. 把左指针拉回本次处理开始前的位置,这就是回滚。

清空的话,暴力循环就可以了,不会浪费太多时间。

P5906 【模板】回滚莫队&不删除莫队

有很多细节需要注意。

点击查看代码
if(block[q[i].l]==block[q[i].r]){//暴力
	c = 0;
	for(int j=q[i].l;j<=q[i].r;j++)st[a[j]] = 0;
	for(int j=q[i].l;j<=q[i].r;j++){
		if(!st[a[j]])st[a[j]] = j;
		c = max(c,j-st[a[j]]);
	}
	for(int j=q[i].l;j<=q[i].r;j++)st[a[j]] = 0;
	ans[q[i].id] = c;
}else{
	if(block[q[i].l]!=lst){//不在同一块内
		for(int j=l;j<=r;j++)endr[a[j]] = st[a[j]] = 0;
		l = R[block[q[i].l]] , lst = block[q[i].l] , r = l - 1 , c = 0;
	}
	while(r<q[i].r){//右指针移动
		r++;
		if(!st[a[r]])st[a[r]] = r;//只用更新一次
		endr[a[r]] = r , c = max(c,r-st[a[r]]);
	}
	int p = l , tmp = 0;//不能直接用上面的c,因为下次的q[i].l可能在这次的后面c就用不了了
	while(p>q[i].l){//左指针
		p--;
		if(!endll[a[p]])endll[a[p]] = p;//同上
		tmp = max(tmp,max(endr[a[p]],endll[a[p]])-p);
	}
	ans[q[i].id] = max(tmp,c);
	while(p<l)endll[a[p]] = 0 , p++;//回滚
}

树上莫队

遍历树,到 \(x\) 点就push_back(x),遍历完就push_back(-x)
扩展的时候:

  • 添加 \(x\)add(x)
  • 添加 \(-x\)del(x)
  • 删除 \(x\)del(x)
  • 删除 $-x#:add(x)
posted @ 2025-10-16 08:59  虚空远行者  阅读(8)  评论(1)    收藏  举报