分块
分块——优雅的暴力
分块算法的思想是通过适当的划分,预处理一部分信息保存下来,用空间换取时间,达到时空平衡。
分块实现的基本框架:
划分块(一般将其分为\(\sqrt n\)块,每块也有\(\sqrt n\)个元素),预处理,操作或查询。
操作或查询通常为4步:
1.判断要操作或是查询的区间是否在一个块内
2.若在一个块内,暴力操作或查询 \(O(\sqrt n)\)
3.若不在一个块内,将除了最左边和最右边这两个角块外 其余的块 进行整体的操作,即直接对块打上修改标记之类的 \(O(\sqrt n)\)
4.单独暴力处理最左边的块和最右边的角块 \(O(\sqrt n)\)
一些代码含义
b[] —— belong 第\(i\)个数所在块的编号——我们想把\([1,size]\)分在一个块里,所以 是 \((i-1)/size+1\) ;
\(size\) 块大小
对于一个编号为\(k\)的块,其表示的范围为\([(k-1)*size+1,k*size]\)
分块入门 1
区间加法,单点查值
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define N 50005
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int n,a[N],tag[N],op,l,r,c;
int size,b[N];
void add(int l,int r,int c) {
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
a[i]+=c;
}else {
for(int i=l;i<=b[l]*size;i++)
a[i]+=c;
for(int i=b[l]+1;i<=b[r]-1;i++)
tag[i]+=c;
for(int i=(b[r]-1)*size+1;i<=r;i++)
a[i]+=c;
}
}
int main() {
n=read();
size=sqrt(n);
for(int i=1;i<=n;i++)
a[i]=read(),b[i]=(i-1)/size+1;
for(int i=1;i<=n;i++) {
op=read();l=read();r=read();c=read();
if(op==0) add(l,r,c);
if(op==1) printf("%d\n",a[r]+tag[b[r]]);
}
return 0;
}
分块入门 2
区间加法,询问区间内小于某个值 的元素个数
我们先来思考只有询问操作的情况,角块枚举统计即可;而要在每个整块内寻找小于一个值的元素数,于是我们不得不要求块内元素是有序的,这样就能使用二分法对块内查询,需要预处理时每块做一遍排序,复杂度\(O(nlogn)\),每次查询在\(\sqrt n\)个块内二分,以及暴力\(2\sqrt n\)个元素,总复杂度\(O(nlogn + n\sqrt nlog\sqrt n)\)。
那么区间加呢?
套用第一题的方法,维护一个加法标记,略有区别的地方在于,角块修改后可能会使得该块内数字乱序,所以头尾两个角块需要重新排序
在加法标记下的询问操作,块外还是暴力,查询小于(x – 加法标记)的元素个数,块内用(x – 加法标记)作为二分的值即可。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<vector>
#include<algorithm>
using namespace std;
#define N 500005
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int n,a[N],tag[N],op,l,r,c;
int size,b[N];
vector<int>v[5005];
void reset(int x) {
v[x].clear();
for(int i=(x-1)*size+1;i<=min(x*size,n);i++)
v[x].push_back(a[i]);
sort(v[x].begin(),v[x].end());
}
void add(int l,int r,int c) {
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
a[i]+=c;
reset(b[l]);
} else {
for(int i=l;i<=b[l]*size;i++)
a[i]+=c;
reset(b[l]);
for(int i=b[l]+1;i<=b[r]-1;i++)
tag[i]+=c;
for(int i=(b[r]-1)*size+1;i<=r;i++)
a[i]+=c;
reset(b[r]);
}
}
int query(int l,int r,int c) {
int ans=0;
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
if(a[i]+tag[b[i]]<c) ans++;
return ans;
} else {
for(int i=l;i<=b[l]*size;i++)
if(a[i]+tag[b[i]]<c) ans++;
for(int i=b[l]+1;i<=b[r]-1;i++) {
int x=c-tag[i];
ans+=lower_bound(v[i].begin(),v[i].end(),x)-v[i].begin();
}
for(int i=(b[r]-1)*size+1;i<=r;i++)
if(a[i]+tag[b[i]]<c) ans++;
return ans;
}
}
int main() {
n=read();
size=sqrt(n);
for(int i=1;i<=n;i++) {
a[i]=read();
b[i]=(i-1)/size+1;
v[b[i]].push_back(a[i]);
}
for(int i=1;i<=b[n];i++)
sort(v[i].begin(),v[i].end());
for(int i=1;i<=n;i++) {
op=read();l=read();r=read();c=read();
if(op==0) add(l,r,c);
if(op==1) printf("%d\n",query(l,r,c*c));
}
return 0;
}
一道考试题
很类似
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<vector>
using namespace std;
#define N 1000005
int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
vector<int> v[1005];
int n,size,m;
int a[N],b[N],tag[N];
void reset(int x){
v[x].clear();
for(int i=(x-1)*size+1;i<=min(x*size,n);i++)
v[x].push_back(a[i]);
sort(v[x].begin(),v[x].end());
}
void add(int l,int r,int c){
for(int i=l;i<=min(r,b[l]*size);i++)
a[i]+=c;
reset(b[l]);
if(b[l]!=b[r]){
for(int i=(b[r]-1)*size+1;i<=r;i++)
a[i]+=c;
reset(b[r]);
}
for(int i=b[l]+1;i<=b[r]-1;i++)
tag[i]+=c;
}
int query(int l,int r,int c){
int ans=0;
for(int i=l;i<=min(b[l]*size,r);i++)
if(a[i]+tag[b[l]]>=c)ans++;
if(b[l]!=b[r])
for(int i=(b[r]-1)*size+1;i<=r;i++)
if(a[i]+tag[b[r]]>=c)ans++;
for(int i=b[l]+1;i<=b[r]-1;i++){
int x=c-tag[i];
ans+=size-(lower_bound(v[i].begin(),v[i].end(),x)-v[i].begin());
}
return ans;
}
int main(){
n=read();m=read();size=sqrt(n);
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=n;i++){
b[i]=(i-1)/size+1;
v[b[i]].push_back(a[i]);
}
for(int i=1;i<=b[n];i++)
sort(v[i].begin(),v[i].end());
for(int i=1;i<=m;i++){
char f;cin>>f;
int x=read(),y=read(),z=read();
if(f=='M')add(x,y,z);
if(f=='A')printf("%d\n",query(x,y,z));
}
return 0;
}
分块入门 3
区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
n<=100000其实是为了区分暴力和一些常数较大的写法
不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个 set ,这样如果还有插入、删除元素的操作,会更加的方便。
#include<set>
#include<cmath>
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
#define N 100005
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
set<int>s[505];
int size,b[N];
int n,a[N],tag[N],op,l,r,c;
void add(int l,int r,int c) {
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
s[b[i]].erase(a[i]),a[i]+=c,s[b[i]].insert(a[i]);
}else {
for(int i=l;i<=b[l]*size;i++)
s[b[i]].erase(a[i]),a[i]+=c,s[b[i]].insert(a[i]);
for(int i=b[l]+1;i<=b[r]-1;i++)
tag[i]+=c;
for(int i=(b[r]-1)*size+1;i<=r;i++)
s[b[i]].erase(a[i]),a[i]+=c,s[b[i]].insert(a[i]);
}
}
int query(int l,int r,int c) {
int ans=-1;
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
if(a[i]+tag[b[i]]<c)
ans=max(ans,a[i]+tag[b[i]]);
return ans;
}else {
for(int i=l;i<=b[l]*size;i++)
if(a[i]+tag[b[i]]<c)
ans=max(ans,a[i]+tag[b[i]]);
for(int i=b[l]+1;i<=b[r]-1;i++) {
int x=c-tag[i];
set<int>::iterator it=s[i].lower_bound(x);
if(it==s[i].begin()) continue;
--it;
ans=max(ans,*it+tag[i]);
}
for(int i=(b[r]-1)*size+1;i<=r;i++)
if(a[i]+tag[b[i]]<c)
ans=max(ans,a[i]+tag[b[i]]);
return ans;
}
}
int main() {
n=read();
size=1000;
for(int i=1;i<=n;i++) {
a[i]=read();
b[i]=(i-1)/size+1;
s[b[i]].insert(a[i]);
}
for(int i=1;i<=n;i++) {
op=read();l=read();r=read();c=read();
if(op==0) add(l,r,c);
if(op==1) printf("%d\n",query(l,r,c));
}
return 0;
}
分块入门 4
区间加法,区间求和
so easy ~ 开long long
#include<cmath>
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
#define int long long
#define N 100005
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int size,b[N];
int n,a[N],tag[N],op,l,r,c,sum[N];
bool flag[N];
void add(int l,int r,int c) {
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
sum[b[i]]+=c,a[i]+=c;
} else {
for(int i=l;i<=b[l]*size;i++)
sum[b[i]]+=c,a[i]+=c;
for(int i=b[l]+1;i<=b[r]-1;i++)
tag[i]+=c;
for(int i=(b[r]-1)*size+1;i<=r;i++)
sum[b[i]]+=c,a[i]+=c;
}
}
int query(int l,int r,int c) {
int ans=0;
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
ans=(ans+a[i]+tag[b[i]])%c;
return ans;
} else {
for(int i=l;i<=b[l]*size;i++)
ans=(ans+a[i]+tag[b[i]])%c;
for(int i=b[l]+1;i<=b[r]-1;i++) {
ans=(ans+tag[i]*size+sum[i])%c;
}
for(int i=(b[r]-1)*size+1;i<=r;i++)
ans=(ans+a[i]+tag[b[i]])%c;
return ans;
}
}
signed main() {
n=read();
size=sqrt(n);
for(int i=1;i<=n;i++) {
a[i]=read();
b[i]=(i-1)/size+1;
sum[b[i]]+=a[i];
}
for(int i=1;i<=n;i++) {
op=read();l=read();r=read();c=read();
if(op==0) add(l,r,c);
if(op==1) printf("%lld\n",query(l,r,c+1));
}
return 0;
}
分块入门 5
区间开方,区间求和。
稍作思考可以发现,开方操作比较棘手,主要是对于整块开方时,必须要知道每一个元素,才能知道他们开方后的和,也就是说,难以快速对一个块信息进行更新。
不难发现,这题的修改就只有下取整开方,而一个数经过几次开方之后,它的值就会变成 0 或者 1。
对于角块,不超过2√n个元素,直接暴力。
对于整块,这些块经过几次操作以后就会都变成 0 / 1,于是我们记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1 的块即可。
这样每个元素至多被开方不超过4次,显然复杂度没有问题。
#include<cmath>
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
#define int long long
#define N 100005
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int size,b[N];
int n,a[N],op,l,r,c,sum[N];
bool flag[N];
void solve(int x) {
if(flag[x]) return;
flag[x]=1;
sum[x]=0;
for(int i=(x-1)*size+1;i<=x*size;i++) {
a[i]=sqrt(a[i]),sum[x]+=a[i];
if(a[i]>1) flag[x]=0;
}
}
void SQRT(int l,int r) {
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
sum[b[i]]-=a[i],a[i]=sqrt(a[i]),sum[b[i]]+=a[i];
} else {
for(int i=l;i<=b[l]*size;i++)
sum[b[i]]-=a[i],a[i]=sqrt(a[i]),sum[b[i]]+=a[i];
for(int i=b[l]+1;i<=b[r]-1;i++)
solve(i);
for(int i=(b[r]-1)*size+1;i<=r;i++)
sum[b[i]]-=a[i],a[i]=sqrt(a[i]),sum[b[i]]+=a[i];
}
}
int query(int l,int r) {
int ans=0;
if(b[l]==b[r]) {
for(int i=l;i<=r;i++)
ans+=a[i];
return ans;
} else {
for(int i=l;i<=b[l]*size;i++)
ans+=a[i];
for(int i=b[l]+1;i<=b[r]-1;i++)
ans+=sum[i];
for(int i=(b[r]-1)*size+1;i<=r;i++)
ans+=a[i];
return ans;
}
}
signed main() {
n=read();
size=sqrt(n);
for(int i=1;i<=n;i++) {
a[i]=read();
b[i]=(i-1)/size+1;
sum[b[i]]+=a[i];
}
for(int i=1;i<=n;i++) {
op=read();l=read();r=read();c=read();
if(op==0) SQRT(l,r);
if(op==1) printf("%lld\n",query(l,r));
}
return 0;
}
Chef and Churu
有一个长度为 n 的数组 A,元素编号 1 - n。还有 n 个函数,函数编号 1 - n,第 i 个函数返回 L[i] 和 R[i] 之间的数的和。 支持两种操作: 将 A[x] 修改为 y。 求编号在 l 到 r 之间函数的返回值的和。 1≤n,m≤10^5
(函数就是一段区间和,然后让求几个函数的和)
Solution
函数可以用树状数组维护,然后所有函数用分块维护
问题在于修改一个地方会影响n个函数,然后再去影响\(\sqrt(n)\) 个块
怎么把影响n个函数这一步干掉呢?
预处理每一块内实际 每个数组位置 的出现次数。
https://www.cnblogs.com/chenyushuo/p/5275753.html
piao来的代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
const int smaxn=400;
int n,q,l[maxn],r[maxn],opt,u,v,b[maxn];
namespace bit{
unsigned long long s[maxn];
int lowbit(int x) {return x&(-x);}
void add(int id,int val){
for(int i=id;i<=n;i+=lowbit(i)){ //注意这里的n,需要根据题目要求改
s[i]+=val;
}
}
unsigned long long query(int id){
unsigned long long ans=0;
for(int i=id;i>=1;i-=lowbit(i)){
ans+=s[i];
}
return ans;
}
unsigned long long qsum(int l,int r){return query(r)-query(l-1);}
void clear(){memset(s,0,sizeof(s));}
}
using namespace bit;
namespace blocking{
int tot,belong[maxn],siz[smaxn],block; //分块基本数据
unsigned long long sum[maxn]; //解题数据
int a[maxn],f[maxn][smaxn],c[maxn];
void init(int n) {
block=(int)sqrt((double)n);
tot=n%block?(n/block+1):(n/block);
for(int i=1;i<=n;i++) belong[i]=(i-1)/block+1;
for(int i=1;i<=tot;i++) siz[i]=(min(i*block,n)-(i-1)*block);
for(int i=1;i<=tot;i++) {
for(int j=1;j<=n;j++) c[j]=0;
for(int j=block*(i-1)+1;j<=min(i*block,n);j++) c[l[j]]++,c[r[j]+1]--;
for(int j=1;j<=n;j++) c[j]+=c[j-1],f[j][i]=c[j];
}
}
void update(int id,int val) {
add(id,val-a[id]);
for(int i=1;i<=tot;i++) sum[i]+=1LL*f[id][i]*(val-a[id]);
a[id]=val;
}
unsigned long long query(int u,int v) {
unsigned long long ans=0;
if(belong[v]-belong[u]<=1) {
for(int i=u;i<=v;i++) ans+=qsum(l[i],r[i]);
return ans;
}
for(int i=u;i<=belong[u]*block;i++) ans+=qsum(l[i],r[i]);
for(int i=belong[u]+1;i<=belong[v]-1;i++) ans+=sum[i];
for(int i=(belong[v]-1)*block+1;i<=v;i++) ans+=qsum(l[i],r[i]);
return ans;
}
void debug() {
printf("block= %d, tot= %d\n",block,tot);
for(int i=1;i<=n;i++) printf("%d%c",belong[i],i==n?'\n':' ');
}
}
using namespace blocking;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&b[i]);
for(int i=1;i<=n;i++) scanf("%d %d",&l[i],&r[i]);
init(n);
for(int i=1;i<=n;i++) update(i,b[i]);
scanf("%d",&q);
while(q--) {
scanf("%d %d %d",&opt,&u,&v);
if(opt==2) printf("%llu\n",query(u,v));
else update(u,v);
}
}
一道类似的题