【详解】分块

概念

所谓“分块”,就是把一个序列分成一个个长度相等的块,那么对于一个长度为 n 的序列,我们一般可以将这个序列分成 \sqrt n 个长度为 \sqrt n 的块。

当然,对于某些毒瘤出题人,我们可以相应地将块长更改一下(例如 \sqrt n+1,\sqrt n -1 \sqrt n /2等)。

分块实现

那得先分块吧。

首先,至少会有 \sqrt n 个块,但是,可能会有多余的出来,所以要多分一个块。

局部代码:

(len 为块长,cnt 为块的数量)

len=sqrt(n);
cnt=n/len;
if(n%len)
	cnt++;

然后,对于第 i 个块,我们设 l_i 为它的左端点,r_i 为它的右端点。

易得:l_i=(i-1)\times len +1,r_i=i\times len

特别的,r_{cnt}=n

局部代码:

for(int i=1;i<=cnt;i++)
{
	l[i]=(i-1)*len+1;
	r[i]=i*len;
}
r[cnt]=n;

为了方便一些操作,我们令 pos_i 表示原序列中的第 i 个数在哪个块中。

代码也很简单:

for(int i=1;i<=cnt;i++)
{
	for(int j=l[i];j<=r[i];j++)
	{
		pos[j]=i;
	}
} 

就这样,块就分好了。

分块的基本操作

一、在序列的 [x,y] 内加上/减去 d

先对区间两端的不完整的块进行暴力修改,

对于中间的块:

和线段树懒标记类似的,我们可以设 lazy_i 表示第 i 个块的懒标记(其实也不算),那么块中的每个数就应该是 a_i+lazy_{pos_i}

二、查询序列中 [x,y] 的和

涉及到查询区间和,可以考虑先预处理出 sum_i(第 i 个块的和),那么上面的修改操作就需要对 sum_i 做出修改。

修改操作如何修改 sum?

对于两端的块,暴力改 sum;

中间的块,就加上 d 乘以块长。

查询也是一样,累加 sum 即可。

三、查询序列 [x,y] 中有多少个数大于/小于(等于)c

涉及到了大小关系,首先想到就是排序……

对块内排序(那么对于前面的修改操作,有对两端进行暴力修改,所以改后要对两端的块重新排序)。

对于两端的块:暴力枚举累计;

对于中间的块:二分找出满足条件的分界点,然后累计即可。

例题:教主的魔法

传送门icon-default.png?t=O83Ahttps://www.luogu.com.cn/problem/P2801模板题,都是分块的基本操作……

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,q;
int a[1000001];
int b[1000001];
int len;
int cnt;
int l[10001],r[10001],pos[1000001];
int lazy[10001];
void init()
{
    for(int i=1;i<=n;i++)
    {
        a[i]=b[i];
    }
	len=sqrt(n);
	cnt=n/len;
	if(n%len)
		cnt++;
	for(int i=1;i<=cnt;i++)
	{
		l[i]=(i-1)*len+1;
		r[i]=i*len;
	}
	r[cnt]=n;
	for(int i=1;i<=cnt;i++)
	{
		for(int j=l[i];j<=r[i];j++)
		{
			pos[j]=i;
			//cout<<i<<" "<<j<<endl;
		}
	} 
//	for(int i=1;i<=n;i++)
//	{
//		cout<<pos[i]<<" ";
//	}
//	cout<<endl;
	for(int i=1;i<=cnt;i++)
	{
		sort(a+l[i],a+r[i]+1);
	}
	
//	for(int i=1;i<=n;i++)
//		cout<<a[i]<<" ";
//	cout<<endl;
}
void change(int x,int y,int d)
{
	if(pos[x]==pos[y])
	{
		for(int i=x;i<=y;i++)
		{
			b[i]+=d;
		}
        for(int i=l[pos[x]];i<=r[pos[x]];i++)
        {
            a[i]=b[i];
        }
		sort(a+l[pos[x]],a+r[pos[x]]+1);
		return;
	}
	
	for(int i=x;i<=r[pos[x]];i++)
	{
		b[i]+=d;
	}
    for(int i=l[pos[x]];i<=r[pos[x]];i++)
    {
        a[i]=b[i];
    }
	sort(a+l[pos[x]],a+r[pos[x]]+1);
	
	
	
	for(int i=l[pos[y]];i<=y;i++)
	{
		b[i]+=d;
	}
	for(int i=l[pos[y]];i<=r[pos[y]];i++)
	{
		a[i]=b[i];
	}
	sort(a+l[pos[y]],a+r[pos[y]]+1);
	for(int i=pos[x]+1;i<=pos[y]-1;i++)
	{
		lazy[i]+=d;
	}
//	cout<<r[pos[x]]<<" "<<l[pos[y]]<<endl;
//	for(int i=1;i<=n;i++)
//		cout<<a[i]<<" ";
//	cout<<endl;
//	for(int i=1;i<=n;i++)
//		cout<<b[i]<<" ";
//	cout<<endl;
}
int query(int x,int y,int c)
{
	int res=0;
	if(pos[x]==pos[y])
	{
		for(int i=x;i<=y;i++)
		{
			if(b[i]+lazy[pos[x]]>=c)res++;
		}
		return res;
	}
	
	for(int i=x;i<=r[pos[x]];i++)
	{
	//	cout<<a[i]+lazy[pos[x]]<<" ";
		if(b[i]+lazy[pos[x]]>=c)res++;
	}
	for(int i=l[pos[y]];i<=y;i++)
	{
	//	cout<<a[i]+lazy[pos[y]]<<" ";
		if(b[i]+lazy[pos[y]]>=c)res++;
	}
//	cout<<res<<endl;
	int ll,rr,mid;
	int temp;
	for(int i=pos[x]+1;i<=pos[y]-1;i++)
	{
		ll=l[i],rr=r[i];
		temp=0;
		while(ll<=rr)
		{
			mid=(ll+rr)>>1;
			if(a[mid]+lazy[i]>=c)
			{
				temp=r[i]-mid+1;
				rr=mid-1;
			}
			else
				ll=mid+1;
		}
	//	cout<<i<<" "<<temp<<endl;
		res+=temp;
	}
//	cout<<lazy[2]<<endl;
//	for(int i=l[2];i<=r[2];i++)
//	{
//		cout<<a[i]<<" "<<b[i]<<endl;
//	}
	return res;
}
signed main()
{
//	freopen("aa.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	cin>>n>>q;
	for(int i=1;i<=n;i++)
	{
		cin>>b[i];
	}
	init();
	char ch;
	int x,y,z;
	while(q--)
	{
		cin>>ch;
		cin>>x>>y>>z;
		if(ch=='M')
		{
			change(x,y,z);
		}
		else{
			cout<<query(x,y,z)<<"\n";
		}
	}
	return 0;
}

最后,求赞勿喷

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值