分块

分块

这是第一篇博客。嘿嘿嘿!话不多说,直接开始。
首先来说一下什么是分块:
       分块,顾名思义,即将数据切分成块。那么,这有什么意义呢?其实,分块是一种优雅的暴力
这里给出一道例题:洛谷P2801。https://www.luogu.org/problemnew/show/P2801
       以此题为例:
       有一组数据,共有n项,我们通常以根号n作为划分标准,即每一块有根号n项数据(为保持原有数据,需要开设新的数组)。以下是分块的初始化:

int n,  a[maxn]//n为数据的规模,a数组存储原始数据
int b[maxn]int  l[maxn], r[maxn], belong[maxn],  block, num;
//b数组是进行分块操作的数组,初始时是a数组的一个副本

void bulid(void) {
	block = int (sqrt(n));		//block为每一块所包含的元素个数
	num = n / block;		//num为数组最终被分块之后的块数
	if (n%block) {
		num++;
	}
	for (int i = 1; i < num; i++) {		//默认下标从1开始,第num块单独处理,因为第num块数组元素无法确定
		l[i] = (i - 1) * block + 1;		//l数组存储每一块数据的起始点下标
		r[i] = i * block;				//r数组存储每一块数据的结束下标
		sort(b+l[i], b+r[i]+1);
	}
	l[num] = (num - 1)*block + 1;
	r[num] = n;
	sort(b+l[num], b+r[num]+1);		//对块进行分类

	for (int i = 1; i <= n; i++) {
		belong[i] = (i-1) / block+1;	//belong数组存储每一个元素所属的下标
	}
}

       此题如果使用暴力,必然超时,那分块又是如何来达到这优雅的暴力的呢?
       此题的关键在于频繁地对某一区间的数据进行访问。众所周知,对有序数据的访问在很多时候比对无序数据的访问更为高效,比如搜索(有序数组的二分搜索必然比无序数组的遍历更为高效)。因此,我们对每一块都进行排序。(此步预处理,虽会消耗时间和空间,但在数据量大,且对区间操作频繁的时候,会在不久的将来比暴力省下不少时间,可谓是未雨绸缪啊!)
       首先请看对数据的更新操作:
       大致分为如下两种情况:
       1:需要更新的区间包含多个块
       2:需要更新的区间同属于一个块
分块
       对于第一种情况,区间之间的完整块可直接利用数组来记录区间的变化情况(此处利用add数组来记录整一块数据的变化情况),而对于非完整块,则只需对原数组进行跟新即可,同时更新之后需要对非完整块所属的块进行排序,以维持其有序性。

void reset(int x) {	//对非完整区间进行数据跟新之后,需要保持其有序性
	for (int i = l[x]; i <= r[x]; i++) {
		b[i] = a[i];
	}
	sort(b + l[x], b + r[x] + 1);
}

void update(int left,int right,int val) {
	if (belong[left] == belong[right]) {	//第二种情况
		for (int i = left; i <= right; i++) {
			a[i] += val;
		}
		reset(belong[left]);
		return;
	}
	//第一种情况
	for (int i = left; i <= r[belong[left]]; i++) {	//左端非完整块
		a[i] += val;
	}
	reset(belong[left]);	//保持有序性
	
	for (int i = belong[left] + 1; i < belong[right]; i++) {	//中间区块
		add[i] += val;	//此处的add数组记录了此块区域的数据至此为止的变化量,add数组是分块优雅的一处体现
	}					

	for (int i = l[belong[right]]; i <= right; i++) {	右端非完整块
		a[i] += val;
	}
	reset(belong[right]);	//保持有序性
}

       接下来是访问数据:由于每一块都是有序的,因此对块内的更新可以利用二分来缩短时间。对于第一种情况,区间之间的完整块利用b数组来完成访问,而非完整块,则利用原数组进行访问,可见非完整块最多只会有两块。对于第二种情况,只需对原数组访问即可。(以下给出此题的访问操作)

int ask(int left, int right, int val) {
	int sum = 0;
	if (belong[left] == belong[right]) {		//第二种情况
		for (int i = left; i <= right; i++) {
			if (a[i] + add[belong[i]] >= val) {	//原数据加上变动量才是此时的数据值,注意!
				sum++;
			}
		}
		return sum;
	}
	//第一种情况
	for (int i = left; i <= r[belong[left]]; i++) {	//左端非完整区块
		if (a[i] + add[belong[i]] >= val) {
			sum++;
		}
	}
	//中间完整块
	for (int i = belong[left] + 1; i < belong[right]; i++) {	//此处以块进行遍历
		sum +=fin(i, val-add[i]);		//在块内配合以二分
	}									//速度远大于普通暴力
	
	for (int i = l[belong[right]]; i <= right; i++) {	//右端非完整区块
		if (a[i] + add[belong[i]] >= val) {
			sum++;
		}
	}
	return sum;
}

       至此,此题讲解完毕。分块的核心思想应该阐述清楚。

       总结:之所以要分块,是为了能在区间进行更为高效的操作, 同时也要注意不能让他上升到整个区间,不然就没有意义了。
终于写完第一篇了,好累。。。。。。虽然写的不太美观,也没什么样式,以后再慢慢改善吧。