莫队算法
用于解决一类区间询问问题的办法
对于一段给定的数据序列,有 $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]*cnt[a];
cnt[a]++;
sum+=cnt[a]*cnt[a];
};
auto del=[&](int x){
sum-=cnt[a]*cnt[a];
cnt[a]--;
sum+=cnt[a]*cnt[a];
};
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$ 指针前后会重复移动:
解决办法时将奇数块的内的 $r$ 按照升序排列,偶数块内的 $r$ 按照降序排列:
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$ 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:
- $Q\ L\ R$ 代表询问你从第 $L$ 支画笔到第 $R$ 支画笔中共有几种不同颜色的画笔。
- $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==1) sum++;
};
auto del=[&](int x){
if (--mp==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--;
}
评论区(暂无评论)