详解分块与块状链表
普通分块
思想
对于一个长度为 \(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:此数据结构十分毒瘤。
思想
由于普通的分块很难实现插入、删除等操作,所以我们就把块用双链表拼起来,以实现插入操作。示例如下:

需要注意的是,块状链表每一块的长度不固定。
块状链表可以实现下面几个操作:
- 插入一段。
将要插入的地方那里的块断开。
将要插入的序列转化成块状链表。
按链表的方式插入。
- 删除一段。
删掉开头的残缺块。
删掉中间的完整块。
删掉结尾的残缺块。
- 合并。由于有些时候块状链表每个块里存的数很少,所以我们把几个很短的块合并,否则时间复杂度将不可控。
遍历整个链表,如果下一个块可以和当前块合并,就将下一个块合并到当前块中。定期执行即可。
本数据结构有两个点很毒瘤:
-
每次做题都要重新想一遍,不能背模板
-
细节极多。
例题
题目。
可以先在字符串最前面插入一个字符,这样就不用处理特殊情况了。
每个块我们可以写成一个结构体:
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;
}

浙公网安备 33010602011771号