分块入门及例题讲解(一)

分块

先讲一个例子

假设现在有一个x轴  在区间 [ left , right ] 上有一些装置
已知在坐标为 i  的位置 可以触发装置 使得你到达 i+k[i] 的这个坐标

现在问你 假设从 坐标为 p 的地方开始 需要触发几次装置才能离开这个区间

这个显然非常简单 只要在输入 装置的 k[i] 属性后 在从后往前 更新这个触发装置的次数就可以了(简单的dp思想)

但是有的题目不会这么简单 他可能会给出一些变化
就比如说上面的题可以变为:

假设现在有一个x轴  在区间 [ left , right ] 上有一些装置
已知在坐标为 i  的位置 可以触发装置 使得你到达 i+k[i] 的这个坐标
现在有两种操作
1、问你 假设从 坐标为 p 的地方开始 需要出发几次装置才能离开这个区间
2、更改k[i]
  1. 一个比较简单的想法就是 在更改的时候 就像输入一样 在从这个更改的位置开始 一直向前更新就好了 但是会发现 这样一次修改的复杂度是
    O(n) 如果处理的区间比较长 更改操作的次数比较多 那么程序将可能会TLE

  2. 那可能可以换一种方式 一开始不计算 每个点出区间的步数 当需要的时候 再重新计算 那这样 复杂度仍然达到了 O(n)
    而且没有任何优化的空间了

所以现在需要想一个办法来优化这个过程

优化

根据前面的分析 也只能对第一种想法进行优化

对上面这个的过程观察一下 发现时间主要就是花在了对 被更改的这个点的前面的所有点的遍历 但是 这些点并不一定都受到了 这个被更改点的影响(因为他们出这个区间的路径可能根本就没有经过这个点)
就是因为这个 做了许多没有用的计算

于是我们就要想办法减少这个遍历的操作

那最好的就是把经过这个被更改点的所有点 然后 这个点一改 这些点就跟着改 完全没有一点浪费 但是这个难以达到 因为在数据输入之前 根本不知道每个点需要被更改多少次 或者是需要进行多少次更改操作 不管怎么处理 都可能有 MLE 的风险

那么怎么办呢 可以利用分块来解决

用分块对这个问题进行优化

再想一想上面的两种操作
一种是直接更新遍历 记录每一个点出区间的步数 可能有TLE的风险
一种是记录与被更改点有关的点的坐标 可能有MLE的风险

那这两种操作综合一下 是否有可能满足要求呢?
那就是分块了
当区间太大时 当一个点的 k[i] 改变后 重新更新其他每一个点 所需要的时间太多 所以就把区间分小一点
当记录不下与被更新点相关的点时 就记录每个点离开分离的小区间后到达的点的位置

这样分离的小区间就叫做块 分块 就是把大区间分成小区间 利用这些小区间的相关性 从而优化过程的一种操作

那么再看一下一开始的这个问题:
1、查询
在分块之后 怎么计算一个点 离开这个整个的大区间所需要的步数呢?
它到达下一个小区间所需要的步数+到达下一个小区间的点再到下一个小区间所需要的步数+…… 直至离开整个的大区间
2、更新
更新也就只需要更新 与被修改点在同一个小区间里面的点就可以了 因为之前的查询已经因为这个小区间的分离 使得 位于不同小区间的点之间 对于这个步数 已经没有影响了

现在再来看一下经过分块的优化之后两个操作的时间复杂度
假设原来的大区间的长度是n 分块分成的小区间的长度是m
查询操作的时间复杂度就是O(n/m)
更新操作的时间复杂度就是O(m)
总的时间复杂度就是O(m+n/m)

而又因为分成的小区间的长度是由我们自己定的 所以随便搞一个均值不等式就可知 当小区间的长度取到 m= n \sqrt{\smash[b]{n}} n 时总的时间复杂度最小 为O( n \sqrt{\smash[b]{n}} n )
(至于最后可能m可能不能正好取到 n \sqrt{\smash[b]{n}} n (因为m肯定是正整数))但是影响不大 复杂度仍然可以算作是O( n \sqrt{\smash[b]{n}} n )

代码实现

在写代码之前 先想一想 这个程序需要实现什么功能 :

  1. 首先 我们要实现分块的功能 计算出 每一个元素属于那一个区块 (根据前面的讨论 我们设 每一个区块的长度为 n \sqrt{\smash[b]{n}} n
  2. 然后 还要记录 每个元素到达下一个区块(或者是跳出这个区间)所需要经过的步数
  3. 记录 每个元素到达下一个区块的位置 (注意 不一定是 相邻)
  4. 一个函数进行修改
  5. 一个函数计算每一个元素 跳出整个的大区间的步数

可以发现 1,2,3 点都是关于元素的性质 4,5 点 则是一些与本题相关的函数
故我们可以用一个结构体来描述这个 元素的属性

节点结构体
struct Node{
	int block;  //计算 这个元素是属于那一个小区块
	//当然这个block 可以不要 因为block都是根据元素的下标直接计算出来的
	//但是如果经常访问元素所处的区块的话 提前先算出来可以减少计算,节省时间
	
	int k;       //题目中所给出的 触发装置将会前进的步数
	int steps;   //从这个位置 到达下一个区块的所需的操作数
	int to_next_block_position;  //到达下一个区块的位置
}node[maxn];
初始化

初始化 分为两个部分:

  1. 一个是 将节点所属的 区间编号算出来
  2. 一个是 将steps 、to_next_block_position 算出来(计算时应该从后往前计算,简单的dp思想)

首先来看看是怎么编号的(这个随意一点 根据自己的喜好,但是 根据前面的分析 要是的复杂度最小 区块的长度为 n \sqrt{\smash[b]{n}} n

int len=sqrt(n);

分块示意图
可以看到 照这样分块的话

  1. 每一个区块的第一个元素的索引为 len*(block-1)
  2. 而每一个元素计算其所属区块的公式为 index/len+1
//根据现在的这个分块的原则 进行初始化操作
void init(){
	int len=sqrt(n);
	for(int i=0;i<n;i++){
		node[i].block=i/len+1;
		scanf("%d",&node[i].k);
	}
	
	for(int i=n-1;i>=0;i--){
		if(i+node[i].k>=n){
			node[i].to_next_block_position=0;
			node[i].steps=1;
			//表示 这个点已经跳出了整个的大区间了
		}
		else if(node[i].block!=node[i+node[i].k].block){
			//表示 已经到了下一个区块了
			node[i].to_next_block_position=i+node[i].k;
			node[i].steps=1;
		}
		else if(node[i].block==node[i+node[i].k].block){
			//表示 这个点 前进了node[i],k 的步数后仍在这个区间里面
			node[i].to_block_position=node[i+node[i].k].to_next_block_position;
			//因为是从后往前计算的 所以 node[i] 这个后面的节点都算好了
			node[i].steps=node[i+node[i].k].steps;
		}
	}
}
单点查询操作

因为记录了每一个点到达下一个区块的位置(离散化) 根据这个 一个区间一个区间的做加法即可

int cal(int index){
	//计算 从index位置 离开整个大区间 所需的步数
	int ans=0;
	while(index<n){
		ans+=node[i].steps;
		index=node[i].to_next_block_position;
	}
	return ans;
}
单点修改操作

分块的好处就在这个地方 因为 我们将点分成一个一个的小区块 那么改变一个点 的k值 所影响的 现在也就只有在同一个区间内的 经过这个点的节点(因为位于其他区块的点 都记录了 一个 to_next_block_position 根据这个算出离开整个大区间的操作数)
那我们所需要更新的就是在被修改点的同一个区间内的点

void update(int index,int value){
	//表示将index的k值改为 value
	node[index].k=value;
	int start=len*(node[index].block-1);
	int end=start+len;
	//计算出 这个区块开始和结束的坐标
	for(int i=end-1;i>=start;i--){
		if(i+node[i].k>=n){
			node[i].to_next_block_position=0;
			node[i].steps=1;
			//表示 这个点已经跳出了整个的大区间了
		}
		else if(node[i].block!=node[i+node[i].k].block){
			//表示 已经到了下一个区块了
			node[i].to_next_block_position=i+node[i].k;
			node[i].steps=1;
		}
		else if(node[i].block==node[i+node[i].k].block){
			//表示 这个点 前进了node[i],k 的步数后仍在这个区间里面
			node[i].to_block_position=node[i+node[i].k].to_next_block_position;
			//因为是从后往前计算的 所以 node[i] 这个后面的节点都算好了
			node[i].steps=node[i+node[i].k].steps;
		}
	//这一部分其实 就是初始化的那一部分代码 只是 处理的范围减小了
	}
}

以上 就是 分块算法的最基本的处理了
其实 分块也算是暴力的一种 只是可以将复杂度优化 使得能够更快的算出答案

例题

【分块】[Hnoi2010]Bounce 弹飞绵羊

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值