【算法笔记】数列分块入门

前言

分块不能说是一种数据结构,它是一种思想,无论是数列分块,块状链表,还是数论分块,莫队算法,都应用了分块的思想。

本文主要介绍狭义上的分块,即数列分块。

数列分块的引入

数列分块可以说是暴力,一种优美的暴力,它的基本思路是,把数列分成若干块(一般取 n \sqrt n n ),分块预处理。块内预处理
在修改时,块内直接修改标记(别告诉我你不会线段树),块外暴力修改(同时更新数据)

同理,查询时块内直接看预处理的数据和标记,块外(边角料)暴力。

在这里插入图片描述

数列分块的复杂度是 O ( n n ) O(n\sqrt n) O(nn ),肯定比线段树或树状数组的 O ( n log ⁡ n ) O(n\log n) O(nlogn)要慢,但是它容易写,而且直观,可以解决一些线段树无法维护的问题,在复杂度允许的情况下可以使用。

数列分块复杂度证明(可跳过)

设对数列分成 T T T块。

最坏情况下需要修改 S 1 < T S_1< T S1<T块。

块外最坏情况下需要修改 S 2 < 2 n T S_2<\frac{2n}{T} S2<T2n个元素。

整体操作 ≤ S 1 + S 2 \le S_1+S_2 S1+S2次。

由均值不等式,

S 1 + S 2 2 ≤ S 1 S 2 \frac{S_1+S_2}{2} \le \sqrt {S_1S_2} 2S1+S2S1S2

S 1 + S 2 ≤ 2 T × 2 n T S_1+S_2 \le 2 \sqrt {T \times \frac{2n}{T}} S1+S22T×T2n

故数列分块单次操作最坏情况下小于

2 2 n 2 \sqrt {2n} 22n

忽略常数,则数列分块总体复杂度为

q n q \sqrt n qn

q q q n n n同阶,则时间复杂度为

O ( n n ) O(n\sqrt n) O(nn )

注:刚学均值不等式没几天,证明的可能不对,如果有错误请评论或私信我指出。

代码详解

loj. 数列分块入门1为例。

预处理

在预处理中,会处理以下几个变量。

R[],L[]代表每一个块的左右边界(其实可以不处理这个,但是写起来比较麻烦)

pos[]保存着每一个数所在的块。

int t=sqrt(n);//分t个块
for(int i=1;i<=t;i++) L[i]=(i-1)*t+1,R[i]=i*t;//处理出每个块的左右边界
if(R[t]<n) t++,L[t]=R[t-1]+1,R[t]=n;//分块后最后一部分很可能不在块内,所以要增加一个块
for(int i=1;i<=t;i++)
	for(int j=L[i];j<=R[i];j++) pos[j]=i;//处理出每个数所在块的编号

注意,分块后最后一部分很可能不在块内,所以要增加一个块。

这个预处理的时间复杂度为 O ( n ) O(n) O(n)

修改

修改操作采用的是”块内修改标记,块外暴力修改“的策略。

void change(int l,int r,int k){
	int p=pos[l],q=pos[r];//找到左右边界所在的块
	if(p==q){
		//如果这个区间在一个块内,就直接暴力修改
		for(int i=l;i<=r;i++) a[i]+=k;
	}
	else{
		for(int i=l;i<=R[p];i++) a[i]+=k;//整块左边的”边角料“,暴力修改
		for(int i=p+1;i<=q-1;i++) add[i]+=k;//对于整块,直接修改标记
		for(int i=L[q];i<=r;i++) a[i]+=k;//整块右边的”边角料“
	}
}

这就是分块的基本操作,接下来看几道例题。

例题

数列分块入门 2

原题

最开始看到这题,我以为是树套树,后来老师讲解才发现,分块真™暴力。

题意:区间加法,询问区间内小于某个值 x x x的元素个数。

分析:这题看起来很难,实际上直接查询操作直接暴力枚举即可。

AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10;
int a[N],L[N],R[N],add[N],pos[N];
int n;
void change(int l,int r,int k){
	int p=pos[l],q=pos[r];
	if(p==q){
		for(int i=l;i<=r;i++) a[i]+=k;
	}
	else{
		for(int i=l;i<=R[p];i++) a[i]+=k;
		for(int i=p+1;i<=q-1;i++) add[i]+=k;
		for(int i=L[q];i<=r;i++) a[i]+=k;
	}
}
int query(int l,int r,int k){
	int cnt=0;
	for(int i=l;i<=r;i++){
		if(a[i]+add[pos[i]]<k*k) cnt++;
	}
	return cnt;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	int t=sqrt(n);
	for(int i=1;i<=t;i++) L[i]=(i-1)*t+1,R[i]=i*t;
	if(R[t]<n) t++,L[t]=R[t-1]+1,R[t]=n;
	for(int i=1;i<=t;i++)
		for(int j=L[i];j<=R[i];j++) pos[j]=i;
	for(int i=1;i<=n;i++){
		int opt,l,r,k;
		scanf("%d%d%d%d",&opt,&l,&r,&k);
		if(opt) printf("%d\n",query(l,r,k));
		else change(l,r,k);
	}
}

数列分块入门 3

原题

题意:区间加法,询问区间内小于某个值 x x x的前驱(比其小的最大元素)。

分析:

考虑每个块建一个vector,块内排序,每次散块修改就重构,复杂度大概是预处理 O ( n log ⁡ n ) O(n\log n) O(nlogn),散块重构:散块不超过 2 n 2 \sqrt n 2n ,共 O ( n n log ⁡ n ) O(n\sqrt n\log n) O(nn logn),访问前驱:散块直接暴力,整块每块内用lower_bound函数找,共 O ( n n log ⁡ n ) O(n\sqrt n\log n) O(nn logn),总复杂度 O ( n n log ⁡ n ) O(n\sqrt n\log n) O(nn logn)

另一种做法是用set维护,预处理插入进set复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn),散块直接删除再插入,复杂度也是 O ( n n log ⁡ n ) O(n\sqrt n\log n) O(nn logn),访问前驱也是散块暴力,整块lower_bound,总复杂度两种方法相同,都是 O ( n n log ⁡ n ) O(n\sqrt n\log n) O(nn logn)

我的代码使用了set,毕竟自动排序常数可能小点?(雾)

AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,inf=0x3f3f3f3f;
int a[N],L[N],R[N],add[N],pos[N];
set<int>s[N];
int n;
void change(int l,int r,int k){
	int p=pos[l],q=pos[r];
	if(p==q){
		for(int i=l;i<=r;i++){
			s[pos[i]].erase(a[i]);
			a[i]+=k;
			s[pos[i]].insert(a[i]);
		}
	}
	else{
		for(int i=l;i<=R[p];i++){
			s[pos[i]].erase(a[i]);
			a[i]+=k;
			s[pos[i]].insert(a[i]);
		}
		for(int i=p+1;i<=q-1;i++) add[i]+=k;
		for(int i=L[q];i<=r;i++){
			s[pos[i]].erase(a[i]);
			a[i]+=k;
			s[pos[i]].insert(a[i]);
		}
	}
}
int query(int l,int r,int k){
	int p=pos[l],q=pos[r],ans=-1;
	if(p==q){
		for(int i=l;i<=r;i++){
			if(a[i]+add[pos[i]]<k) ans=max(ans,a[i]+add[pos[i]]);
		}
	}
	else{
		for(int i=l;i<=R[p];i++){
			if(a[i]+add[pos[i]]<k) ans=max(ans,a[i]+add[pos[i]]);
		}
		for(int i=p+1;i<=q-1;i++){
			int d=k-add[i];
			auto it=s[i].lower_bound(d);
			if(it==s[i].begin()) continue;
			it--;
			ans=max(ans,*it+add[i]);
		}
		for(int i=L[q];i<=r;i++){
			if(a[i]+add[pos[i]]<k) ans=max(ans,a[i]+add[pos[i]]);
		}
	}
	return ans;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	int t=sqrt(n);
	for(int i=1;i<=t;i++) L[i]=(i-1)*t+1,R[i]=i*t;
	if(R[t]<n) t++,L[t]=R[t-1]+1,R[t]=n;
	for(int i=1;i<=t;i++){
		s[i].insert(-1);
		for(int j=L[i];j<=R[i];j++) pos[j]=i,s[i].insert(a[j]);
	}
	for(int i=1;i<=n;i++){
		int opt,l,r,k;
		scanf("%d%d%d%d",&opt,&l,&r,&k);
		if(opt) printf("%d\n",query(l,r,k));
		else change(l,r,k);
	}
}

数列分块入门 4

原题

题意:区间加法,区间求和。

分析:多维护一个数组sum,表示这个块的和,修改时散块也更新sum,整块也更新sum。

AC代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+10;
int a[N],L[N],R[N],add[N],pos[N],sum[N];
int n;
void change(int l,int r,int k){
	int p=pos[l],q=pos[r];
	if(p==q){
		for(int i=l;i<=r;i++) a[i]+=k,sum[pos[i]]+=k;
	}
	else{
		for(int i=l;i<=R[p];i++) a[i]+=k,sum[pos[i]]+=k;
		for(int i=p+1;i<=q-1;i++) add[i]+=k,sum[i]+=(R[i]-L[i]+1)*k;
		for(int i=L[q];i<=r;i++) a[i]+=k,sum[pos[i]]+=k;
	}
}
int query(int l,int r){
	int p=pos[l],q=pos[r],ans=0;
	if(p==q){
		for(int i=l;i<=r;i++) ans+=a[i]+add[pos[i]];
	}
	else{
		for(int i=l;i<=R[p];i++) ans+=a[i]+add[pos[i]];
		for(int i=p+1;i<=q-1;i++) ans+=sum[i];
		for(int i=L[q];i<=r;i++) ans+=a[i]+add[pos[i]];
	}	
	return ans;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	int t=sqrt(n);
	for(int i=1;i<=t;i++) L[i]=(i-1)*t+1,R[i]=i*t;
	if(R[t]<n) t++,L[t]=R[t-1]+1,R[t]=n;
	for(int i=1;i<=t;i++){
		sum[i]=add[i]=0;
		for(int j=L[i];j<=R[i];j++) pos[j]=i,sum[i]+=a[j];
	}
	for(int i=1;i<=n;i++){
		int opt,l,r,k;
		cin>>opt>>l>>r>>k;
		if(opt) cout<<query(l,r)%(k+1)<<endl;
		else change(l,r,k);
	}
}

习题

  1. loj上分块入门5–9
  2. ynoi大分块
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值