莫队与分块学习笔记
分块思想
介绍
分块是一种思想,而不是一种数据结构。
思想就是,将一块大的区间,转换成小的区间来处理。例如,在一个 \(n\) 长度上的数轴,我们可以将其分成 \(\sqrt n\) 个长度为 \(\sqrt n\) 的块来解决。
典型问题
对于一类很典型的问题,可以用分块来做。
- 单点修改,区间查询
这玩意咋做?
线段树来搞 \(O(q \log n)\),没错,分块他也可以!但是 \(O(q \sqrt n)\)。
我们先与处理一下,把每一个点分到一个他所属的块编号 \(id_i\)。
我们实时维护着两个数组,\(a\) 和 \(b\),\(a\) 代表的是每一个单独的点它的数值,\(b\) 代表块 \(i\) 它的值(总和)。
-
对于单点修改
这个操作,我们先把单独点的值修改,\(a_i + x\)。它对应的块也要加对应的值。\(O(1)\)。 -
对于区间查询操作
我们要找查询中每一个完整的块,然后将其累加起来,然后再暴力找完整块之外的散块 ,加起来,那么时间复杂度也是 \(O(\sqrt n)\)。因为块的长度最多是 $ \sqrt n$,最多只有 $ \sqrt n$ 个块,散块最多 \(2 \times \sqrt n\)。
单点查询也不用说,区间修改的话,也是类似区间查,直接处理一下整块的信息,然后再处理散块的信息,最后,就是一个 $ \sqrt n$ 的时间复杂度。
具体散块和整块的一些细节就不多说,大概就是那样。主要是这种思想。
例题
洛谷P3203
对于这玩意我们可以分块。很容易发现,如果只修改这个点,那么受影响的最多也就是他的这个块,那么我们就直接模拟这个块内,重新跳,如果已经跳到了其他块,那么我们就不用管了,我们直接接着其他块的值,往前即可。然后暴力的把他求出来。所以时间复杂度是 \(O(q \sqrt n)\)。比较抽象(巧妙)的一题,就是运用了分块的思想。巧妙地将暴力转换成了根号级别。与根号分治有异曲同工之妙。
int id[N];//块编号
int l[N],r[N];//块的左端点右端点
int n,q,a[N];
int p[N],s[N];
void sol(int x,int y){
for(int i = y;i >= x;i--){
if(a[i] + i > r[id[i]]) p[i]=i+a[i],s[i]=1;//跳到了块外
else p[i]=p[i+a[i]],s[i]=s[i+a[i]] + 1;
}
}
void init(){
int klen = sqrt(n);//块长
for(int i=1;i <=n;i++) id[i] = (i-1)/klen + 1;
for(int i=1;i <= id[n];i++) l[i] = (i-1)*klen + 1,r[i] = i*klen;
r[id[n]] = n;//最右边特判,防止越界了
sol(1,n);//处理一下 1-n (可以理解为更新
}
int query(int x){
int ans=0;while(x <= n) ans += s[x],x=p[x];
return ans;
}
void solve(){
cin>>n;for(int i = 1;i <= n;i++)cin>>a[i];
init();cin>>q;
while(q--){
int op;cin>>op;
if(op==1){int x;cin >>x;x++;cout<<query(x)<<endl;}
else{int x,y;cin>>x>>y;x++;a[x]=y;sol(l[id[x]],r[id[x]]);}
}
}
莫队
莫队是一个离线算法 + 暴力 + 分块,他可以处理区间内的一些操作。然后他会将每一个查询离线下来,保证,这些查询中都只有查询,没有修改。如果有修改,那么就得要上带修莫队。
普通莫队
我们认为的板子题 :洛谷 P2709
接下来,我们就要讲的是莫队算法的过程,要点。
对于每一个区间 \([l,r]\),他都可以在 \(O(1)\) 时间复杂度内从 \([l,r+1]\),\([l,r-1]\),\([l-1,r]\),\([l+1,r]\)。我们可以将 \(l\) 和 \(r\) 看作 xy 轴,画在一个平面图上,例如,看下图:
图中每一个圆的点都是一个询问,而这些询问所代表的就是 \(l\) 和 \(r\) 映射在图上的点。那么,红色的线条,就是每两个询问之间所要花的步数,你可以理解为时间复杂度。
对于这上面的点,是询问初始的顺序。我们可以考虑将他最小化吗?可以是可以,但是,这是旅行商问题,自行百度,这玩意只能用状压去搞,你要遍历所有的点,然后最短路,显然还是一个非常困难的问题。
那么,既然我们没办法保证x y轴完全最小化,我们是不是可以保证一个轴最小化,就是一个轴有序递增,这时候用分块,在一个块内,y轴是递增的,x轴随便。这样,我们就会得到下图:
这样,我们在一个块内,就会代价很小,但是,块与块之间的代价却有点大,最多可以卡到 \(n\)。
于是,推出了 奇偶优化,如何奇偶优化呢?就是,分奇偶,就搞出来这个是递增,下一个是递减,这样,就有效程度上减少了这些代价,图变成了这样:
现在,来分析一下时间复杂度。
我们为什么要分块?因为我们学了分块。如果不分块,只对一个维度排序,会怎样?可以想象,这样,我们就会有可能另一个维度一下 -inf,一下inf ,这样,直接卡飞。如果说 \(l\),\(r\) 都是在 \(n\) 范围内,那么显然,这样的时间复杂度卡成 \(O(n^2)\)。那如果分块了呢?(先不考虑奇偶优化)。一个块内,排序坐标最多 \(O(n)\),非排序坐标 \(O(\sqrt n)\) 个,每一个 \(O(\sqrt n)\),然后单个块 \(O(n)\) ,因此最慢也只是被卡到 \(O(n \sqrt n)\)。奇偶优化主要还是卡卡常数,最多就是把块之间的 \(O(n)\) 优化,最多最多优化 \(O(n \sqrt n)\),实际上肯定没那么多,毕竟,只要理论时间复杂度没问题,不卡常的话一般也是能过得。(卡常就是奇偶优化)
P2709 的代码:
int n,m,k,a[N],id[N],res[N],ans,ot[N];
struct node{int x,y,id;}s[N];
void init(){
int s=sqrt(n);
for(int i = 1;i <= n;i++){id[i] = (i-1)/s+1;}
}bool cmp(node a,node b){
if(id[a.x] == id[b.x]){
if(id[a.x]&1) return a.y>b.y;
return a.y<b.y;
}return id[a.x] < id[b.x];
}void add(int x){ans += 2*res[a[x]]+1;/*完全平方公式*/res[a[x]]++;}
void del(int x){ans -= 2*res[a[x]]-1;res[a[x]]--;}
void solve(){
cin>>n>>m>>k;
for(int i = 1;i <= n;i++) cin >> a[i];
init();
for(int i = 1;i <= m;i++){
cin >> s[i].x >> s[i].y;s[i].id=i;
}sort(s+1,s+1+m,cmp);
int x=1,y=0;
for(int i = 1;i <= m;i++){
while(s[i].x < x) add(--x);
while(x < s[i].x) del(x++);
while(s[i].y < y) del(y--);
while(y < s[i].y) add(++y);
ot[s[i].id]=ans;
}for(int i = 1;i <= m;i++) {
cout<<ot[i]<<endl;
}
}
带修莫队
普通的莫队相当于把时间戳打乱了,所以无法进行修改操作,然而,待修改莫队可以完成”修改操作“的莫队。
如何实现修改呢?当然选用最暴力的方法,多开一个时间戳的维度即可。
对于普通莫队,从 这个点转移到 下一个点,仅仅需要改动 \(l\) 与 \(r\) 即可。而在带修莫队中,你还需要遍历多一维,进行时间戳上的转移。修改的话,直接另开数组存储即可。
对于块长也不再是 \(\sqrt n\),而是 \(n^{\tfrac{2}{3}}\),这样会更优,具体的证明需要求导,具体看 OI wiki,这里不展开讲。
对于排序的话,以左端点所在块为第一关键字,右端点第二关键字,时间戳第三关键字排序。
然后还是暴力去转移即可。
int n,m;
int len ;//块长
int a[N];
struct query{
int id,delid,l,r;
}q[N];
struct del{
int id,x;
}d[N];
int qn,dn;
int id[N];
bool cmp(query a,query b){
if(id[a.l] != id[b.l]) return id[a.l] < id[b.l];//左端点排序
if(id[a.r] != id[b.r]) return id[a.r] < id[b.r];//右端点排序
return a.delid < b.delid;//按照时间戳排序
}int cnt[M],now;
int ans[N];
void add(int x){if(!(cnt[x]++)) now++;}
void dl(int x){if(!(--cnt[x])) now--;}
void md(){
for(int i = 1;i <= n;i++) id[i] = (i-1)/len + 1;
sort(q+1,q+1+qn,cmp);
int l=1,r=0,tim=0;
for(int i = 1;i <= n;i++){
while(r>q[i].r) dl(a[r--]);
while(r<q[i].r) add(a[++r]);
while(l>q[i].l) add(a[--l]);
while(l<q[i].l) dl(a[l++]);
while(tim<q[i].delid){
tim++;
if(d[tim].id >= l && d[tim].id <= r){
add(d[tim].x);
dl(a[d[tim].id]);
}swap(a[d[tim].id],d[tim].x);//取反,以后还要改直接改就行
}
while(tim>q[i].delid){
if(d[tim].id >= l && d[tim].id <= r){
add(d[tim].x);
dl(a[d[tim].id]);
}swap(a[d[tim].id],d[tim].x);//取反,以后还要改直接改就行
tim--;
}
ans[q[i].id] = now;
}
}
void solve(){
cin>>n>>m;
len = pow(n,2.0/3.0);
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=m;i++){
char c;
int x,y;
cin >> c >> x >> y;
if(c=='Q'){
q[++qn]={qn,dn,x,y};
}else{
d[++dn]={x,y};
}
}
md();
for(int i = 1;i <= qn;i++){
cout<<ans[i]<<endl;
}
}
本文来自博客园,作者:gsczl71,转载请注明原文链接:https://www.cnblogs.com/gsczl71/p/18053062
gsczl71 AK IOI!RP = INF 2024年拿下七级勾!