Loading

详解分块与块状链表

普通分块

思想

对于一个长度为 \(n\) 数组,我们可以将它分解为 \(\sqrt n\) 块,这样每一块都会有 \(\sqrt n\) 个元素。

如果我们要将一段区间加上 \(d\),那么这一段区间包含块的会分两种:

  • 完整块,数量 \(\le \sqrt n\) 块。

  • 残缺块,最多两个,每个包含的元素个数 \(<\sqrt n\)

这样一个区间就会分为:少于 \(\sqrt n\) 个完整块 \(+\) 两个长度 \(<\sqrt n\) 的残缺块。

这样我们就可以将单次的时间复杂度降到 \(O(\sqrt n)\)

例题

题目

对于每个块,我们可以记录两个值:

  • add,指本块中所有的数都加上 add

  • sum,表示本块的真实和是多少(算上 add)。

对于两个操作,可以这样做:

  • 将这个区间加上 \(d\),可以先将完整块的 add+=d,sum+=d*length,再暴力将残缺块的每个元素的 w[i]+d,同时将整个块的 sum+d

  • 查询这个区间的和,也是类似。先累加完整段的 sum,再暴力累加残缺块的每个元素的 w[i]+add

参考代码:

#include<bits/stdc++.h>
#pragma GCC optimize(3,"Ofast","inline")
#define endl '\n'
#define int long long
#define mems(a,b) memset(a,b,sizeof a)
#ifdef PII
#define x first
#define y second
#endif
using namespace std;
template<typename T>
using _heap=priority_queue<T,vector<T>,greater<T>>;
const int N=100010,M=400;
int n,m;
int w[N];
int add[M],sum[M];
int len;
int get(int u){//映射到哪一块
	return u/len;
}
void change(int l,int r,int d){
	if(get(l)==get(r)){//块内直接暴力
		for(int i=l;i<=r;i++){
			w[i]+=d;
			sum[get(i)]+=d;
		}
	}
	else{
		int i=l,j=r;
		while(get(i)==get(l)){//残缺块
			w[i]+=d;
			sum[get(i)]+=d;
			i++;
		}
		while(get(j)==get(r)){//残缺块
			w[j]+=d;
			sum[get(j)]+=d;
			j--;
		}
		for(int k=get(i);k<=get(j);k++){//完整块
			sum[k]+=len*d;
			add[k]+=d;
		}
	}
}
int query(int l,int r){
	int ans=0;
	if(get(l)==get(r))
		for(int i=l;i<=r;i++) ans+=w[i]+add[get(i)];
	else{
		int i=l,j=r;
		while(get(i)==get(l)){
			ans+=w[i]+add[get(i)];
			i++;
		}
		while(get(j)==get(r)){
			ans+=w[j]+add[get(j)];
			j--;
		}
		for(int k=get(i);k<=get(j);k++) ans+=sum[k];
	}
	return ans;
}
signed main(){
	cin>>n>>m;
	len=sqrt(n);
	for(int i=1;i<=n;i++){
		scanf("%lld",&w[i]);
		sum[get(i)]+=w[i];
	}
	while(m--){
		char op[2];
		int l,r,d;
		scanf("%s%lld%lld",op,&l,&r);
		if(*op=='Q') printf("%lld\n",query(l,r));
		else{
			scanf("%lld",&d);
			change(l,r,d);
		}
	}
	return 0;
}

块状链表

tips:此数据结构十分毒瘤。

思想

由于普通的分块很难实现插入、删除等操作,所以我们就把块用双链表拼起来,以实现插入操作。示例如下:

需要注意的是,块状链表每一块的长度不固定。

块状链表可以实现下面几个操作:

  • 插入一段。
  1. 将要插入的地方那里的块断开。

  2. 将要插入的序列转化成块状链表。

  3. 按链表的方式插入。

  • 删除一段。
  1. 删掉开头的残缺块。

  2. 删掉中间的完整块。

  3. 删掉结尾的残缺块。

  • 合并。由于有些时候块状链表每个块里存的数很少,所以我们把几个很短的块合并,否则时间复杂度将不可控。

遍历整个链表,如果下一个块可以和当前块合并,就将下一个块合并到当前块中。定期执行即可。


本数据结构有两个点很毒瘤:

  1. 每次做题都要重新想一遍,不能背模板

  2. 细节极多。

例题

题目

可以先在字符串最前面插入一个字符,这样就不用处理特殊情况了。

每个块我们可以写成一个结构体:

struct node{
	char s[1145];
	int siz;//存了几个字符
	int l,r;//块状链表的左、右块是谁
};

现在看每个操作如何进行:

  • Move 操作:光标存两个数 \(x,y\),分别表示在第几块和第几个字符后面。从开头开始,看看块的长度和 \(k\) 那个大。如果 \(k> len\),就跳过这个块,并将 \(k\) 减去块长;如果 \(k\le len\),就停止,并更新 \(x,y\)\(O(\sqrt n)\)

  • Insert 操作:模板。\(O(\sqrt n)\)

  • Delete 操作:模板。\(O(\sqrt n)\)

  • Get 操作:与删除操作类似,只是将删除换成输出。\(O(\sqrt n)\)

  • Prev 操作:特判一下光标是不是在块的开头,如果是,就到前一个块,否则前移。\(O(1)\)

  • Next 操作:同上,相反。\(O(1)\)

参考代码:

#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=2010;//N表示块长,也是块的数量
int n;
int x,y;//光标
struct node{//块状链表
	char s[N];//不可以用string
	int siz;
	int l,r;
}p[N];
char str[2000010];//不可以用string
int q[N],tt;//内存回收,存当前所有可用的块,省空间
void add(int x,int u){//将块u插到块x的右边
	p[u].r=p[x].r;
	p[p[u].r].l=u;
	p[x].r=u;
	p[u].l=x;
}
void del(int u){//删除块u
	p[p[u].l].r=p[u].r;
	p[p[u].r].l=p[u].l;
	p[u].l=p[u].r=p[u].siz=0;//清空块u
	q[++tt]=u;//回收块u
}
void move(int k){
	x=p[0].r;//从开头开始
	while(k>p[x].siz){
		k-=p[x].siz;
		x=p[x].r;
	}
	y=k-1;
}
void insert(int k){
	if(y<p[x].siz-1){//分裂
		int u=q[tt--];//新建一个块
		for(int i=y+1;i<p[x].siz;i++) p[u].s[p[u].siz++]=p[x].s[i];//复制
		p[x].siz=y+1;
		add(x,u);
	}
	int cur=x;
	for(int i=0;i<k;){
		int u=q[tt--];//创建新的块
		while(p[u].siz<N && i<k) p[u].s[p[u].siz++]=str[i++];//将这个块塞满
		add(cur,u);
		cur=u;
	}
}
void remove(int k){
	if(p[x].siz-1-y>=k){//块内删除
		for(int i=y+k+1,j=y+1;i<p[x].siz;i++,j++) p[x].s[j]=p[x].s[i];//将后部分复制到前部分
		p[x].siz-=k;//更新长度
	}
	else{
		k-=p[x].siz-y-1;
		p[x].siz=y+1;//删除当前块的后部分
		while(p[x].r && k>=p[p[x].r].siz){//删除整段
			int u=p[x].r;
			k-=p[u].siz;
			del(u);
		}
		int u=p[x].r;//删除结尾块的前部分
		for(int i=0,j=k;j<p[u].siz;i++,j++) p[u].s[i]=p[u].s[j];//将后部分复制到前部分
		p[u].siz-=k;
	}
}
void get(int k){//与remove差不多
	if(p[x].siz-1-y>=k){//块内输出
		for(int i=0,j=y+1;i<k;i++,j++) putchar(p[x].s[j]);//删除换成输出
	}
	else{
		k-=p[x].siz-y-1;
		for(int i=y+1;i<p[x].siz;i++) putchar(p[x].s[i]);//输出当前块的后部分
		int cur=x;
		while(p[cur].r && k>=p[p[cur].r].siz){//输出整段
			int u=p[cur].r;
			for(int i=0;i<p[u].siz;i++) putchar(p[u].s[i]);
			k-=p[u].siz;
			cur=u;
		}
		int u=p[cur].r;
		for(int i=0;i<k;i++) putchar(p[u].s[i]);//输出结尾块的前部分
	}
	puts("");
}
void prev(){
	if(y==0){//特判,下标从0开始
		x=p[x].l;
		y=p[x].siz-1;
	}
	else y--;
}
void next(){//同prev函数
	if(y==p[x].siz-1){
		x=p[x].r;
		y=0;
	}
	else y++;
}
void merge(){//将长度较短的相邻块合并,保证块状链表时间复杂度的核心
	for(int i=p[0].r;i;i=p[i].r){//从第一个块开始枚举
		while(p[i].r && p[i].siz+p[p[i].r].siz<N){//下一个块存在,且可以合并
			int r=p[i].r;
			for(int j=p[i].siz,k=0;k<p[r].siz;j++,k++) p[i].s[j]=p[r].s[k];//将下一个块复制到当前块中
			if(x==r){//更新光标位置
				x=i;
				y+=p[i].siz;
			}
			p[i].siz+=p[r].siz;
			del(r);//删掉下一个块
		}
	}
}
int main(){
	for(int i=1;i<N;i++) q[++tt]=i;//现在所有块都可用
	cin>>n;
	char op[10];
	str[0]='>';
	insert(1);//插入哨兵
	move(1);//将光标移到哨兵后面
	while(n--){
		int a;
		scanf("%s",op);
		if(!strcmp(op,"Move")){//op=="Move"
			scanf("%d",&a);
			move(a+1);
		}
		else if(!strcmp(op,"Insert")){
			scanf("%d",&a);
			int i=0,k=a;// **回车
			while(a){
				str[i]=getchar();
				if(str[i]>=32 && str[i]<=126){
					i++;
					a--;
				}
			}
			insert(k);
			merge();
		}
		else if(!strcmp(op,"Delete")){
			scanf("%d",&a);
			remove(a);
			merge();
		}
		else if(!strcmp(op,"Get")){
			scanf("%d",&a);
			get(a);
		}
		else if(!strcmp(op,"Prev")) prev();
		else next();
	}
	return 0;
}
posted @ 2025-07-25 20:53  liushuangning  阅读(80)  评论(0)    收藏  举报