莫队学习笔记
ETHANK
·
·
个人记录
P1972 [SDOI2009]HH的项链
题意
给一个有n个正整数的序列,m个询问,问区间内有多少个不同的数。
【数据范围】
1\le n,m,a_i \le 10^6, 1\le l \le r\le n
思路
由于[l,r]的答案可以O(1)扩展到[l,r+1],[l+1,r],[l-1,r],[l,r-1]的答案,我们考虑莫队算法。
时间复杂度
我们设分块长度为B,那么对于任意多个在同一块内的询问,由于R已经在块内排好序,右指针在块内最多移动n次,一共\dfrac{n}{B}个块,移动的总次数就是\dfrac{n^2}{B}。
左指针每次最多移动B,总次数就是mB。于是总复杂度为O(\dfrac{n^2}{B}+mB)。
由均值不等式可得:
\frac{n^2}{B}+mB\ge2\sqrt{\frac{n^2}{B}\cdot mB}=2n\sqrt{m}
不等式取等号当且仅当\dfrac{n^2}{B}=mB,即B=\dfrac{n}{\sqrt{m}}。
此时时间复杂度为\boxed{O(n\sqrt{m})}。
[ [Tutorial] Square root decomposition and applications](https://codeforces.com/blog/entry/83248)
## 代码
```cpp
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
inline int read()
{
int x=0,f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct query{
int l,r,id;
}q[N];
int n,m,len,a[N],id[N],now,ans[N];
int pre[N],nxt[N],tmp[N];
inline bool cmp(query a, query b) {
return (id[a.l]^id[b.l]) ? id[a.l] < id[b.l] : ((id[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
int main(){
n=read();
len=sqrt(n);
for(int i=1;i<=n;i++){
a[i]=read();
id[i]=(i-1)/len+1;
}
memset(tmp,0,sizeof(tmp));
for(int i=1;i<=n;i++){
pre[i]=tmp[a[i]];
tmp[a[i]]=i;
}
memset(tmp,0x3f,sizeof(tmp));
for(int i=n;i>=1;i--){
nxt[i]=tmp[a[i]];
tmp[a[i]]=i;
}
m=read();
for(int i=1;i<=m;i++){
q[i].l=read(),q[i].r=read(),q[i].id=i;
}
sort(q+1,q+m+1,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++){
int ql=q[i].l,qr=q[i].r;
while(l<ql){
now-=(nxt[l++]>r);
}
while(l>ql){
now+=(nxt[--l]>r);
}
while(r>qr){
now-=(pre[r--]<l);
}
while(r<qr){
now+=(pre[++r]<l);
}
ans[q[i].id]=now;
}
for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
return 0;
}
```
# P1903 [国家集训队]数颜色 / 维护队列
## 题意
上道题目带修改。
## 思路
带修莫队模板题。我们考虑对每条询问加一维时间维,即从$(l,r)$变成$(l,r,t)$。不难发现,$(l,r,t)$到$(l,r,t+1)$和$(l,r,t-1)$都可以通过直接单点修改来$O(1)$转移。通过上一题普通莫队类似的套路,就可以写出这道题目。
### 时间复杂度
记$q$为询问数,$T$为修改数。对前两维分块后分别以第一、第二关键字排序,时间维为第三关键字。设块长为$B$,那么会产生$(\dfrac{n}{B})^2$块。对于每一块,时间在排序后最多移动$T$次,复杂度为$O(\dfrac{n^2T}{B^2})$。每个询问贡献$O(B)$的时间复杂度,总复杂度为$O(qB)$。于是最后的复杂度为$O(\dfrac{n^2T}{B^2}+qB)$。
由均值不等式得:
$$\frac{n^2T}{B^2}+qB=\frac{n^2T}{B^2}+\frac{qB}{2}+\frac{qB}{2}\ge3\sqrt[3]\frac{Tn^2q^2}{4}$$
不等式取等号当且仅当$\dfrac{n^2T}{B^2}=\dfrac{qB}{2}$,即$B=\sqrt[3]{\frac{2n^2T}{q}}$。
此时时间复杂度为$\boxed{O(\sqrt[3]{Tn^2q^2})}$。
## 代码
```cpp
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5,C=1e6+5;
inline int read()
{
int x=0,f=1;char ch=getchar();
while (!isdigit(ch))ch=getchar();
while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct query{
int l,r,Tim,id;
}q[N];
struct change{
int pos,Old,New;
}c[N];
int n,m,a[N],cnt[C],blo[N],ans[N];
int t,Time,now[N],T,l=1,r,Ans;
inline bool cmp(query a, query b) {
return blo[a.l]==blo[b.l]?(blo[a.r]==blo[b.r]?a.Tim<b.Tim:a.r<b.r):a.l<b.l;
}
inline void rem(int col,int opt){
cnt[col]+=opt;
if(opt>0) Ans+=(cnt[col]==1);
else Ans-=(cnt[col]==0);
}
inline void doit(int pos,int col){
if(l<=pos&&pos<=r){
rem(col,1);rem(a[pos],-1);
}
a[pos]=col;
}
int main(){
n=read(),m=read();
int B=2*n/pow(n,1.0/3);
for(int i=1;i<=n;i++){
a[i]=read();
now[i]=a[i];
blo[i]=(i-1)/B+1;
}
for(int i=1;i<=m;i++){
char ch;int x,y;
cin>>ch;
x=read(),y=read();
if(ch=='Q'){
t++;
q[t]=(query){x,y,Time,t};
}else{
c[++Time]=(change){x,now[x],y};
now[x]=y;
}
}
sort(q+1,q+t+1,cmp);
for(int i=1;i<=t;i++){
while(T<q[i].Tim){
++T;
doit(c[T].pos,c[T].New);
}
while(T>q[i].Tim){
doit(c[T].pos,c[T].Old);
--T;
}
int ql=q[i].l,qr=q[i].r;
while(l<ql)rem(a[l++],-1);
while(l>ql)rem(a[--l],1);
while(r>qr)rem(a[r--],-1);
while(r<qr)rem(a[++r],1);
ans[q[i].id]=Ans;
}
for(int i=1;i<=t;i++)printf("%d\n",ans[i]);
return 0;
}
```
# AT1219 歴史の研究
## 题意
给一个长度为$n$的数组$A$和$m$个询问$(1\le n,m\le10^5)$,每次询问一个区间$[L,R]$内重要度最大的数字,要求**输出其重要度** 。一个数字$i$重要度的定义为$i$乘上$i$在区间内出现的次数。
## 思路
我们发现增加的操作很好实现,但是删除的操作不太好实现。因为删除后如果影响答案,我们很难确定新的答案是哪一个,所以这道题目不能直接普通莫队。
考虑一种只需要扩张区间,不需要做删除的莫队——**回滚莫队**。我们把左端点按所属块升序为第一关键字,右端点升序为第二关键字排序。
如何处理询问呢?
- 如果询问的左端点所在块$B$和上一个询问不同,我们把莫队的左指针初始化为$B$的右端点$+1$,把右指针初始化为$B$的右端点。
- 如果左右端点在同一块内,直接暴力扫描区间回答询问。
- 如果左右端点不在同一块内,我们先扩展莫队的左右指针来更新答案,再把左指针滚回到$B$的右端点$+1$。
### 时间复杂度
设左右端点在同一块内的询问数量为$C_1$,不在同一块内的询问数量为$C_2$。
对于左右端点在同一块内的询问,时间复杂度为$O(\sqrt n)$。总时间复杂度就是$O(C_1\sqrt n)$。
对于不在同一块内的询问,对于左端点在块$B$内的所有询问,右指针最多扩展$n$次,左端扩展和回滚的复杂度为$O(\sqrt n)$。总共有$\sqrt n$个块,所以总复杂度为$O(C_2\sqrt n+n\sqrt n)$。
综上,这个算法的总复杂度为$O(C_2\sqrt n+n\sqrt n)+O(C_1\sqrt n)=\boxed{O(n\sqrt n+m\sqrt n)}$。
## 代码
```cpp
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=1e5+5;
int n,m,q,B,x[N],t[N],blo[N],cnt[N],tmp[N];
ll ans[N];
struct query{
int l,r,id;
}Q[N];
inline bool cmp(query a,query b){
return (blo[a.l]==blo[b.l])?a.r<b.r:a.l<b.l;
}
inline void add(int v,ll& Ans){
++cnt[v];
Ans=max(Ans,1ll*cnt[v]*t[v]);
}
inline void del(int v){--cnt[v];}
int R[N];
int main(){
scanf("%d%d",&n,&q);
B=sqrt(n);
for(int i=1;i<=n;i++){
scanf("%d",&x[i]);
blo[i]=(i-1+B)/B;
t[++m]=x[i];
}
for(int i=1;i<=blo[n];i++)R[i]=min(i*B,n);
for(int i=1;i<=q;i++){
scanf("%d%d",&Q[i].l,&Q[i].r);
Q[i].id=i;
}
sort(Q+1,Q+q+1,cmp);
sort(t+1,t+m+1);
m=unique(t+1,t+m+1)-t-1;
for(int i=1;i<=n;i++){
x[i]=lower_bound(t+1,t+m+1,x[i])-t;
}
int l=1,r=0,last=0,nl;
ll Ans=0,nans;
for(int i=1;i<=q;i++){
if(blo[Q[i].l]==blo[Q[i].r]){//相同块内暴力
for(int j=Q[i].l;j<=Q[i].r;j++)++tmp[x[j]];
for(int j=Q[i].l;j<=Q[i].r;j++){
ans[Q[i].id]=max(ans[Q[i].id],1ll*tmp[x[j]]*t[x[j]]);
}
for(int j=Q[i].l;j<=Q[i].r;j++)--tmp[x[j]];
continue;
}
if(blo[Q[i].l]!=last){//初始化
while(r>R[blo[Q[i].l]])del(x[r--]);
while(l<R[blo[Q[i].l]]+1)del(x[l++]);
Ans=0;
last=blo[Q[i].l];
}
while(r<Q[i].r)add(x[++r],Ans);
nl=l,nans=Ans;
while(nl>Q[i].l)add(x[--nl],nans);
ans[Q[i].id]=nans;
//回滚
while(nl<l)del(x[nl++]);
}
for(int i=1;i<=q;i++)printf("%lld\n",ans[i]);
return 0;
}
```