本文不涉及 Ynoi 大分块。
分块简介
对于一道数据结构题,如果常规思路(如线段树、BIT、ST 表、主席树、平衡树、树套树、K-D Tree 等)不好做,我们可以考虑将序列分成 \(B\) 块,进行区间 修改/询问 的时候散块暴力,整块通过一些奇妙的操作做到更优的复杂度。
\(B\) 一般取 \(\sqrt{n}\) 或者 \(\sqrt{n\log n}\)。
这里给出将序列 \(a\) 分成 \(\texttt{blk}\) 块。第 \(i\) 个块是 \([\texttt{bl}_i,\texttt{br}_i]\)。\(a_i\) 归属块 \(\texttt{bel}_i\)。第 \(i\) 个块有 \(\texttt{bs}_i\) 个元素(即块长)。
blk=分块个数
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++){bel[j]=i;bs[i]++;}
}
约定几个名词:
- 整块:指区间中间的几个完整的块。
- 散块:指区间左右端的非完整块。
- 标记:又称懒标记(Lazy Tag),指修改不方便时,通过一个标记来保存修改状态,等到合适的实际和值进行合并。一般用于线段树中。
例题选讲
洛谷P3374 【模板】树状数组 1
给你一个长为 \(n\) 的序列,有 \(m\) 个操作,支持:
1 x k
将第 \(x\) 个元素加上 \(k\)。2 x y
输出区间 \([x,y]\) 的和。\(1 \leq n,m \leq 5\times10^5\)。
首先对原序列进行分块。我们在维护序列的同时维护每个块的块内和。
对于 1
操作,我们更新原序列和它所属的块的块内和。时间复杂度 \(O(1)\)。
对于 2
操作,如果区间在一个块内我们就直接暴力搞,否则我们把区间分成三段:
- 左边的散块 \([l,\texttt{br}[\texttt{bel}[l]]\) 和右边的散块 \([\texttt{bl}[{\texttt{bel}[r]}],r]\) 我们直接暴力搞。
- 中间的整块,由于是几个完整的块,我们直接将所有块的块内和累加。
时间复杂度 \(O(\frac{n}{B}+B)\),取 \(B=\sqrt{n}\) 最优,为 \(O(\sqrt{n})\)。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5,BL = 5e3+5;
int a[N],bl[BL],br[BL],bel[N],bs[BL],val[BL];
int n,blk,m;
inline void preworks(){
blk=sqrt(n);
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++){bel[j]=i;bs[i]++;}
}
for(int i=1;i<=n;i++) val[bel[i]]+=a[i];
}
inline void update(int p,int v){
a[p]+=v;
val[bel[p]]+=v;
}
inline int query(int l,int r){
if(bel[l]==bel[r]){
return accumulate(a+l,a+r+1,0ll);
}
return accumulate(a+l,a+br[bel[l]]+1,0ll)+accumulate(a+bl[bel[r]],a+r+1,0ll)+accumulate(val+bel[l]+1,val+bel[r],0ll);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
preworks();
while(m--){
int op,x,y;
cin>>op>>x>>y;
if(op==1) update(x,y);
else cout<<query(x,y)<<'\n';
}
return 0;
}
洛谷P3368 【模板】树状数组 2
给你一个长为 \(n\) 的序列,有 \(m\) 个操作,支持:
1 x y k
将位于区间 \([x,y]\) 的元素加上 \(k\)。2 x
输出第 \(x\) 个元素的值。\(1 \leq n,m \leq 5\times10^5\)。
这道题可以在上一道题的分块加上差分完成。但那就没有达到练习的目的了。
区间修改如果维护块内和的话散块和查询都不好搞,我们考虑引入线段树思想。给每一个块上打一个加法标记。修改的时候整块直接打标记,散块暴力改。单点查询的时候将原序列的值加上所在块的标记值即可。
查询时间复杂度 \(O(1)\) ,修改时间复杂度 \(O(\frac{n}{B}+B)\),取 \(B=\sqrt{n}\) 最优,为 \(O(\sqrt{n})\)。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5,BL = 5e3+5;
int a[N],bl[BL],br[BL],bel[N],bs[BL],tag[BL];
int n,blk,m;
inline void preworks(){
blk=sqrt(n);
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++){bel[j]=i;bs[i]++;}
}
}
inline void update(int l,int r,int v){
if(bel[l]==bel[r]){
for(int i=l;i<=r;i++) a[i]+=v;
return;
}
for(int i=l;i<=br[bel[l]];i++) a[i]+=v;
for(int i=bel[l]+1;i<bel[r];i++) tag[i]+=v;
for(int i=bl[bel[r]];i<=r;i++) a[i]+=v;
}
inline int query(int p){
return a[p]+tag[bel[p]];
}
signed main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
preworks();
while(m--){
int op,l,r,v;
cin>>op>>l;
if(op==1) {cin>>r>>v;update(l,r,v);}
else cout<<query(l)<<'\n';
}
return 0;
}
洛谷P1908 逆序对
对于位于 \([1,n]\) 的两个数 \(i,j\)。如果 \(i>j\) 且 \(a_i<a_j\),则称有序数对 \((i,j)\) 是一对逆序对。
给出一个长为 \(n\) 的序列 \(a\)。输出这个序列中有多少对逆序对。
\(1 \leq n \leq 5\times10^5,1\leq a_i \leq 10^9\)
逆序对是一个经典二维偏序问题。一般用树状数组解决。而树状数组能做的,分块当然可以做!
先离散化(貌似可以不离散化,不过我不会)
然后对值域分块,遍历序列,到 \(a_i\) 时将 \(a_i\) 插进值域分块,然后查询 \(i-\sum_{1}^{a_i}\)(即序号比它小,值比它大的元素个数)。累加即可。实现这个功能,只需要实现单点修改区间查询。
时间复杂度 \(O(n\sqrt{n})\)。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5,BL = 5e3+5;
int a[N],bl[BL],br[BL],bel[N],bs[BL],val[BL];
int n,blk,m;
inline void preworks(){
blk=sqrt(n);
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++){bel[j]=i;bs[i]++;}
}
}
inline void update(int p,int v){
a[p]+=v;
val[bel[p]]+=v;
}
inline int query(int l,int r){
if(bel[l]==bel[r]){
return accumulate(a+l,a+r+1,0ll);
}
return accumulate(a+l,a+br[bel[l]]+1,0ll)+accumulate(a+bl[bel[r]],a+r+1,0ll)+accumulate(val+bel[l]+1,val+bel[r],0ll);
}
int rk[N];
struct node{
int v,id;
bool operator<(const node x) const {
return v<x.v;
}
} v[N];
int ans;
signed main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){cin>>v[i].v;v[i].id=i;}
stable_sort(v+1,v+n+1);
for(int i=1;i<=n;i++) rk[v[i].id]=i;
preworks();
for(int i=1;i<=n;i++){update(rk[i],1);ans += (i-query(1,rk[i]));}
cout<<ans;
return 0;
}
洛谷P3372 【模板】线段树 1
给你一个长为 \(n\) 的序列,有 \(m\) 个操作,支持:
1 x y k
将位于区间 \([x,y]\) 的元素加上 \(k\)。2 x y
输出 \([x,y]\) 的和。\(1 \leq n,m \leq10^5\)。
这道题是区间修改区间查询。我们考虑将第一题和第二题的思路结合起来。既维护块内和,也维护加法标记。
对于修改操作,如果在同一块仍然暴力修改,否则对于左右散块暴力改,整块打标记。
对于查询操作,如果在同一块就暴力查询,否则左右散块暴力求和,整块直接用块内和加上标记乘上块长即可。记得暴力求和时需要加上标记。
修改和查询都是 \(O(\frac{n}{B}+B)\) 的,取 \(B=\sqrt{n}\) 最优,为 \(O(\sqrt{n})\)。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5+5,BL = 1e3+5;
int a[N],bl[BL],br[BL],bel[N],bs[BL],val[BL],tag[BL];
int n,blk,m;
inline void preworks(){
blk=sqrt(n);
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++){bel[j]=i;bs[i]++;}
}
for(int i=1;i<=n;i++) val[bel[i]]+=a[i];
}
void update(int l,int r,int v){
if(bel[l]==bel[r]){
for(int i=l;i<=r;i++){a[i]+=v;val[bel[i]]+=v;}
return;
}
for(int i=l;i<=br[bel[l]];i++){a[i]+=v;val[bel[i]]+=v;}
for(int i=bel[l]+1;i<bel[r];i++){tag[i]+=v;}
for(int i=bl[bel[r]];i<=r;i++){a[i]+=v;val[bel[i]]+=v;}
}
int query(int l,int r){
int ret=0;
if(bel[l]==bel[r]){
for(int i=l;i<=r;i++) ret+=(a[i]+tag[bel[i]]);
return ret;
}
for(int i=l;i<=br[bel[l]];i++) ret+=(a[i]+tag[bel[i]]);
for(int i=bel[l]+1;i<bel[r];i++) ret+=(tag[i]*bs[i]+val[i]);
for(int i=bl[bel[r]];i<=r;i++) ret+=(a[i]+tag[bel[i]]);
return ret;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
preworks();
while(m--){
int op,x,y,v;
cin>>op>>x>>y;
if(op==1){cin>>v;update(x,y,v);}
else cout<<query(x,y)<<'\n';
}
return 0;
}
LibreOJ6278 数列分块入门 2
给你一个长为 \(n\) 的序列,有 \(n\) 个操作,支持:
0 l r c
将位于区间 \([l,r]\) 的元素加上 \(c\)。1 l r c
输出 \([l,r]\) 中,有多少个元素小于 \(c^2\)。\(1\leq n\leq5\times10^4\)。
先考虑静态全局版本,如何查询一个序列有多少个元素小于 \(c^2\)?最朴素的思路是挨个找,但是这样子单次询问需要 \(O(n)\) 的时间。我们可以将其排序一遍后,每一次查询只需要二分即可。就可以做到预处理 \(O(n\log n)\),单次询问 \(O(\log n)\)。应该是最优的了。
然后考虑如何查询区间有多少个元素小于 \(c^2\)?我们将原序列分块,然后询问就可以看成散块暴力,整块执行我们刚才讨论的静态全局版本。就可以做到预处理 \(O(n\log B)\),单次询问 \(O(B+\frac{n}{B}\log B)\)。
然后加上修改。同样使用打加法标记法。容易发现整块同时加上一个数,块内排序后顺序不变。因此每次修改只需要将散块所在的两个块重新排序即可。单次修改时间复杂度 \(O(B\log B+\frac{n}{B})\)。
貌似 \(B=\sqrt{n}\) 仍然最优,预处理 \(O(n\log\sqrt{n})\)。单次操作为 \(O(\sqrt{n}\log\sqrt{n})=O(\sqrt{n}\log n)\)。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5,BL = 1e3+5;
int blk,bl[BL],br[BL],bs[BL],bel[N],a[N],add[BL];
vector<int> sorted[BL];
int n,q;
inline void sort(int p){
sorted[p].clear();
for(int i=bl[p];i<=br[p];i++) sorted[p].push_back(a[i]);
stable_sort(sorted[p].begin(),sorted[p].end());
}
inline void preworks(){
blk=sqrt(n);
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++){bel[j]=i;bs[i]++;}
}
for(int i=1;i<=blk;i++) sort(i);
}
inline void update(int l,int r,int v){
if(bel[l]==bel[r]){
for(int i=l;i<=r;i++) a[i]+=v;
sort(bel[l]);return;
}
for(int i=l;i<=br[bel[l]];i++) a[i]+=v;
sort(bel[l]);
for(int i=bl[bel[r]];i<=r;i++) a[i]+=v;
sort(bel[r]);
for(int i=bel[l]+1;i<bel[r];i++) add[i]+=v;
}
inline int query(int l,int r,int c){
c=c*c;
int ans=0;
if(bel[l]==bel[r]){
for(int i=l;i<=r;i++) ans+=(a[i]+add[bel[i]]<c);
return ans;
}
for(int i=l;i<=br[bel[l]];i++) ans+=(a[i]+add[bel[i]]<c);
for(int i=bl[bel[r]];i<=r;i++) ans+=(a[i]+add[bel[i]]<c);
for(int i=bel[l]+1;i<bel[r];i++){
ans+=(lower_bound(sorted[i].begin(),sorted[i].end(),c-add[i])-sorted[i].begin());
}
return ans;
}
signed main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
q=n;
for(int i=1;i<=n;i++) cin>>a[i];
preworks();
while(q--){
int l,r,v;char op;
cin>>op>>l>>r>>v;
if(op=='0') update(l,r,v);
else cout<<query(l,r,v)<<'\n';
}
return 0;
}
LibreOJ6279 数列分块入门 3
给你一个长为 \(n\) 的序列,有 \(n\) 个操作,支持:
0 l r c
将位于区间 \([l,r]\) 的元素加上 \(c\)。1 l r c
输出 \([l,r]\) 中 \(c\) 的前驱。\(1\leq n\leq10^5\)。
注:一个数在一个序列中的前驱 就是序列中 最大的 比这个数小 的数。
和上一题差不多,也是预处理时将整块排序。修改时打标记+散块暴力排序。查询时散块暴力,整块的时候二分,如果没有找到比 \(c\) 小的,否则就找到最大的比 \(c\) 小的。最后将求得的值取最大值即可。
时间复杂度和上一题一样。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5,BL = 1e3+5;
int blk,bl[BL],br[BL],bs[BL],bel[N],a[N],add[BL];
vector<int> sorted[BL];
int n,q;
inline void sort(int p){
sorted[p].clear();
for(int i=bl[p];i<=br[p];i++) sorted[p].push_back(a[i]);
stable_sort(sorted[p].begin(),sorted[p].end());
}
inline void preworks(){
blk=sqrt(n);
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++){bel[j]=i;bs[i]++;}
}
for(int i=1;i<=blk;i++) sort(i);
}
inline void update(int l,int r,int v){
if(bel[l]==bel[r]){
for(int i=l;i<=r;i++) a[i]+=v;
sort(bel[l]);return;
}
for(int i=l;i<=br[bel[l]];i++) a[i]+=v;
sort(bel[l]);
for(int i=bl[bel[r]];i<=r;i++) a[i]+=v;
sort(bel[r]);
for(int i=bel[l]+1;i<bel[r];i++) add[i]+=v;
}
inline int query(int l,int r,int c){
int ans=-1;
if(bel[l]==bel[r]){
for(int i=l;i<=r;i++) {
if(a[i]+add[bel[i]]<c) ans=max(ans,a[i]+add[bel[i]]);
}
return ans;
}
for(int i=l;i<=br[bel[l]];i++) {
if(a[i]+add[bel[i]]<c) ans=max(ans,a[i]+add[bel[i]]);
}
for(int i=bl[bel[r]];i<=r;i++) {
if(a[i]+add[bel[i]]<c) ans=max(ans,a[i]+add[bel[i]]);
}
for(int i=bel[l]+1;i<bel[r];i++){
int v=lower_bound(sorted[i].begin(),sorted[i].end(),c-add[i])-sorted[i].begin();
if(v == 0) continue;
if(sorted[i][v-1]==(c-add[i])) v--;
v = sorted[i][v-1];
ans=max(ans,v+add[i]);
}
return ans;
}
signed main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
q=n;
for(int i=1;i<=n;i++) cin>>a[i];
preworks();
while(q--){
int l,r,v;char op;
cin>>op>>l>>r>>v;
if(op=='0') update(l,r,v);
else cout<<query(l,r,v)<<'\n';
}
return 0;
}
LibreOJ6281 数列分块入门 5
给你一个长为 \(n\) 的自然数序列,有 \(n\) 个操作,支持:
0 l r c
将位于区间 \([l,r]\) 的元素取平方根并向下取整。1 l r c
输出 \([l,r]\) 的和。\(1 \leq n \leq 5\times10^4\)
平方根有一个性质,任意一个自然数在经过一定次数的开方向下取整后,一定是一个小于等于 \(1\) 的自然数。
于是修改操作,我们散块暴力,整块先判断这个块的所有元素是否小于等于 \(1\)(这个可以维护)。如果是就不必暴力了,如果不是就暴力开根。
查询操作就照例维护块内和,散块暴力整块累加块内和即可。
时间复杂度不会算,不过可以通过本题。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5,BL = 5e3+5;
int a[N],bl[BL],br[BL],bel[N],bs[BL],val[BL];
bool one[N];
int n,blk;
inline void preworks(){
blk=sqrt(n);
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++){bel[j]=i;bs[i]++;}
}
for(int i=1;i<=n;i++) val[bel[i]]+=a[i];
}
inline void update(int l,int r){
if(bel[l]==bel[r]){
for(int i=l;i<=r;i++){
val[bel[i]]-=a[i];
a[i]=sqrt(a[i]);
val[bel[i]]+=a[i];
}
return;
}
for(int i=l;i<=br[bel[l]];i++){
val[bel[i]]-=a[i];
a[i]=sqrt(a[i]);
val[bel[i]]+=a[i];
}
for(int i=bel[l]+1;i<bel[r];i++){
if(one[i]) continue;
one[i]=true;
for(int j=bl[i];j<=br[i];j++){
val[i]-=a[j];
a[j]=sqrt(a[j]);
val[i]+=a[j];
if(a[j] > 1) one[i]=false;
}
}
for(int i=bl[bel[r]];i<=r;i++){
val[bel[i]]-=a[i];
a[i]=sqrt(a[i]);
val[bel[i]]+=a[i];
}
}
inline int query(int l,int r){
if(bel[l]==bel[r]){
return accumulate(a+l,a+r+1,0ll);
}
return accumulate(a+l,a+br[bel[l]]+1,0ll)+accumulate(a+bl[bel[r]],a+r+1,0ll)+accumulate(val+bel[l]+1,val+bel[r],0ll);
}
signed main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
preworks();
for(int i=1;i<=n;i++){
int op,l,r,c;
cin>>op>>l>>r>>c;
if(op==0) update(l,r);
else cout<<query(l,r)<<'\n';
}
return 0;
}
LibreOJ6282 数列分块入门 6
给你一个长为 \(n\) 的自然数序列,有 \(n\) 个操作,支持:
0 l r c
将 \(r\) 插入到第 \(l\) 个元素前。1 l r c
输出第 \(r\) 个元素的值。\(1 \leq n \leq 10^5\)
这道题如果直接用 vector
复杂度是 \(O(n^2)\) 的,难以通过本题(实际上无压力跑过)
我们考虑将序列分块,每个块用一个 vector
来维护。插入的时候找到对应块的 vector
插入。但是这样子如果不停地往一个块插入就直接退化成 \(O(n^2)\) 了。我们可以定义一个阈值 \(\alpha\),在每次插入后判断如果当前块长大于预期块长的 \(\alpha\) 倍,就重构整个序列。询问就直接找到对应的块即可。
\(\alpha\) 取 \(7\) 最优。
时间复杂度期望 \(O(n\sqrt{n})\)。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int BL = 500;
const int alpha=7;
vector<int> arr[BL];
int n,blk;
void build(){
vector<int> a;
a.push_back(0);
for(int i=1;i<=blk;i++){
for(int j:arr[i]) a.push_back(j);
arr[i].clear();
}
blk=sqrt(n);
vector<int> bl(blk+1),br(blk+1);
for(int i=1;i<=blk;i++){bl[i]=n/blk*(i-1)+1;br[i]=n/blk*i;}
br[blk]=n;
for(int i=1;i<=blk;i++){
for(int j=bl[i];j<=br[i];j++) arr[i].push_back(a[j]);
}
}
pair<int,int> getpos(int p){
int vec=1,cursor=0;
for(;cursor+arr[vec].size()<p;cursor+=arr[vec].size(),vec++);
return make_pair(vec,p-cursor-1);
}
inline void insert(int p,int v){
pair<int,int> ppos=getpos(p);
arr[ppos.first].insert(arr[ppos.first].begin()+getpos(p).second,v);
n++;
if(arr[ppos.first].size()>(alpha*(n/blk))) build();
}
inline int query(int p){
pair<int,int> ppos=getpos(p);
return arr[ppos.first][ppos.second];
}
signed main(){
cin>>n;
blk=1;
for(int i=1;i<=n;i++){
int v;cin>>v;
arr[blk].push_back(v);
}
build();
const int m = n;
for(int i=1;i<=m;i++){
int op,l,r,c;
cin>>op>>l>>r>>c;
if(op==0) insert(l,r);
else cout<<query(r)<<'\n';
}
return 0;
}
标签:入门,分块,int,sqrt,BL,blk,序列,op
From: https://www.cnblogs.com/zheyuanxie/p/blocking-array-thresold.html