【优雅的区间问题暴力】莫队算法
对dalao口中可以\(O(n^{\frac{3}{2}})\)区间内绝大部分无修改离线问题的莫队算法,一直处于“好骑”的状态,最近终于找到了学习的机会,其实感觉,这着实是一个优雅的暴力。
莫队算法的大前提,是可以利用已知的[l,r]内的答案,直接得到[l,r+1],[l+1,r],[l-1,r],[l,r-1]上的答案。
以下内容默认上述转移为O(1)的。
在此基础上,显然的,作为两次询问[l,r],[l',r']之间的转移,时间花费为O(|l-l'|+|r-r'|)。
我们将每次询问看做二维平面上的点,则所有询问间转移的花费就为在沿着这个平面上最小直线斯坦纳树做的花费,不管你会不会,反正我是不会这种做法。
因此,我们有个良好的替代品,分块。
将每个询问按照[l->pos,r]的顺序依次排序,l->pos指的是l属于的区块编号,按照排序后的顺序去做,显然是相对较优的做法之一。
分析一下时间复杂度:
考虑分块大小为x的情况:
考虑左指针移动次数:
若在块内移动,每次最多移动\(O(x)\)次,最多q次,时间复杂度为\(O(xq)\);
若在块外移动,每次最多移动\(O(n/x+x)\)次,最多从块1移动到块n/x,每次最多加上x次块内移动,时间复杂度为O(n).
当q较大时,总复杂度可以近似看为\(O(xq)\).
考虑右指针移动次数:
若在块内移动,每次最多移动\(O(x)\)次,最多q次,时间复杂度为\(O(xq)\);
考虑块外移动,最坏情况由n到1,总共经过n/x块,最坏时间复杂度为\(O(nq/x)\);
总时间复杂度视x不定,但当x取\(n^{\frac{1}{2}}\)时,最坏时间复杂度最稳定,稳定于\(O(qn^{\frac{1}{2}})\);
结合上述论述可以发现,为了保证右端点的移动的时间复杂度稳定,x取\(O(n^{\frac{1}{2}})\)是最优的选择,故总时间复杂度为\(O(qn^{\frac{1}{2}})\)。
当n与q为同数量级时,可近似看为\(O(n^{\frac{3}{2}})\)。
接下来贴一下模板,模板题传送门。
#include <math.h>
#include <stdio.h>
#include <algorithm>
#define R register
#define MN 50005
#define ll long long
int n,m,cnt[MN],col[MN];ll ans[MN],sum;
struct Query{
int l,r,id,pos;
inline bool operator <(Query &x)const{
return pos<x.pos||(x.pos==pos&&r<x.r);
}
}query[MN];
inline int read(){
R int x; R char c; R bool f;
for (f=0; (c=getchar())<'0'||c>'9'; f=c=='-');
for (x=c-'0'; (c=getchar())>='0'&&c<='9'; x=(x<<3)+(x<<1)+c-'0');
return f?-x:x;
}
int main(){
n=read(),m=read();read();
R int size=(int)sqrt(n);
for (R int i=1; i<=n; ++i) col[i]=read();
for (R int i=1; i<=m; ++i){
query[i].l=read(),query[i].r=read(),query[i].id=i;
query[i].pos=(query[i].l-1)/size+1;
}std::sort(query+1,query+m+1);
for (R int i=1,l=1,r=0; i<=m; ++i){
while (l>query[i].l) sum+=(++cnt[col[--l]]<<1)-1;
while (r<query[i].r) sum+=(++cnt[col[++r]]<<1)-1;
while (l<query[i].l) sum-=(--cnt[col[l++]]<<1)+1;
while (r>query[i].r) sum-=(--cnt[col[r--]]<<1)+1;
ans[query[i].id]=sum;
}for (R int i=1; i<=m; ++i) printf("%lld\n",ans[i]);
}