分块学习笔记(gu gu gu)

在一次的luogu比赛后,问学长一道题的做法,学长说了句分块,那是第一次听说分块,那道题我的第一直觉是线段树可是我线段树不好写不出来,学长的一句分块让我有了一种用分块代替线段树的想法。我在网上搜博客的时候看到了hzwer大神的分块练习1~9某位博主说从学习开始四天做完这九道题,我就暗下决心怎么样四天之内也要写完吧,最好更快。然而现在我心态爆炸,已经没有心情继续写下去,所以原定写完九道题再写的总结,还是在我忘掉之前写完吧。。。。。。

希望退役之前我可以把1~9写完吧。

分块,说白了就是大暴力,把原来的区间分成一块一块的,区间修改时把包含在这个区间中的各个块进行标记修改,不在一整块中的部分即最左端和最右端进行暴力修改,(例如:在9个数的序列中修改第2位到第7位,

image

9个数可以分为3块(一般每个块的长度均为根号下n,我也不知道为什么,只知道这样操作确实快,唉,我太弱了) ,每块长度为3,

image

那么2在第一块,7在第3块,

image

所以从2开始修改,

image

修改到其所在块的最右端,接着一整块都包含在修改区间内,

image

所以标记此块,而到下一块我们发现我们需要修改的区间此块只包含7,所以我们只修改7)

image

对于这样处理起来可以节省很多时间,这就是分块,时间复杂度o(√n)。
在loj上有hzw的九道数列分块入门题

image.png
每道题的难度显而易见了

数列分块入门1:

操作:区间加法,单点查值

线段树和树状数组都可以进行的经典操作,而分块怎么进行呢,其实就和上面的讲解一样,定义一个标记数组存储下区间内每个数需要修改的值,对于直接暴力的地方(如:上面的2,3和7)直接在原数组上修改,输出时需要记得加上标记数组中的值。
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<cmath>
using namespace std;
inline long long read(){//读入优化
	long long x = 0; long long f = 1; char c = getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();}
	while(c<='9'&&c>='0'){x=x*10+c-'0';c=getchar();}
	return x*f;
}
int n;
int m;
int opt,l,r,c;
int z[500521];//原数组
int pos[500521];//存储每个点所在的块
int tag[500521];//标记数组
void modify(int l,int r,int c){
	if(pos[l] == pos[r])//如果在同一个块内
//直接暴力修改
		for(int i = l; i<=r; i++)
		z[i] += c;
    else {
//修改右边不在一整块中的部分
    	for(int i = l; i<=pos[l]*m; i++)
        z[i] += c;
//标记每个块需要加上的值
        for(int i = pos[l]+1; i<=pos[r]-1; i++)
        tag[i] += c;
//修改左边不在一整块中的部分
        for(int i = (pos[r]-1)*m+1; i<=r; i++)
        z[i] += c;
	}
    
}
int main(){
	n = read();
	m = sqrt(n);//开方操作,确定每块的长度,sqrt在math库中需要加上
	for(int i = 1; i<=n; i++) z[i] = read();
	for(int i = 1; i<=n; i++) pos[i] = (i-1)/m+1;//计算每个点所在的块可以算一下进行证明
	for(int i = 1; i<=n; i++){
		 opt = read();
		 if(opt == 0){
		 	l = read();
		 	r = read();
		 	c = read();
		 	modify(l,r,c);
		 }
		 if(opt == 1){
		 	l = read();
		 	r = read();
		 	c = read();
//最后输出的值就是该点的值加上该点所在块的标记值(即需要加上的值)
		 	printf("%d\n",z[r]+tag[pos[r]]);
		 }
	}
	return 0;
}

其中修改操作为了简便也可以这样写

void modify(int l,int r,int c)
{
//修改左边不在区间中的部分
    for(int i=l;i<=min(pos[l]*m,r);i++)
        z[i]+=c;
//修改右边不在区间中的部分
    if(pos[l]!=pos[r])
        for(int i=(pos[r]-1)*m+1;i<=r;i++)
            v[i]+=c;
//修改中间的块
    for(int i=pos[l]+1;i<=pos[r]-1;i++)
        tag[i]+=c;
}

数列分块入门2:

操作:区间加法,询问区间内小于某个值 xxx 的元素个数

如果说我们每个数都去比较一遍,肯定是会t的,我们学分块主要还是学分块思想,我们对于每个块进行操作查询每个块中小于某个值 xxx 的元素个数,不就可以大大节省时间了吗?那由此可知为了更快获得小于某个数的个数,我们必须要求块内数据有序。
黄大神采用的是vector,开个二维vectorv[xxxx],利用vector的lower_bound操作。同时我们需要注意一点,在原数据有序的基础上进行加减,原数据次序并不会比打乱,所以修改包含的块直接修改,但是对于那些并不是在一整块中的数据进行修改时,原次序可能会被打乱,所以对于这一部分数据要进行重新排序

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<cmath>
#include<vector> 
using namespace std;
inline long long read(){
	long long x = 0; long long f = 1; char c = getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();}
	while(c<='9'&&c>='0'){x=x*10+c-'0';c=getchar();}
	return x*f;
}
long long  n;
long long  m;
long long  opt,l,r,c;
long long  z[5211314];
long long  pos[5211314];
long long  tag[5211314];
vector<long long>v[1314];
void reset(long long  x){
    v[x].clear();//清空该块内的元素
     //重新插入
	for(long long  i = (x-1)*m+1; i<=x*m; i++) v[x].push_back(z[i]); 
    // 重新排序
	sort(v[x].begin(),v[x].end());
}
void modify(long long  l,long long  r,long long  c){//修改函数
	if(pos[l] == pos[r]){//在同一块内
		for(long long  i = l; i<=r; i++)
		z[i] += c;
         // 排序只是在vector中有序,因为是原数组修改,所以需要清空此块重新插入进行排序
		reset(pos[l]);
	}
	else {
    	for(long long  i = l; i<=pos[l]*m; i++)
        z[i] += c;
 // 排序只是在vector中有序,因为是原数组修改,所以需要清空此块重新插入进行排序
        reset(pos[l]);
//对块进行标记时,区间加法并不会影响序列次序,所以只需要标记块
        for(long long  i = pos[l]+1; i<=pos[r]-1; i++)
        tag[i] += c;
        for(long long  i = (pos[r]-1)*m+1; i<=r; i++)
        z[i] += c;
 // 排序只是在vector中有序,因为是原数组修改,所以需要清空此块重新插入进行排序
		reset(pos[r]);
	}
}

long long query(long long  l,long long  r,long long  f){
	long long  ans = 0;
	if( pos[l] == pos[r]){
		for(long long  i = l; i<=r; i++)
		if(z[i] + tag[pos[i]] < f)
		ans++;
		return ans;
	}
	else {
		for(long long  i = l; i<=pos[l]*m; i++)
			if(z[i] + tag[pos[i]]< f)
			ans++;
		for(long long  i = pos[l]+1; i<=pos[r]-1;i++){
			long long  t = f - tag[i];
    //lowe_bound返回第一个大于或等于t的位置,减去begin得到区间内元素个数
			ans += lower_bound(v[i].begin(),v[i].end(),t) - v[i].begin();
		}
		for(long long  i = (pos[r]-1)*m+1; i<=r; i++)
		    if(z[i] +tag[pos[i]] < f)
		    ans++;
	}
	return ans;
}
int main(){
	n = read();
	m = sqrt(n);
   //预处理每个点所在的快
	for(long long  i = 1; i<=n; i++) pos[i] = (i-1)/m+1;
	for(long long  i = 1; i<=n; i++) {
		z[i] = read();
      //将数据z[i]存入所在的块pos[i]
		v[pos[i]].push_back(z[i]); 
	}
     //利用sort把每个块内的数据排好序 begin和end 代表所要排序的范围即整个v[i][~]‘’
	for(long long  i = 1; i<=pos[n]; i++)sort(v[i].begin(),v[i].end());
	for(long long  i = 1; i<=n; i++){
		 opt = read();
		 if(opt == 0){
		 	l = read();
		 	r = read();
		 	c = read();
		 	modify(l,r,c);
		 }
		 if(opt == 1){
		 	l = read();
		 	r = read();
		 	c = read();
		 	printf("%lld\n",query(l,r,c*c));
		 }
	}
	return 0;
}

数列分块入门3:

操作:涉及区间加法,询问区间内小于某个值 xxx 的前驱(比其小的最大元素)
这个题让我相当难受,,,hzw写的用set来实现,而我感觉用vector也能实现,又鉴于我set并不了解,所以努力实现vector版本的却无奈于实力有限,在打算放弃的时候恰巧翻到一篇vector实现的博客,所以就按照那份代码改,最后还是没改出来,wa了一页多,唯一的ac是我为了验证这份代码的正确性,还是等什么时候改出来再发吧,,,其实就是和二差不多一样的,不知道哪里错了,就是改不出来,头疼

数列分块入门4:

操作:区间加法,区间求和
操作还是不完整的块暴力,完整的标记区间,不过需要预处理出每个块的和。
修改时,修改每个点或块的同时,需要修改每个块的和

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<vector>
using namespace std;
inline int read(){
	int x = 0; int f = 1; char c = getchar();
	while(c<'0'||c>'9'){
		if(c == '-')f=-f;
		c=getchar();
	}
	while(c<='9'&&c>='0'){
		x=x*10+c-'0';
		c=getchar(); 
	}
	return x*f;
}
long long n;
long long m; 
const int maxn = 5211314;
long long  z[maxn];//数组不能随便拿long long定义我还不知道为什么
long long  pos[maxn];//刚刚做题遇到两次long long 定义MLE的情况
long long tag[maxn];//只是两天前做的就没改
long long  sum[maxn];
long long opt,l,r,c;
long long ans;
void modify(long long l,long long r,long long v){
	for(int i = l; i<=min(pos[l]*m,r); i++) {
		z[i] += v;
		sum[pos[i]] += v;
	}
	if(pos[l] != pos[r]){
	     int t = pos[r];
		for(int i = (pos[r]-1)*m+1; i<=r;i++){
			z[i] += v;
			sum[t] += v;  
		}
	}
	for(int i = pos[l]+1; i<=pos[r]-1; i++){
		tag[i] += v;
		sum[i] += v*m;
	}
	
}
long long query(long long l,long long r){
	int h = pos[l];
	for(int i = l; i<=min(pos[l]*m,r); i++){
	        ans += z[i] + tag[h];
	}
	if(pos[l] != pos[r]){
		int t = pos[r];
		for(int i = (pos[r]-1)*m+1; i<=r; i++){
			ans += z[i]+tag[t];
		}
	}
	for(int i = pos[l]+1; i<=pos[r]-1; i++){
		ans += sum[i];
	}
	return ans;
}
int main(){
	n = read();
	m = sqrt(n);
	for(int i = 1; i<=n; i++){
		z[i] = read(); 
		pos[i] = (i-1)/m+1;
		sum[pos[i]] += z[i];
	}
	for(int i = 1; i<=n; i++){
		opt = read();
		l = read();
		r = read();
		c = read();
		if(opt == 0)modify(l,r,c);
		else printf("%lld\n",query(l,r)%(c+1));
		ans = 0;//每次计算完需要重新赋初值
	}
	return 0;
}

数列分块入门5:

操作区间开方,区间求和
九个题当中除了第九题留给了莫队去解决,这是唯一一道没有写的题,区间开方?搞不明白,,,我太弱了

数列分块入门6:

操作:单点插入,单点询问
在loj上,这九道题中这道题的通过率仅次于第一道题,但这道题挺重要,我感觉难度上要比前几道题大一点,这道题由于插入的存在原分好的块内数据就会发生改变,而且块越来越大十分影响效率,所以需要重新分块。
//我代码还没理解透,以后再放吧。。。

数列分块入门7:

操作:区间乘法,区间加法,单点询问**
非常经典的操作,在luogu上 线段树2和本题完全相同,还有和[AHOI2009]维护序列也是一样的,可是我一直调不出来,直接导致心态爆炸,还是太弱了。

数列分块入门8:

操作:操作涉及区间询问等于一个数 ccc 的元素,并将这个区间的所有元素改为 ccc
我的想法是和2差不很多,在每个块内使用lower_bound进行查找ccc元素,修改时使用懒标记,对于左右两个端点,暴力查找,然后实现所在块内的懒标记,后直接修改值。
可是和7一样,他们一起让我心态爆炸,,,

数列分块入门8:

操作:查询区间最小众数
这其实是莫队的题吧,用分块的话,基于桶排,每出现一个数下标为此数的数组++。同时记录数组最大值和下标即众数,每次进行比较,就能得出答案。不过,我还是感觉使用莫队,可以更快的得出答案,毕竟莫队不需要像分块重新去查找

posted @ 2018-11-05 09:09  Euplectella  阅读(264)  评论(0编辑  收藏  举报