分块笔记&训练

算法思想

树状数组和线段树维护的信息必须满足信息合并特性,分块算法可以维护一些线段树维护的内容,分块算法为优化后的暴力算法,几乎可以解决所有区间更新和区间查询

单点更新: 一般先将对应块懒标记下传,再暴力更新块状态

区间更新: 若区间更新横跨若干块,则只需对完全覆盖块打上懒标记,最多需要修改两端两个块,对两端剩余部分暴力更新

区间查询: 与区间更新类似,无需修改操作

预处理

将序列分块,然后每个块都标记左右端点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为例

  1. 求l和r所属块,p=pos[l],q=pos[r]
  2. 属于同一块,暴力更新
  3. 不属于同一块,完全覆盖区打标记,首尾暴力更新

代码如下

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]元素和为例

  1. 求l和r所属块,p=pos[l],q=pos[r]
  2. 若属于同一块,暴力累加,加上标记
  3. 不属于同一块,完全覆盖区域累加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,lenpos)%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;
}

总结

分块算法虽然时间复杂度还不够优秀,但是其通用性和模板的可变化是很大的优点,而且较容易理解,模板也较容易

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值