莫队
莫队入门
莫队是一种离线算法,适用性及其广泛。
只要就是用区间 \([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--;
}
回滚莫队
回滚莫队的思想主要是正难则反。说人话就是删除或添加操作很难实现时用更多的另一种操作代替。
具体看实现:
- 排序
- 遍历所有询问:
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)

浙公网安备 33010602011771号