算法思想
树状数组和线段树维护的信息必须满足信息合并特性,分块算法可以维护一些线段树维护的内容,分块算法为优化后的暴力算法,几乎可以解决所有区间更新和区间查询
单点更新: 一般先将对应块懒标记下传,再暴力更新块状态
区间更新: 若区间更新横跨若干块,则只需对完全覆盖块打上懒标记,最多需要修改两端两个块,对两端剩余部分暴力更新
区间查询: 与区间更新类似,无需修改操作
预处理
将序列分块,然后每个块都标记左右端点L[i]和R[i],对最后一块特别处理,一般每块大小为给定范围n的算术平方根
代码如下
t=sqrt(n);
int num=n/t;
if(n%t) num++;
for(int i=1;i<=num;i++)
{
L[i]=(i-1)*t+1;
R[i]=i*t;
}
R[num]=n;
用pos标记每个元素所属的块,用sum累加每一块的值(这里以求区间和为例子)
代码如下
for(int i=l;i<=num;i++)
for(int j=L[i];j<=R[i];j++)
{
pos[j]=i;
sum[i]+=a[j];
}
区间更新
以[l,r]区间都加上d为例
- 求l和r所属块,p=pos[l],q=pos[r]
- 属于同一块,暴力更新
- 不属于同一块,完全覆盖区打标记,首尾暴力更新
代码如下
void Update(int l,int r,int d)
{
int p=pos[l],q=pos[r];
if(p==q)
{
for(int i=l;i<=r;i++)
a[i]+=d;
sum[p]+=d*(r-l+1);
}
else
{
for(int i=p+1;i<=q-1;i++)
add[i]+=d;
for(int i=l;i<=R[l];i++)
a[i]+=d;
sum[p]=d*(R[p]-l+1);
for(int i=L[q];i<=r;i++)
a[i]+=d;
sum[q]+=d*(r-L[q]+1);
}
}
区间查询
以查询[l,r]元素和为例
- 求l和r所属块,p=pos[l],q=pos[r]
- 若属于同一块,暴力累加,加上标记
- 不属于同一块,完全覆盖区域累加sum和标记值,暴力累加首尾端
代码如下
int Query(int l,int r)
{
int p=pos[l],q=pos[r],ans=0;
if(p==q)
{
for(int i=l;i<=r;i++)
ans+=a[i];
add+=add[p]*(r-l+1);
}
else
{
for(int i=p+1;i<=q-1;i++)
ans+=sum[i]+add[i]*(R[i]-L[i]+1);
for(int i=l;i<=R[p];i++)
ans+=a[i];
ans+=add[p]*(R[p]-l+1);
for(int i=L[q];i<=r;i++)
ans+=a[i];
ans+=add[q]*(r-L[q]+1);
}
return ans;
}
训练
POJ3468
题目大意:N个数,两种操作,区间添加给定数和输出区间和
思路:分块算法完成区间修改和区间查询
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long ll;
ll N,data[121212],Q,sum[121212],pos[121212],L[121212],R[121212],add[121212];
void Update(int l,int r,ll d ) {//更新l~r的数值
int p=pos[l],q=pos[r];
if(p==q) {//如果更新的是单个块,暴力
for(int i=l; i<=r; i++)
data[i]+=d;
sum[p]+=d*(r-l+1);
} else {
for(int i=p+1; i<=q-1; i++)//头尾之间更新标记
add[i]+=d;
for(int i=l; i<=R[p]; i++)//头单独暴力
data[i]+=d;
sum[p]+=d*(R[p]-l+1);
for(int i=L[q]; i<=r; i++)//尾单独暴力
data[i]+=d;
sum[q]+=d*(r-L[q]+1);
}
}
ll Query(int l,int r) {
int p=pos[l],q=pos[r];
ll ans=0;
if(p==q) {
for(int i=l; i<=r; i++)
ans+=data[i];
ans+=add[p]*(r-l+1);
} else {
for(int i=p+1; i<=q-1; i++)
ans+=sum[i]+add[i]*(R[i]-L[i]+1);
for(int i=l; i<=R[p]; i++)
ans+=data[i];
ans+=add[p]*(R[p]-l+1);
for(int i=L[q]; i<=r; i++)
ans+=data[i];
ans+=add[q]*(r-L[q]+1);
}
return ans;
}
int main() {
scanf("%lld%lld",&N,&Q);
for(int i=1; i<=N; i++)
scanf("%lld",&data[i]);
ll t=sqrt(N);
ll num=N/t;
if(N%t)
num++;
for(int i=1; i<=num; i++) {
L[i]=(i-1)*t+1;
R[i]=i*t;
}
for(int i=1; i<=num; i++)
for(int j=L[i]; j<=R[i]; j++) {
pos[j]=i;
sum[i]+=data[j];
}
while(Q--) {
char ch;
ll a,b,c;
cin >>ch;
switch(ch) {
case 'C':
scanf("%lld%lld%lld",&a,&b,&c);
Update(a,b,c);
break;
case 'Q':
scanf("%lld%lld",&a,&b);
printf("%lld\n",Query(a,b));
break;
}
}
return 0;
}
POJ1019
题目大意:有这样一个序列112123123412345…1234567891012345…,该序列由无数个数字组组成,每个数字组有k个数字(k各不相同且从小到大),组内有1~k这k个数字,现在给出一个位置i,输出序列第i位数字是多少(从1开始算)
思路:把每个组看成一个分块,每个组长度为a[i],当组内都是个位数时,当前组长度为前一组+1,当组内都为两位数时,当前组长度为前一组+2,以此类推。计算每一块长度a[i]和前i块总长度sum[i],定位到第i块,在块内查找第pos位所在的数k,数k可能为多位数,第pos为 k / ( i n t ) p o w ( 10.0 , l e n − p o s ) % 10 k/(int)pow(10.0,len-pos)\%10 k/(int)pow(10.0,len−pos)%10,其余详见代码
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
typedef long long ll;
ll a[121212],sum[121212];//空间需足够大
int t;
int main() {
for(int i=1; i<121212; i++) {
a[i]=a[i-1]+(int)log10(double(i))+1;//log10获得是几位十进制
sum[i]=sum[i-1]+a[i];
}
scanf("%d",&t);
while(t--) {
int n,i=0;
scanf("%d",&n);//n以位单位
while(sum[i]<n)//确定n在第i块
i++;
int pos=n-sum[i-1],len=0,k=0;//确定n在第i块的第pos个位置
while(len<pos)
len+=(int)log10((double)(++k))+1;//初始值为1,len获得了前k个数字的总长度,k为第几个数字
printf("%d\n",k/(int)pow(10.0,len-pos)%10);
//此时的k是pos对应所在的数字,len是包括了k这个数字的总长度,len-pos为第k个数字在pos之后的位数,需要去掉,%10将所得数取个位得到结果
}
return 0;
}
POJ4417
题目大意:查询给定区间内小于等于特定值的数量
思路:分块,并对每一块非递减排序,在辅助数组上排序,查询给定区间,若属于同一块,直接暴力统计,若不属于同一块,在辅助数组上用upper_bound函数统计,首尾端暴力
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
int T,data[121212],temp[121212],L[121212],R[121212],n,m,belong[121212];
void Build() {
int t=sqrt(n);//每个块的大小
int num=n/t;
if(n%num)
num++;//有多少块
for(int i=1; i<=num; i++) {//画出每个块左右边界
L[i]=(i-1)*t+1;
R[i]=i*t;
}
R[num]=n;//最后一个边界特殊对待
for(int i=1; i<=n; i++)//归属每个元素
belong[i]=(i-1)/t+1;
for(int i=1; i<=num; i++)
sort(temp+L[i],temp+R[i]+1);//对临时块排序
}
int Query(int l,int r,int t) {
int ans=0;
if(belong[l]==belong[r])//如果是同一个块,直接暴力
for(int i=l; i<=r; i++) {
if(data[i]<=t)
ans++;
} else {
for(int i=l; i<=R[belong[l]]; i++)//左端
if(data[i]<=t)
ans++;
for(int i=belong[l]+1; i<belong[r]; i++)//中间
ans+=upper_bound(temp+L[i],temp+R[i]+1,t)-temp-L[i];
for(int i=L[belong[r]]; i<=r; i++)//右端
if(data[i]<=t)
ans++;
}
return ans;
}
int main() {
scanf("%d",&T);
for(int i=1; i<=T; i++) {
scanf("%d%d",&n,&m);
for(int i=0; i<n; i++) {
scanf("%d",&data[i]);
temp[i]=data[i];
}
Build();
printf("Case %d:\n",i);
while(m--) {
int l,r,H;
scanf("%d%d%d",&l,&r,&H);
printf("%d\n",Query(l,r,H));
}
memset(temp,0,sizeof(temp));
}
return 0;
}
HDU5057
题目大意:对序列两种操作,将位置x上的值变为y(从1算)和统计区间[l,r]内元素第D位是P的元素的个数(二进制)
思路:分块,block[i][j][k]表示第i块中第j位是k的元素,查询,若区间为同一块,暴力累加,多个块,累加完全覆盖区域的block,暴力首尾,更新,需要去掉a[x]每一位上的元素,把a[x]换成y,然后以y为基准来增加,详见代码
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
int block[400][12][12],a[1212121],belong[1212121],L[1212121],R[1212121],n,m,T;
int ten[11]= {0,1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000};
void Build() {
int t=sqrt(n);
int num=n/t;
if(n%t)
num++;
for(int i=1; i<=num; i++) {
L[i]=(i-1)*t+1;
R[i]=i*t;
}
R[num]=n;//划定边界
for(int i=1; i<=n; i++)//确定归属
belong[i]=(i-1)/t+1;
for(int i=1; i<=n; i++) {
int temp=a[i];
for(int j=1; j<=10; j++) {
block[belong[i]][j][temp%10]++;//统计第i个块中第j位01对应元素个数
temp/=10;
}
}
}
int Query(int l,int r,int d,int p) {
int ans=0;
if(belong[l]==belong[r]) {//范围为一个块
for(int i=l; i<=r; i++)
if((a[i]/ten[d])%10==p)
ans++;
} else {
for(int i=l; i<=R[belong[l]]; i++)//左端
if((a[i]/ten[d])%10==p)
ans++;
for(int i=L[belong[r]]; i<=r; i++)//中端
if((a[i]/ten[d])%10==p)
ans++;
for(int i=belong[l]+1; i<belong[r]; i++)//右端
ans+=block[i][d][p];
}
return ans;
}
void Update(int x,int y) {
for(int i=1; i<=10; i++) {
block[belong[x]][i][a[x]%10]--;
a[x]/=10;
}
a[x]=y;
for(int i=1; i<=10; i++) {
block[belong[x]][i][y%10]++;
y/=10;
}
}
int main() {
scanf("%d",&T);
while(T--) {
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
scanf("%d",&a[i]);
Build();
while(m--) {
char ch;
int L,R,D,P;
cin >>ch;
if(ch=='S') {
scanf("%d%d",&L,&R);
Update(L,R);
} else if(ch=='Q') {
scanf("%d%d%d%d",&L,&R,&D,&P);
printf("%d\n",Query(L,R,D,P));
}
}
memset(belong,0,sizeof(belong));
memset(L,0,sizeof(L));
memset(R,0,sizeof(R));
memset(block,0,sizeof(block));
}
return 0;
}
总结
分块算法虽然时间复杂度还不够优秀,但是其通用性和模板的可变化是很大的优点,而且较容易理解,模板也较容易