分块
分块
特点:一种优雅的暴力,大段维护,小段朴素。
假设我们有一个长度为 的数组 。
需要我们维护区间修改和区间查询等操作。
那么朴素算法就不用说了,如果是万能的线段树还行。但是线段树码量过大,容易出bug。
这个时候就得用到分块的思想。
分块思想:
对于需要维护的数组 ,将其分为 块,每一块块长为 ,然后对每一块进行维护,最后求解。
那么这个 是多少好呢?
根据均值不等式,当 ,即当 时最优,单次修改为 。
显然有些情况下,,即分完会有剩下的元素。那么很显然可以将块数 ,就当作是新建一块。
对于每个块的维护
可以建立一个结构体来存放每一个快。
内存块的左端点、右端点、区间和。如果题目中有区间修改,那么类似的,放一个懒标记进去,表示该区间所有数都要加上多少。
对于第 块 :
左端点 ,右端点是 。因为有可能 。
下面就是一个例子:
其中:
对于每个下标 ,用 表示第 号元素属于第 块。
操作维护
设 分别是增加区间的左右端点,增加 。
若 ,即在同一个块中:
将所有的 加上 ,再将 。(小段朴素)
否则,设 :
对于区间 ,用同一个块中的方法进行维护。
即维护下图的区间 。
同理,也可以维护区间 ,即上图区间 。
现在我们已经解决了区间 和 ,还剩下在两个区间中间的区间 和 。
显然 和 是两个块,就将块的懒标记全部加上 即可。
若不用懒标记,像小区间那样的朴素维护,很容易时间就超了。
代码部分
例题:P3374 【模板】树状数组 1。
一个块的元素:
struct NODE{ ljl l,r,sum; }node[N];
建立块:
void build() { block=floor(sqrt(n)); num=n/block; if(n%block!=0) ++num;//不能分为整块 for(ljl i=1;i<=num;++i) { node[i].l=(i-1)*block+1,node[i].r=min(i*block,n);//公式已说 for(ljl j=node[i].l;j<=node[i].r;++j) node[i].sum+=a[j]; } // for(ljl i=1;i<=num;++i) // cout<<node[i].l<<' '<<node[i].r<<" "<<node[i].sum<<'\n'; for(ljl i=1;i<=n;++i) belong[i]=(i-1)/block+1; return; }
单点修改:
void update(ljl x,ljl y) { a[x]+=y; node[belong[x]].sum+=y; return; }
区间查询:
ljl query(ljl l,ljl r) { ljl q=belong[l],p=belong[r]; if(q==p) { ljl ans=0; for(ljl i=l;i<=r;++i) ans+=a[i]; return ans; } ljl ans=0; for(ljl i=l;i<=node[q].r;++i) ans+=a[i]; for(ljl i=q+1;i<=p-1;++i) ans+=node[i].sum; for(ljl i=node[p].l;i<=r;++i) ans+=a[i]; return ans; }
完整代码:
#include<bits/stdc++.h> using namespace std; #define ljl long long const ljl N=5e5+5; ljl n,m,a[N],block; namespace BLOCK{ struct NODE{ ljl l,r,sum; }node[N]; ljl belong[N],num; void build() { block=floor(sqrt(n)); num=n/block; if(n%block!=0) ++num; for(ljl i=1;i<=num;++i) { node[i].l=(i-1)*block+1,node[i].r=min(i*block,n); for(ljl j=node[i].l;j<=node[i].r;++j) node[i].sum+=a[j]; } // for(ljl i=1;i<=num;++i) // cout<<node[i].l<<' '<<node[i].r<<" "<<node[i].sum<<'\n'; for(ljl i=1;i<=n;++i) belong[i]=(i-1)/block+1; return; } void update(ljl x,ljl y) { a[x]+=y; node[belong[x]].sum+=y; return; } ljl query(ljl l,ljl r) { ljl q=belong[l],p=belong[r]; if(q==p) { ljl ans=0; for(ljl i=l;i<=r;++i) ans+=a[i]; return ans; } ljl ans=0; for(ljl i=l;i<=node[q].r;++i) ans+=a[i]; for(ljl i=q+1;i<=p-1;++i) ans+=node[i].sum; for(ljl i=node[p].l;i<=r;++i) ans+=a[i]; return ans; } } using namespace BLOCK; int main(){ ios::sync_with_stdio(0); cin>>n>>m; for(ljl i=1;i<=n;++i) cin>>a[i]; build(); // for(ljl i=1;i<=n;++i) // cout<<belong[i]<<' '; // cout<<"\n"; while(m--) { ljl op,x,y; cin>>op>>x>>y; if(op==1) update(x,y); else { cout<<query(x,y)<<'\n'; } // cout<<"--------------\n"; // for(ljl i=1;i<=num;++i) // cout<<node[i].l<<" "<<node[i].r<<" "<<node[i].sum<<'\n'; // cout<<"----------------\n"; } return 0; }
例题二:P3372 【模板】线段树 1。
因为上一题仅仅是单点修改,过于简单。
所以上强度:
本题需要区间修改和区间查询,具体看代码:
#include<bits/stdc++.h> using namespace std; #define ljl long long const ljl N=1e5+5,BLOCKS=1e3+5; ljl n,a[N],q,l,r,v; namespace BLOCK{ struct NODE{ ljl l,r,sum,lz; }node[BLOCKS]; ljl belong[N],num,block; void build()//建块 { block=(ljl)sqrt(n); num=n/block; if(n%block)++num; for(ljl i=1;i<=num;++i) { node[i].l=(i-1)*block+1,node[i].r=min(i*block,n); for(ljl j=node[i].l;j<=node[i].r;++j) node[i].sum+=a[j]; } for(ljl i=1;i<=n;++i) belong[i]=(i-1)/block+1; return; } void change_part(ljl id,ljl l,ljl r)//对于是一个块内的区间修改 { for(ljl i=l;i<=r;++i) a[i]+=v; node[id].sum+=v*(r-l+1); } void change(ljl l,ljl r)//询问的区间修改 { ljl q=belong[l],p=belong[r]; if(q==p) { change_part(q,l,r);return; } change_part(q,l,node[q].r);//按照上文讲过的方法实现即可 change_part(p,node[p].l,r); for(ljl i=q+1;i<p;++i) node[i].lz+=v;//这里是懒标记 return; } ljl query_part(ljl id,ljl l,ljl r) { ljl ans=0; for(ljl i=l;i<=r;++i) ans=ans+(a[i]+node[id].lz); return ans; } ljl query(ljl l,ljl r) { ljl q=belong[l],p=belong[r]; if(q==p) return query_part(q,l,r); ljl ans=0; ans+=query_part(q,l,node[q].r); ans+=query_part(p,node[p].l,r); for(ljl i=q+1;i<p;++i) ans=ans+node[i].sum+(node[i].r-node[i].l+1)*node[i].lz; return ans; } } using namespace BLOCK; int main(){ ios::sync_with_stdio(0); cin>>n>>q; for(ljl i=1;i<=n;++i) cin>>a[i]; build(); while(q--) { ljl op; cin>>op; if(op==1) { cin>>l>>r>>v; change(l,r); } else { cin>>l>>r; cout<<query(l,r)<<'\n'; } } return 0; }
线段树和分块的比较:
线段树1:
时间 | 空间 | 代码长度 | |
---|---|---|---|
分块 | 974ms | 1.98MB | 1.48KB |
线段树 | 297ms | 11.19MB | 1.67KB |
树状数组1:
时间 | 空间 | 代码长度 | |
---|---|---|---|
分块 | 1.34s | 9.55MB | 1.40KB |
树状数组 | 521ms | 4.23MB | 554B |
虽然分块不如线段树或树状数组,但是在不熟练他们俩的情况下可以使用分块,反正能A。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)