xy0v0's Blog

xy0v0's Blog

算法查漏补缺 #1 分块

OI
30
2025-08-04

什么是分块

用于实现区间更新与区间查询的数据结构, 可用于骗分

相较于BIT、线段树等数据结构虽速度较慢,但更加灵活,多种问题均可转化为分块

分块的具体实现(单点修改)

使用查询区间最大值的实现来举例:

定义数组 ​a_i 为原始输入,​b_i 代表 ​i 号元素对应的块编号​bm_k 代表 ​k 号块对应的块内最大值。

要对于一个区间 ​[l,r] 进行区间查询,首先需要注意** ​l​r 是否在同一块内**。如果不在同一块内,首先记录左侧端点所在块号 ​b_l
与右侧端点所在块号 ​b_r ,遍历从 ​l 到所在块号为 ​b_l 的最后一个元素,右端点附近处理方式同理。接下来遍历中间块,即块编号在 ​[b_l+1,b_r-1] 中的所有块的 ​bm

需要注意的是,以上操作仅支持单点修改,未考虑区间修改。

代码实现(单点修改)

块编号预处理/输入处理

int B=sqrt(n)+1; // 块总数
for(int i=1;i<=n;i++) b[i]=(i-1)/B+1;
for(int i=1;i<=n;i++){
	cin>>a[i];
	bm[b[i]]=max(bm[b[i]],a[i]);
}

其中我们令总块数为 ​\sqrt{n} 。可以证明,取块数为 ​\sqrt{n} 时最优,下面给出简单证明:

考虑最坏情况,即遍历一个块长度的离散元素和 ​\frac nB 个块,时间复杂度为 ​B+\frac nB 。考虑基本不等式,显然 ​B > 0 ,则 ​B+\frac nB​B =\frac nB​B=\sqrt{n} 时取得最小值。

区间查询

int rmq(int l,int r){
	int res=-INF;
	if(b[l]==b[r]){
		res=*max_element(a+1+l,a+1+r);
		return res;
	}
	else{
		for(int i=l;b[i]==b[l];i++) res=max(res,a[i]);
		for(int i=b[l]+1;i<=b[r]-1;i++) res=max(res,bm[i]);
		for(int i=r;b[i]==b[r];i--) res=max(res,a[i]);
	}
	return res;
}

实现区间修改

参考线段树中的 lazytag ,实现区间修改,定义 ​c_k​k 号块的偏移量。

每次区间修改类似于区间查询,先判断是否在同一块,再修改两边元素和中间的 lazytag

区间修改代码实现

区间修改

int add(int l,int r,int k){
	int tag;
	if(b[l]==b[r]){
		tag=c[b[l]];
		for(int i=l;i<=r;i++){
			a[i]+=k;
			bm[b[i]]=max(bm[b[i]],a[i]+tag);
		}
		return ;
	}
	else{
		tag=c[b[l]];
		for(int i=l;b[i]==b[l];i++){
			a[i]+=k;
			bm[b[i]]=max(bm[b[i]],a[i]+tag);
		}
		tag=c[b[r]];
		for(int i=r;b[i]==b[r];i--){
			a[i]+=k;
			bm[b[i]]=max(bm[b[i]],a[i]+tag);
		}
		for(int i=b[l]+1;i<=b[r]-1;i++) c[i]+=k,bm[i]+=k; // 不要忘记修改区间最大值! 
	}
}

区间查询(添加 lazytag

int rmq(int l,int r){
	int res=-INF;
	int tag;
	if(b[l]==b[r]){
		tag=c[b[l]];
		res=*max_element(a+1+l,a+1+r)+tag;
		return res;
	}
	else{
		tag=c[b[l]];
		for(int i=l;b[i]==b[l];i++) res=max(res,a[i]+tag);
		tag=c[b[r]];
		for(int i=r;b[i]==b[r];i--) res=max(res,a[i]+tag);
		for(int i=b[l]+1;i<=b[r]-1;i++) res=max(res,bm[i]); // 修改时已经加过了,不要重复加
	}
	return res;
}
tips:warn 代码虽逻辑简单,但其中部分细节需要特别注意,比如区分元素编号与块编号。

例题1(区间内特定值域内元素计数)

长城上共有n个防御塔。第i个防御塔的防御力为 ​a_i 。在你的作战指挥部里,你正在模拟m种轰炸方式。第j种轰炸方式的攻击范围从第 ​l_j 个防御塔炸到第 ​r_j 个,攻击力为 ​k_j 。其中防御力小于等于 ​k_j 的塔都会被摧毁。请对每种轰炸方式,计算能炸毁几个塔。

思路分析

对于这种问题,我们可以考虑块内排序后再在块内进行二分查找。由于每个块的处理时间发生改变,我们的块数需要重新计算。

每个分块查询进行一次二分,单个分块的时间复杂度为 ​O(\log B) ,共有 ​B 块,最坏情况为 ​B + \frac {n \log B}B 。则有 ​B=\sqrt{n \log B} \approx \sqrt{n \log n}

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+9;
ll n,m;
ll a[N],b[N],as[N];
ll lB[N],rB[N]; // k号块的左右端点下表
ll B;

int query(int l,int r,int k){
	ll cnt=0;
	if(b[l]==b[r]){
		for(int i=l;i<=r;i++){
			if(a[i]<=k) cnt++;
		}
		return cnt;
	}
	else{
		for(int i=l;b[i]==b[l];i++) if(a[i]<=k) cnt++;
		for(int i=r;b[i]==b[r];i--) if(a[i]<=k) cnt++;
		for(int i=b[l]+1;i<=b[r]-1;i++){
			// 寻找第一个大于的,所以使用upper_bound
			cnt+=upper_bound(as+lB[i],as+rB[i]+1,k)-as-lB[i];
		}
		return cnt;
	}
}

int main(){
	cin>>n>>m;
	B=sqrt(log2(n)*n)+1;
	for(int i=1;i<=n;i++) b[i]=(i-1)/B+1;
	for(int i=1;i<=b[n]-1;i++){ // 这里要对最后一块单独处理
		lB[i]=(i-1)*B+1;
		rB[i]=B*i;
	}
	for(int i=1;i<=n;i++){
		cin>>a[i];
		as[i]=a[i];
	}
	for(int i=1;i<=b[n];i++) sort(as+lB[i],as+rB[i]+1); // 块内排序
	for(int i=1;i<=m;i++){
		ll l,r,k;
		cin>>l>>r>>k;
		cout<<query(l,r,k)<<' ';
	}
	return 0;
}

例题2(实时排名)

大赛共 ​n 位选手。决赛只有一题一次网络提交机会,​n 位选手依次提交,第 ​n 个提交的选手显示出得分为 ​x_i ,即刻他的分数就会记录到得分排行榜上,对于每个选手请你求出他提交的时刻是当时第几名?可能名次并列。
输入一行为正整数n,2<=n<=200000。接着一行为n个非负整数,第i个数为xi,均不超过10000

这题我们需要转变思路,不一定需要对选手编号进行分块,而是对分数进行分块。但是这需要数据的最大值较小,观察到这道题中 ​x_i 均不超过10000,因此可对分数进行分块。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int S=10001; //将分数全部+1,保证从1开始
const int B=sqrt(S)+1;
const int N=10009;
int n;
int x[N],b[N],cnt[N],bc[N];
int main(){
	cin>>n;
	for(int i=1;i<=S;i++) b[i]=(i-1)/B+1;
	for(int i=1;i<=n;i++){
		int s;
		cin>>s;
		s++;
		cnt[s]++,bc[b[s]]++;

		int ans=0;
		for(int j=b[s]+1;j<=b[S];j++){
			ans+=bc[j];
		}
		int rB=min(S,b[s]*B);
		for(int j=s+1;j<=rB;j++){
			ans+=cnt[j];
		}
		cout<<ans+1<<' '; //排名要+1!
	}
	return 0;
}
tips:info 其实这道题使用后续的BIT做更方便,放在这为了展示分块算法的灵活性,分块的对象不仅限于编号。