学习笔记:分块

前言

分块可以解决几乎全部的区间查询区间更新等问题,功能比线段树和树状数组要强大,但是时间复杂度会更大一点。其实分块就是一种优化过的暴力,它是对于整体进行像线段树一样的维护,对局部进行暴力的修改。

原理

顾名思义分块分块,我们把长度为n的序列分为若干块。维护块内信息即可。
又要问了,要多大呢?通常将块的大小设为 n \sqrt n n ,用pos数组储存每个块的位置。
最后分完有剩,那么剩余的单独为一个块。
在这里插入图片描述

我们用线段树的例题来介绍传送门luoguP3372

题目要求:
1.区间更新:
我们找在这个区间有多少个块是全部在里面的,因为这些块全要更新,我们可以添加懒惰标记,下次要用时才更新。
对于部分在区间的块,我们只有暴力的把这些在区间内的点更新即可、
2.区间查询:
还是讨论如果被区间包含的块,就直接加上处理懒标的值和之前的和即可。
部分在区间的块,就暴力计算值即可。

代码非常简单。也好理解。

#include<bits/stdc++.h>
using namespace std;
#define int long long

const int N=1e5+5;
int n,m,a[N];
int pos[N],sum[N],L[N],R[N],lz[N];
//块的位置,块的和,块的左区间和右区间,懒惰标记 

inline void build()
{
    
    	
	int t=sqrt(n*1.0);
	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;//最后一个块的右区间是n 
	
	for(int i=1;i<=num;i++)
		for(int j=L[i];j<=R[i];j++)
		{
    
    
			pos[j]=i;//该点属于哪个块 
			sum[i]+=a[j];//统计和 
		}
}

inline void update(int l,int r,int k)
{
    
    
	int x=pos[l],y=pos[r];
	if(x==y)//块包含了这个区间,暴力 
	{
    
    
		for(int i=l;i<=r;i++)
			a[i]+=k;
		sum[x]+=k*(r-l+1);
	}
	else
	{
    
    
		for(int i=x+1;i<=y-1;i++)//全部在区间的,懒标 
			lz[i]+=k;
		for(int i=l;i<=R[x];i++)//部分在区间的,暴力 
			a[i]+=k;
		sum[x]+=k*(R[x]-l+1);
		for(int i=L[y];i<=r;i++)
			a[i]+=k;
		sum[y]+=k*(r-L[y]+1);
	}
}

inline int query(int l,int r)
{
    
    
	int x=pos[l],y=pos[r];//得到所在块的位置 
	int ans=0;
	if(x==y)//块包含了这个区间,暴力 
	{
    
    
		for(int i=l;i<=r;i++)
			ans+=a[i];
		ans+=lz[x]*(r-l+1);
	}
	else
	{
    
    
		for(int i=x+1;i<=y-1;i++)//处理懒标 
			ans+=sum[i]+lz[i]*(R[i]-L[i]+1);
		for(int i=l;i<=R[x];i++)//暴力 
			ans+=a[i];
		ans+=lz[x]*(R[x]-l+1);
		for(int i=L[y];i<=r;i++)
			ans+=a[i];
		ans+=lz[y]*(r-L[y]+1);
	}
	return ans;
}

signed main()
{
    
    
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	build();//预处理 
	int cas,x,y,k;
	for(int i=1;i<=m;i++)
	{
    
    
		scanf("%lld",&cas);
		if(cas==1)
		{
    
    
			scanf("%lld%lld%lld",&x,&y,&k);
			update(x,y,k);
		}
		else
		{
    
    
			scanf("%lld%lld",&x,&y);
			cout<<query(x,y)<<endl;
		}
	}
	return 0;
}

再来一道分块题目传送门HDU4417

题目要求我们求出区间内小于等于h的数的个数。
首先分块。
我们不能把块排序因为会改变位置,但我们可以复制一份,将新的数组tmp排序,对于每一块。
对于被包含在区间的块,只需在tmp中找到临界的点,它以下的点必定都小于等于h,可以使用upper_bound找到第一个大于h的数,那么它以下的数就一定小于等于h了。
对于部分在区间的块,我们就只能枚举这些数,是否小于等于h,因为排序是对整体进行排序,局部是无法处理的。
代码实现:

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5;
int L[N],R[N],pos[N],tmp[N],a[N];
int n,m,t;

inline void build()
{
    
    
	int t=sqrt(n*1.0);
	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<=num;i++)
	{
    
    
		for(int j=L[i];j<=R[i];j++)
			pos[j]=i;
		sort(tmp+L[i],tmp+R[i]+1);
	}
}

inline int query(int l,int r,int h)
{
    
    
	int ans=0;
	if(pos[l]==pos[r])
	{
    
    
		for(int i=l;i<=r;i++)
			if(a[i]<=h) ans++;
	}
	else
	{
    
    
		for(int i=l;i<=R[pos[l]];i++)
			if(a[i]<=h) ans++;
		for(int i=L[pos[r]];i<=r;i++)
			if(a[i]<=h) ans++;
		for(int i=pos[l]+1;i<=pos[r]-1;i++)
			ans+=upper_bound(tmp+L[i],tmp+R[i]+1,h)-tmp-L[i];
	}
	return ans;
}

int main()
{
    
    
	scanf("%d",&t);
	for(int o=t;o<=t;o++)
	{
    
    
		printf("Case %d:\n",o);
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;i++) scanf("%d",&a[i]),tmp[i]=a[i];
		build();
		int x,y,h;
		for(int i=1;i<=m;i++)
		{
    
    
			scanf("%d%d%d",&x,&y,&h);
			cout<<query(x+1,y+1,h)<<endl;
		}
	}
	return 0;
}

Guess you like

Origin blog.csdn.net/pigonered/article/details/121128459