分块

分块

分块是一种优雅的暴力。 分块就是暴力。

支持大部分的区间操作(包括线段树不能做的),但是复杂度比线段树的 \(O(N\log N)\) 更劣,时间复杂度 \(O(N\sqrt N)\)

把一个长度为 \(N\) 的序列分成长度为 \(\sqrt N\)\(\sqrt N\) 个块。

对一个区间 \([L,R]\) 操作时,可以将这个区间分成两端的不完整的块和中间的至多 \(\sqrt N\) 的块。

对于不完整的块,可以直接暴力跑每个点,复杂度 \(O(\sqrt N)\)

对于中间完整的块,块中的所有元素肯定都是要被作的,可以像线段树那样打一个懒标记,等到真正遍历到那些点的时候再操作。

总时间复杂度 \(O(N\sqrt N)\)

例题:P13977 数列分块入门 2

用两个数组,一个维护原序列,一个维护一个块中的有序序列。

查询不完整块的时候直接暴力跑,查询完整块的时候在块中二分。

时间复杂度是 \(O(N\sqrt N\log\sqrt N)\)

#include<bits/stdc++.h>
#define int long long
//#define lc p<<1
//#define rc p<<1|1
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int N=3e5+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int lowbit(int x){
	return x&-x;
}
int n,m,k;
int T;
int a[N];
int b[N];
int len;
int cnt;
int L[N],R[N];
int belong[N];
int add[N];
void init(){
	for(int i=1;i<=n;i+=len){
		cnt++;
		L[cnt]=i,R[cnt]=min(n,i+len-1);
		for(int j=i;j<=i+len-1&&j<=n;j++){
			belong[j]=cnt;
			b[j]=a[j];
		}
		sort(b+L[cnt],b+R[cnt]+1);
	}
}
void update(int l,int r,int c){
	if(belong[l]==belong[r]){
		for(int i=l;i<=r;i++)a[i]+=c;
		for(int i=L[belong[l]];i<=R[belong[l]];i++)b[i]=a[i];
		sort(b+L[belong[l]],b+R[belong[l]]+1);
	}
	else{
		for(int i=l;i<=R[belong[l]];i++)a[i]+=c;
		for(int i=L[belong[l]];i<=R[belong[l]];i++)b[i]=a[i];
		sort(b+L[belong[l]],b+R[belong[l]]+1);
		for(int i=L[belong[r]];i<=r;i++)a[i]+=c;
		for(int i=L[belong[r]];i<=R[belong[r]];i++)b[i]=a[i];
		sort(b+L[belong[r]],b+R[belong[r]]+1);
		for(int i=belong[l]+1;i<=belong[r]-1;i++){
			add[i]+=c;
		}
	}
}
int query(int l,int r,int c){
	int res=0;
	if(belong[l]==belong[r]){
		for(int i=l;i<=r;i++)res+=(a[i]+add[belong[l]]<c);
	}
	else{
		for(int i=l;i<=R[belong[l]];i++)res+=(a[i]+add[belong[l]]<c);
		for(int i=L[belong[r]];i<=r;i++)res+=(a[i]+add[belong[r]]<c);
		for(int i=belong[l]+1;i<=belong[r]-1;i++){
			int RR=lower_bound(b+L[i],b+R[i]+1,c-add[i])-b;
			if(RR<L[i])continue;
			res+=RR-L[i];
		}
	}
	return res;
}
signed main(){
	//ios::sync_with_stdio(0);
	n=read();
	for(int i=1;i<=n;i++)a[i]=read();
	len=sqrt(n);
	init();
	for(int i=1;i<=n;i++){
		int op=read(),l=read(),r=read(),c=read();
		if(op==0){
			update(l,r,c);
		}
		else{
			print(query(l,r,c*c)),endl;
		}
	}
}

块状链表

用来维护一个序列(插入元素,删除元素)。

前面的分块实现的功能都不包括元素的增删。

除了平衡树以外,还有一个东西能维护序列——链表。

链表的优点是插入 \(O(1)\),但是查找一个元素需要 \(O(N)\),可以利用分块的思想将两个操作的时间复杂度均衡一下。

将原本链表中的一个元素改成一个块,快的长度是 \(\sqrt N\),所以总的块数就是 \(\sqrt N\) 个。这样插入和删除都是 \(O(\sqrt N)\) 的。

例题:P4008 [NOI2003] 文本编辑器

这道题最好写成迭代的版本,递归会炸。

#include<bits/stdc++.h>
//#define lc p<<1
//#define rc p<<1|1
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int N=1.5e6+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int lowbit(int x){
	return x&-x;
}
int n,m,k;
int T;
int len;
struct node{
	int pre;
	int nex;
	vector<char>num;
}f[N];
int cnt=1;
int a[N];
int cur;
string s;
int tot;
int dirt[N];
int new_block(){
	if(tot){
		int num=dirt[tot--];
		f[num]=node{0,0};
		f[num].num.clear();
		return num;
	}
	return ++cnt;
}
pair<int,int>find(int pos){
	int p=0;
	int sz=0;
	while(1){
		p=f[p].nex;
		sz+=f[p].num.size();
		if(sz>=pos)break;
	}
	sz-=f[p].num.size();
	return {p,pos-sz-1};
}
void insert(int p,int id,int num){
	while(1){
		char c=getchar();
		while(c=='\n'||c=='\r')c=getchar();
		f[p].num.insert(f[p].num.begin()+1+id,c);
		num--;
		if(!num)return;
		if(f[p].num.size()>2*len){
			int cnt=new_block();
			for(int i=len;i<f[p].num.size();i++)f[cnt].num.push_back(f[p].num[i]);
			while(f[p].num.size()>len)f[p].num.pop_back();
			f[cnt].pre=p;
			f[cnt].nex=f[p].nex;
			if(f[p].nex)f[f[p].nex].pre=cnt;
			f[p].nex=cnt;
			if(id+1>=f[p].num.size()){
				id-=f[p].num.size();
				p=cnt;
			}
		}
		id++;
	}
}
void del(int p,int id,int num){
	while(1){
		int has=f[p].num.size()-id;
		if(has<=num){
			if(has==f[p].num.size()){
				f[f[p].pre].nex=f[p].nex;
				f[f[p].nex].pre=f[p].pre;
				num-=f[p].num.size();
				int nex=f[p].nex;
				dirt[++tot]=p;
				if(!num){
					if(f[0].nex==0){
						int nn=new_block();
						f[0].nex=nn;
					}
					return;
				}
				p=nex;
			}
			else{
				while(has--){
					f[p].num.pop_back();
					num--;
				}
				if(!num)return;
				p=f[p].nex;
			}
			id=0;
		}
		else{
			vector<char>new_node;
			for(int i=0;i<id;i++)new_node.push_back(f[p].num[i]);
			for(int i=id+num;i<f[p].num.size();i++)new_node.push_back(f[p].num[i]);
			f[p].num=new_node;
			new_node.clear();
			return;
		}
	}
}
void output(int p,int id,int num){
	while(1){
		cout<<f[p].num[id];
		num--;
		if(!num)return;
		id++;
		if(id==f[p].num.size())p=f[p].nex,id=0;
	}
}
signed main(){
//	freopen("P4008_1.in","r",stdin);
//	freopen("fifi.txt","w",stdout);
	//ios::sync_with_stdio(0);
	f[0].nex=1;
	cin>>m;
	len=sqrt(m);
	while(m--){
		string op;
		cin>>op;
		if(op=="Move"){
			cin>>cur;
		}
		else if(op=="Insert"){
			pair<int,int>ccc=find(cur);
			cin>>n;
//			cin>>ws;
//			getline(cin,s);
//			reverse(s.begin(),s.end());
			insert(ccc.first,ccc.second,n);
		}
		else if(op=="Delete"){
			pair<int,int>ccc=find(cur+1);
			cin>>n;
			del(ccc.first,ccc.second,n);
		}
		else if(op=="Get"){
			pair<int,int>ccc=find(cur+1);
			cin>>n;
			output(ccc.first,ccc.second,n);
			cout<<'\n';
		}
		else if(op=="Prev"){
			cur--;
		}
		else{
			cur++;
		}
	}
}

根号分治

适用于:
一个题有两种做法(暴力方法),一种是查询 \(O(1)\),修改 \(O(N)\),一种是查询 \(O(N)\),修改 \(O(1)\)

我们需要找到一个阈值 \(B\),当 \(x<B\) 时,第一种方法更快。当 \(x>B\) 时,第二种方法更快。

比如:P3396 哈希冲突

两种方法:

  1. 暴力查找答案,修改直接修改原数组。
  2. 预处理所有情况,查询直接访问数组,修改要改 \(n\) 个元素。

当输入的 \(x\) 较小时,答案分布更密集,更多,明显预处理更优,但一次修改的复杂度是 \(O(B)\)

当输入的 \(x\) 较大时,答案分布更稀疏,更少,暴力就快一点,查询复杂度 \(O(\frac{N}{B})\)

要使整体的复杂度更优,就要两个情况的复杂度更接近,即 \(B=\frac{N}{B}\),那么 \(B=\sqrt N\)

单次操作复杂度 \(O(\sqrt N)\),总复杂度 \(O(M\sqrt N)\)

#include<bits/stdc++.h>
//#define int long long
//#define lc p<<1
//#define rc p<<1|1
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
const int N=1e6+5;
const int M=1e3+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int lowbit(int x){
	return x&-x;
}
int n,m,k;
int T;
int a[N];
int mod[M][M];
signed main(){
	ios::sync_with_stdio(0);
	cin>>n>>m;
	int len=sqrt(n);
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=len;i++){
		for(int j=1;j<=n;j++){
			mod[i][j%i]+=a[j];
		}
	}
	while(m--){
		char op;
		int x,y;
		cin>>op>>x>>y;
		if(op=='A'){
			if(x<=len){
				cout<<mod[x][y]<<'\n';
			}
			else{
				int res=0;
				for(int i=y;i<=n;i+=x)res+=a[i];
				cout<<res<<'\n';
			}
		}
		else{
			for(int i=1;i<=len;i++)mod[i][x%i]-=a[x];
			a[x]=y;
			for(int i=1;i<=len;i++)mod[i][x%i]+=a[x];
		}
	}
}
posted on 2026-07-21 19:14  fish2012  阅读(6)  评论(0)    收藏  举报