这里是块状数组

1.引入
分块是一种思想,把一个整体划分为若干个等长的小块,对整块整体处理,打标记。零散块暴力处理。可以做到均摊O(n)的询问时间复杂度,总时间复杂度O(nn)的一个优雅的暴力思想
我们将一个长度为n的数组分成len个块,那么每个块的长度就是lenn,一般来说这个len取n,是需要自己用基本不等式去算出来的,但是一般比赛中手算一下理论复杂度,只要块长n能过就行。
为啥非要取n?因为nn=n,显然用基本不等式可以证明
以下是对1~16进行分块,分块实质上就是3层树,第二层就是块,每个块的子树共有n个孩子节点。只不过,块状数组最顶层的信息不用维护。

我们对于一次区间操作,可能有如下的可能
- 我们所操作的区间左右端点刚好在一个块里,这时候我们就可以暴力操作。但是我们要注意如果所修改的信息对块整体有影响块也要更新(例如求和),如下图。但是问题我们怎么左右端点是否在一个区间里呢。很简单我们只需要在预处理每个块的时候记录一下各个端点所属于的块的id就行
![[粘贴的图像 (4).png]]
当然下面这个情况也是属于第一种情况的,这种情况也可以保证复杂度是O(n)

2. 我们所操作的区间左右端点不在一个块,如下图

这种情况我们根据上面所说的定义,散块暴力,大块打tag,橙色就是打tag,绿色代表暴力处理,打tag显然复杂度O(1),两边暴力加起来也不会绿色总长度超过2n

优雅的暴力,虽然他的时间复杂度干不过O(nlog2n)的算法,但是他有一个好处,就是他的信息不需要满足结合律,也不需要一层层地传递标记,它具有更高的灵活性。
根据定义就能够写出以下建块的代码,这里我们以P3374——单点加区间和为例子,我们用sum[i]数组表示第i个块维护的数字和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void init(){ len=sqrt(n); for(int i=1;i<=len;i++){ l[i]=r[i-1]+1; r[i]=i*len; } if(r[len]<n){ r[++len]=n; l[len]=r[len-1]+1; } for(int i=1;i<=len;i++){ for(int j=l[i];j<=r[i];j++){ pos[j]=i; sum[i]+=a[j]; } } }
|
单点加如下,要注意对点修改也会对块维护的信息产生影响
1 2 3 4
| void add(int x,ll k){ a[x]+=k; sum[pos[x]]+=k; }
|
询问如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ll query(int fl,int fr){ int ql=pos[fl],qr=pos[fr]; ll ret=0; if(ql==qr){ for(int i=fl;i<=fr;i++){ ret+=a[i]; } return ret; } for(int i=fl;i<=r[ql];i++){ ret+=a[i]; } for(int i=l[qr];i<=fr;i++){ ret+=a[i]; } for(int i=ql+1;i<qr;i++){ ret+=sum[i]; } return ret; }
|
AC代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include<iostream> #include<cmath> #define endl '\n' #define ll long long using namespace std; const int MN=5e5+15; int n,m; int l[MN],r[MN],pos[MN]; ll a[MN],sum[MN],len; void add(int x,ll k){ a[x]+=k; sum[pos[x]]+=k; } void init(){ len=sqrt(n); for(int i=1;i<=len;i++){ l[i]=r[i-1]+1; r[i]=i*len; } if(r[len]<n){ r[++len]=n; l[len]=r[len-1]+1; } for(int i=1;i<=len;i++){ for(int j=l[i];j<=r[i];j++){ pos[j]=i; sum[i]+=a[j]; } } } ll query(int fl,int fr){ int ql=pos[fl],qr=pos[fr]; ll ret=0; if(ql==qr){ for(int i=fl;i<=fr;i++){ ret+=a[i]; } return ret; } for(int i=fl;i<=r[ql];i++){ ret+=a[i]; } for(int i=l[qr];i<=fr;i++){ ret+=a[i]; } for(int i=ql+1;i<qr;i++){ ret+=sum[i]; } return ret; } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; } init(); while (m--) { int op; ll x,y; cin>>op>>x>>y; if(op==1){ add(x,y); }else cout<<query(x,y)<<endl; } return 0; }
|
2.数列分块
成功解锁了分块,那么现在就来开始做题吧
- 单点加,区间求值,就是P3369
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include<iostream> #include<cmath> #define endl '\n' #define ll long long using namespace std; const int MN=5e5+15; int n,m; int l[MN],r[MN],pos[MN]; ll a[MN],sum[MN],len; void add(int x,ll k){ a[x]+=k; sum[pos[x]]+=k; } void init(){ len=sqrt(n); for(int i=1;i<=len;i++){ l[i]=r[i-1]+1; r[i]=i*len; } if(r[len]<n){ r[++len]=n; l[len]=r[len-1]+1; } for(int i=1;i<=len;i++){ for(int j=l[i];j<=r[i];j++){ pos[j]=i; sum[i]+=a[j]; } } } ll query(int fl,int fr){ int ql=pos[fl],qr=pos[fr]; ll ret=0; if(ql==qr){ for(int i=fl;i<=fr;i++){ ret+=a[i]; } return ret; } for(int i=fl;i<=r[ql];i++){ ret+=a[i]; } for(int i=l[qr];i<=fr;i++){ ret+=a[i]; } for(int i=ql+1;i<qr;i++){ ret+=sum[i]; } return ret; } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; } init(); while (m--) { int op; ll x,y; cin>>op>>x>>y; if(op==1){ add(x,y); }else cout<<query(x,y)<<endl; } return 0; }
|
- 区间加,区间查询小于等于某个数
显然这个我们入手角度就是对于整个块怎么进行处理,我们显然可以发现。“小于等于”,如果暴力就是O(n),但是如果我们以有序数组二分那就是O(log2n)。思想就是对于块要维护有序。对于区间加,对于情况1和情况2的散列块我们需要重新建块进行sort排序。对于查询,我们可以对散列块暴力找,整块二分找。区间加对于大块来说是都加上一个数,不会改变相对大小的顺序所以只需要打tag就可以啦
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| #include<iostream> #include<cmath> #include<algorithm> #include<vector> using namespace std; const int MN=5e4+15,MQ=700; int n; vector<int> bl[MQ]; int l[MQ],r[MQ],pos[MN],tag[MQ],a[MN],len; void init(){ len=sqrt(n); for(int i=1;i<=len;i++){ l[i]=r[i-1]+1; r[i]=i*len; } if(r[len]<n){ r[++len]=n; l[len]=r[len-1]+1; } for(int i=1;i<=len;i++){ for(int j=l[i];j<=r[i];j++){ pos[j]=i; bl[i].push_back(a[j]); } sort(bl[i].begin(),bl[i].end()); } } void bladd(int fl,int fr,int k){ int ql=pos[fl]; for(int i=fl;i<=fr;i++){ a[i]+=k; } bl[ql].clear(); for(int i=l[ql];i<=r[ql];i++){ bl[ql].push_back(a[i]); } sort(bl[ql].begin(),bl[ql].end()); } void add(int fl,int fr,int k){ int ql=pos[fl],qr=pos[fr]; if(ql==qr){ bladd(fl,fr,k); return; } bladd(fl,r[ql],k); bladd(l[qr],fr,k); for(int i=ql+1;i<qr;i++){ tag[i]+=k; } } int query(int fl,int fr,int k){ int ql=pos[fl],qr=pos[fr],ret=0; if(ql==qr){ for(int i=fl;i<=fr;i++){ if(a[i]+tag[ql]<k){ ret++; } } return ret; } for(int i=fl;i<=r[ql];i++){ if(a[i]+tag[ql]<k){ ret++; } } for(int i=l[qr];i<=fr;i++){ if(a[i]+tag[qr]<k){ ret++; } } for(int i=ql+1;i<qr;i++){ ret+=lower_bound(bl[i].begin(),bl[i].end(),k-tag[i])-bl[i].begin(); } return ret; } int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } init(); while (n--) { int op,l,r,k; cin>>op>>l>>r>>k; if(op==0){ add(l,r,k); }else cout<<query(l,r,k*k)<<endl; } return 0; }
|
- 区间加,查询某个数的前继
这个其实和2是一样的,也是二分查找,这里贴一下他人的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| const int inf=1e5+7; int n,len,a[inf]; int bel[inf],L[400],R[400]; int tag[400]; vector<int>h[400]; void baoli(int l,int r,int k) { int in=bel[l]; for(int i=l;i<=r;i++) a[i]+=k; h[in].clear(); for(int i=L[in];i<=R[in];i++) h[in].push_back(a[i]); sort(h[in].begin(),h[in].end()); } void update(int l,int r,int k) { int lin=bel[l],rin=bel[r]; if(lin==rin) { baoli(l,r,k); return; } baoli(l,R[lin],k); baoli(L[rin],r,k); for(int i=lin+1;i<rin;i++) tag[i]+=k; } int query(int l,int r,int k) { vector<int>ls; for(int i=l;i<=r;i++) ls.push_back(a[i]+tag[bel[l]]); sort(ls.begin(),ls.end()); vector<int>::iterator ret=lower_bound(ls.begin(),ls.end(),k); if(ret==ls.begin())return -1; return *--ret; } int ask(int l,int r,int k) { int lin=bel[l],rin=bel[r]; if(lin==rin)return query(l,r,k); int ans=-1; ans=max(ans,query(l,R[lin],k)); ans=max(ans,query(L[rin],r,k)); for(int i=lin+1;i<rin;i++) { vector<int>::iterator ls=lower_bound(h[i].begin(),h[i].end(),k-tag[i]); if(ls==h[i].begin())ans=max(ans,-1); else ans=max(ans,*--ls+tag[i]); } return ans; } int main() { n=re();len=sqrt(n); for(int i=1;i<=n;i++) a[i]=re(); for(int i=1;i<=len;i++) L[i]=R[i-1]+1,R[i]=i*len; R[len]=n; for(int i=1;i<=len;i++) { for(int j=L[i];j<=R[i];j++) bel[j]=i,h[i].push_back(a[j]); sort(h[i].begin(),h[i].end()); } for(int i=1;i<=n;i++) { int op=re(),l=re(),r=re(),k=re(); if(op)wr(ask(l,r,k)),putchar('\n'); else update(l,r,k); } return 0; }
|
- 区间加,区间求和
我会线段树!!!!
用分块也是可以做的,与第一类型的相比来说只需要维护两个tag,一个区间加tag,一个区间和tag就可以啦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| #include<iostream> #include<cmath> #define int long long using namespace std; const int MN=1e5+15,MQ=700; int l[MQ],r[MQ],pos[MN],sum[MQ],tag[MQ],a[MN],n,len; void init(){ len=sqrt(n); for(int i=1;i<=len;i++){ l[i]=r[i-1]+1; r[i]=i*len; } if(r[len]<n){ r[++len]=n; l[len]=r[len-1]+1; } for(int i=1;i<=len;i++){ for(int j=l[i];j<=r[i];j++){ pos[j]=i; sum[i]+=a[j]; } } } void add(int fl,int fr,int k){ int ql=pos[fl],qr=pos[fr]; if(ql==qr){ for(int i=fl;i<=fr;i++){ a[i]+=k; sum[ql]+=k; } return; } for(int i=fl;i<=r[ql];i++){ a[i]+=k; sum[ql]+=k; } for(int i=l[qr];i<=fr;i++){ a[i]+=k; sum[qr]+=k; } for(int i=ql+1;i<qr;i++){ tag[i]+=k; sum[i]+=(r[i]-l[i]+1)*k; } } int query(int fl,int fr,int k){ int ql=pos[fl],qr=pos[fr],ans=0; if(ql==qr){ for(int i=fl;i<=fr;i++){ ans=(ans+a[i]+tag[ql])%k; } return ans; } for(int i=fl;i<=r[ql];i++){ ans=(ans+a[i]+tag[ql])%k; } for(int i=l[qr];i<=fr;i++){ ans=(ans+a[i]+tag[qr])%k; } for(int i=ql+1;i<qr;i++){ ans=(ans+sum[i])%k; } return ans; } signed main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } init(); while (n--) { int op,l,r,k; cin>>op>>l>>r>>k; if(op==0){ add(l,r,k); }else cout<<query(l,r,k+1)<<endl; } return 0; }
|
- 区间开方,区间求和
唉这个题不还是线段树么
P4145
这个题我们不要被1012吓到了,对其一直开根号,操作次数显然可证明为log2log2n,计算一下大约一个264的数开7次就可以开到1或0。解题的关键就是1=1,0=0,只要对块进行检测,如果都是1或都是0就打个tag,之后就不用处理啦
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| #include<iostream> #include<cmath> #define ll long long using namespace std; const int MN=1e5+15,MQ=710; bool isok[MQ]; int l[MQ],r[MQ],pos[MN],n,len,m; ll a[MN],sum[MQ]; void build(){ len=sqrt(n); for(int i=1;i<=len;i++){ l[i]=r[i-1]+1; r[i]=i*len; } if(r[len]<n){ len++; l[len]=r[len-1]+1; r[len]=n; } for(int i=1;i<=len;i++){ for(int j=l[i];j<=r[i];j++){ pos[j]=i; sum[i]+=a[j]; } } } void kuaikai(int x){ if(isok[x]) return; sum[x]=0; isok[x]=1; for(int i=l[x];i<=r[x];i++){ a[i]=sqrt(a[i]); sum[x]+=a[i]; if(a[i]>1) isok[x]=0; } } void kai(int fl,int fr){ int ql=pos[fl],qr=pos[fr]; if(ql==qr){ for(int i=fl;i<=fr;i++){ sum[ql]-=a[i]; a[i]=sqrt(a[i]); sum[ql]+=a[i]; } return; } for(int i=fl;i<=r[ql];i++){ sum[ql]-=a[i]; a[i]=sqrt(a[i]); sum[ql]+=a[i]; } for(int i=l[qr];i<=fr;i++){ sum[qr]-=a[i]; a[i]=sqrt(a[i]); sum[qr]+=a[i]; } for(int i=ql+1;i<qr;i++){ kuaikai(i); } } ll ask(int fl,int fr){ ll ans=0; int ql=pos[fl],qr=pos[fr]; if(ql==qr){ for(int i=fl;i<=fr;i++){ ans+=a[i]; } return ans; } for(int i=fl;i<=r[ql];i++){ ans+=a[i]; } for(int i=l[qr];i<=fr;i++){ ans+=a[i]; } for(int i=ql+1;i<qr;i++){ ans+=sum[i]; } return ans; } int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } build(); cin>>m; while (m--) { int op,x,y; cin>>op>>x>>y; if(x>y) swap(x,y); if(op==0) kai(x,y); else cout<<ask(x,y)<<endl; } return 0; }
|
- 单点修改,单点查询
考虑用vector存块,vector在中间插入的时间复杂度是O(len),但是长度是n,所以修改复杂度其实也是O(n)
代码如下(粘贴自分块九讲)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| const int inf=1e5+7; int n,len,a[inf]; int L[400],R[400]; vector<int>h[inf]; void insert(int id,int k) { for(int i=1;i<=len;i++) { if(id<=h[i].size()) { vector<int>::iterator ls=h[i].begin()+id-1; h[i].insert(ls,k); return; } id-=h[i].size(); } } int ask(int id) { for(int i=1;i<=len;i++) { if(id<=h[i].size()) return *(h[i].begin()+id-1); id-=h[i].size(); } } int main() { n=re();len=sqrt(n); for(int i=1;i<=n;i++) a[i]=re(); for(int i=1;i<=len;i++) L[i]=R[i-1]+1,R[i]=i*len; R[len]=n; for(int i=1;i<=len;i++) for(int j=L[i];j<=R[i];j++) h[i].push_back(a[j]); for(int i=1;i<=n;i++) { int op=re(),l=re(),r=re(),k=re(); if(op)wr(ask(r)),putchar('\n'); else insert(l,r); } return 0; }
|
- 区间众数问题 P4168
我们显然可以考虑用桶来去计数,但是问题是只知道一个块的众数很显然没有用是吧。
这里我们考虑众数的性质本身就是统计数字出现的个数,也就是说对于询问[L,R]区间的众数,其本质可以用[1,R]的数字个数减去[1,L−1]的数字个数
那么就很简单了,我们开一个二位数组定义为zs,zsi,j表示第i个块到第j个块的众数
我们可以使用O(nn)的复杂度预处理众数
既然大块的众数已经解决,那么零散块如何处理,我们显然可以发现众数只会有两种情况,第一种就是在中间的大块,第二种就是在零散块中。
暴力找数可以,但是我们需要找出现次数的话,我们可以考虑用vector存储点的出现顺序,显然这个序列满足单调递增的顺序,我们就可以对左端点进行二分,右端点进行二分,右减去左就能够得到顺序啦
注意数据过大,由于二分我们需要经过离散话才可以
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| #include<iostream> #include<unordered_map> #include<cstring> #include<algorithm> #include<vector> #include<cmath> using namespace std; const int MN=1e5+15,MQ=420; unordered_map<int,int> um,fum; int ls=0;
int n,zs[MQ][MQ],l[MQ],r[MQ],pos[MN],a[MN],len,bok[MN]; int tong[MN]{}; vector<int> numpos[MN]; void build(){ len=sqrt(n); for(int i=1;i<=len;i++){ l[i]=r[i-1]+1; r[i]=i*len; } r[len]=n; for(int i=1;i<=len;i++){ for(int j=l[i];j<=r[i];j++){ pos[j]=i; } } for(int i=1;i<=len;i++){ memset(tong,0,sizeof(tong)); int zsnum=0,maxx=0; for(int j=l[i];j<=n;j++){ tong[a[j]]++; if(maxx<tong[a[j]]||(maxx==tong[a[j]]&&zsnum>a[j])){ zsnum=a[j]; maxx=tong[a[j]]; } zs[i][pos[j]]=zsnum; } } }
int getcishu(int fl,int fr,int k){ auto start=lower_bound(numpos[k].begin(),numpos[k].end(),fl); auto end=upper_bound(numpos[k].begin(),numpos[k].end(),fr); return end-start; } int query(int fl,int fr){ int ql=pos[fl],qr=pos[fr]; int zsnum=0,maxx=0; if(ql==qr){ memset(tong,0,sizeof(tong)); for(int i=fl;i<=fr;i++){ tong[a[i]]++; if(maxx<tong[a[i]]||(maxx==tong[a[i]]&&zsnum>a[i])){ zsnum=a[i]; maxx=tong[zsnum]; } } return bok[zsnum]; } zsnum=zs[ql+1][qr-1]; maxx=getcishu(fl,fr,zsnum); for(int i=fl;i<=r[ql];i++){ int ret=getcishu(fl,fr,a[i]); if(maxx<ret||(maxx==ret&&zsnum>a[i])){ zsnum=a[i]; maxx=ret; } } for(int i=l[qr];i<=fr;i++){ int ret=getcishu(fl,fr,a[i]); if(maxx<ret||(maxx==ret&&zsnum>a[i])){ zsnum=a[i]; maxx=ret; } } return bok[zsnum]; } int main(){ int m; ios::sync_with_stdio(0); cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; bok[i]=a[i]; } sort(bok+1,bok+1+n); int num=unique(bok+1,bok+1+n)-bok-1; for(int i=1;i<=n;i++){ a[i]=lower_bound(bok+1,bok+1+num,a[i])-bok; numpos[a[i]].push_back(i); } build(); int bef=0; for(int i=1;i<=m;i++){ int fl,fr; cin>>fl>>fr; fl=(fl+bef-1)%n+1; fr=(fr+bef-1)%n+1; if(fl>fr) swap(fl,fr); bef=query(fl,fr); cout<<bef<<endl; } return 0; }
|
3.总结
分块是一种思想,不同的题目有不同的做法,具体情况具体分析。分块强调整块的统一处理,算法设计十分重要。
注意块长直接决定你的时间复杂度,如果根号被卡
可以考虑n+1,lgnn,n+常数
YNOI大分块之后再说
引用
- Zvelig1205的分块九讲——link