前言
lxlNB,lxl永远滴神.
块状链表
其基本定应用为:把一个长度为 \(n\) 的串,分成约块,相邻两块的大小不小于 \(\sqrt{n}\),每一块的大小不超过 \(2\sqrt{n}\)。这样就可以在的时间内解决一个插入、询问、拆分、合并等等的操作。
当然,并不是所有的块状链表都像Ynoi一样毒瘤,本篇文章按loj上入门题的顺序讲解数列分块(未必按难度排序).
loj入门题链接
在没有特别说明时块长取 \(\sqrt{n}\).
数列分块入门1
题目描述
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,单点查值。
分析
树状数组模板,用数列分块,对于每次修改操作时对于修改区间内完整的块可以直接打上一个加标记,对于不完整的部分可以暴力修改,在查询时只需要将当前位置的值+标记的值就可以了,时间复杂度 \(\mathcal{O}(n\sqrt{n})\).
代码
#include
#define REP(i,first,last) for(register int i=first;i<=last;++i)
#define DOW(i,first,last) for(register int i=first;i>=last;--i)
using namespace std;
const int MAXN=1e5+5;
const int SQRT_MAXN=505;
const long long INF=1e18;
int n;
int len,len_;//记录块长
int id[MAXN];//记录每个点所属的块
int l[MAXN],r[MAXN];//记录每个块的区间
long long lazy[SQRT_MAXN];//记录标记
long long arr[MAXN];//记录序列
void Updata(int left,int right,long long add)//修改操作
{
if(id[left]==id[right])//对于同一个块内就直接暴力修改
{
REP(i,left,right)
{
arr[i]+=add;
}
return;
}
REP(i,id[left]+1,id[right]-1)//对于完整的块打上一个标记
{
lazy[i]+=add;
}
//对于不完整的块也直接暴力修改
REP(i,left,r[id[left]])
{
arr[i]+=add;
}
REP(i,l[id[right]],right)
{
arr[i]+=add;
}
}
long long Query(int place)
{
return arr[place]+lazy[id[place]];//输出当前位置的值+当前块的标记
}
int main()
{
scanf("%d",&n);
REP(i,1,n)
{
scanf("%lld",&arr[i]);
}
len=sqrt(n);//块长为根号n
len_=len*3;
id[1]=0;//预处理每个块的区间以及每个点所属的块
l[0]=1;
REP(i,2,n)
{
id[i]=(i-1)/len;
if(id[i]^id[i-1])
{
l[id[i]]=i;
r[id[i-1]]=i-1;
}
}
r[id[n]]=n;
int opt,left,right;
long long x,answer;
REP(i,1,n)
{
scanf("%d%d%d%lld",&opt,&left,&right,&x);
if(opt==0)
{
Updata(left,right,x);
}
if(opt==1)
{
printf("%lld\n",Query(right));
}
}
return 0;
}
数列分块入门2
题目描述
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的元素个数。
分析
需要查询区间内小于某个数的个数这个东西看起来很难搞,那么考虑对一个有序的序列来查询,可以发现只要直接二分就行了,那么考虑对于每个块维护两个序列,一个是原序列,一个是排完序的序列,可以发现在修改的时候中间的块中排完序的序列每个数的位置并不会发生改变,所以并不需要重新排序,对于不完整的块就暴力修改,暴力排序,查询时对于中间的块直接二分即可,两边的块暴力查询.时间复杂度 \(\mathcal{O}(n\sqrt{n}\log_2\sqrt{n})\).
代码
#include
#define REP(i,first,last) for(register int i=first;i<=last;++i)
#define DOW(i,first,last) for(register int i=first;i>=last;--i)
using namespace std;
const int MAXN=1e5+5;
const int SQRT_MAXN=233;
int n;
int len,len_;//记录块长
int id[MAXN];//记录每个点所属的块
int l[MAXN],r[MAXN];//记录每个块的区间
long long lazy[SQRT_MAXN];//记录标记
long long arr[MAXN];//记录原序列
long long sor[MAXN];//记录排完序的序列
void Sort(int id)//排序操作
{
REP(i,l[id],r[id])
{
sor[i]=arr[i];
}
sort(sor+l[id],sor+r[id]+1);
}
void Updata(int left,int right,long long add)//修改操作
{
if(id[left]==id[right])//在同一个块就暴力修改然后重新排序
{
REP(i,left,right)
{
arr[i]+=add;
}
Sort(id[left]);
return;
}
REP(i,id[left]+1,id[right]-1)//对于中间的块直接修改标记就可以了
{
lazy[i]+=add;
}
//对于两边不完整的块保留修改后排序
REP(i,left,r[id[left]])
{
arr[i]+=add;
}
Sort(id[left]);
REP(i,l[id[right]],right)
{
arr[i]+=add;
}
Sort(id[right]);
}
int Smaller(int id,long long x)//二分
{
int left=l[id];//二分的范围
int right=r[id];
int answer=-1;
int middle;
while(left<=right)
{
middle=(left+right)>>1;
if(sor[middle]+lazy[id]/*需要加上标记*/
数列分块入门3
题目描述
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的前驱(比其小的最大元素)。
分析
这题和数列分块入门2几乎相同,也可以记录下排完序后的序列,然后对于完整的块二分出第一个小于 \(x\) 的值,最后只要取一个 \(\max\) 就好了.时间复杂度 \(\mathcal{O}(n\sqrt{n}\log_2\sqrt{n})\)
代码
#include
#define REP(i,first,last) for(register int i=first;i<=last;++i)
#define DOW(i,first,last) for(register int i=first;i>=last;--i)
using namespace std;
const int MAXN=1e5+5;
const int SQRT_MAXN=505;
const long long INF=1e18;
int n;
int len,len_;
int id[MAXN];
int l[MAXN],r[MAXN];
long long lazy[SQRT_MAXN];
long long arr[MAXN];
long long sor[MAXN];
void Sort(int id)
{
REP(i,l[id],r[id])
{
sor[i]=arr[i];
}
sort(sor+l[id],sor+r[id]+1);
}
void Updata(int left,int right,long long add)//修改操作,和上题一样
{
if(id[left]==id[right])
{
REP(i,left,right)
{
arr[i]+=add;
}
Sort(id[left]);
return;
}
REP(i,id[left]+1,id[right]-1)
{
lazy[i]+=add;
}
REP(i,left,r[id[left]])
{
arr[i]+=add;
}
Sort(id[left]);
REP(i,l[id[right]],right)
{
arr[i]+=add;
}
Sort(id[right]);
}
long long Smaller(int id,long long x)//二分答案
{
int left=l[id];
int right=r[id];
int answer=l[id];
int middle;
while(left<=right)
{
middle=(left+right)>>1;
if(sor[middle]+lazy[id]
数列分块入门4
题目描述
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,区间求和。
分析
和数列分块1挺像的,改成了区间查询和而已,那么考虑维护一个每一块的和,然后对于完整的块直接查询这一块的和,不完整的直接暴力查询.时间复杂度 \(\mathcal{O}(n\sqrt{n})\).
代码
#include
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int MAXN=1e5+5;
const int SQRT_MAXN=505;
const long long INF=1e18;
int n;
int len,len_;
int id[MAXN];
int l[MAXN],r[MAXN];
long long lazy[SQRT_MAXN];
long long arr[MAXN];
long long sum[MAXN];//在第一题的基础上记录每个块的和
void Updata(int left,int right,long long add)
{
if(right-left
数列分块入门5
题目描述
给出一个长为 n 的数列 \(a\),以及 \(n\) 个操作,操作涉及区间开方,区间求和。
分析
对于一个正整数 \(x\) 开 \(log_2log_2x\) 就会变成 \(1\),那么考虑维护一下每个块中的最大值,如果最大值 \(\leq1\),那么就不用修改了,如果最大值 \(>1\),那么考虑暴力修改,时间复杂度 \(\mathcal{O}(n\sqrt{n}+n\log_2\log_2a_i)\).
代码
#include
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int MAXN=1e5+5;
const int SQRT_MAXN=505;
int n;
int len,len_;
int id[MAXN];
int l[MAXN],r[MAXN];
long long sum[SQRT_MAXN];//记录块的和
long long arr[MAXN];//记录序列
long long max_num[SQRT_MAXN];//当前块中的最大值
void ChangeBlock(int id)//修改这个块需要维护的东西
{
max_num[id]=arr[l[id]];
sum[id]=arr[l[id]];
REP(i,l[id]+1,r[id])
{
sum[id]+=arr[i];
max_num[id]=max(max_num[id],arr[i]);
}
}
void Updata(int left,int right)
{
if(id[left]==id[right])//在同一个块内就暴力修改
{
REP(i,left,right)
{
arr[i]=sqrt(arr[i]);
}
ChangeBlock(id[left]);//需要修改维护的信息
return;
}
REP(i,id[left]+1,id[right]-1)//对于完整的块
{
if(max_num[i]>1)//如果最大值>1,那么就暴力修改
{
REP(j,l[i],r[i])
{
arr[j]=sqrt(arr[j]);
}
ChangeBlock(i);
}
}
//对于不完整的块直接暴力修改
REP(i,left,r[id[left]])
{
arr[i]=sqrt(arr[i]);
}
ChangeBlock(id[left]);
REP(i,l[id[right]],right)
{
arr[i]=sqrt(arr[i]);
}
ChangeBlock(id[right]);
}
long long Query(int left,int right)//查询方式与数列分块入门4相同
{
if(id[left]==id[right])
{
long long result=0;
REP(i,left,right)
{
result+=arr[i];
}
return result;
}
long long result=0;
REP(i,id[left]+1,id[right]-1)
{
result+=sum[i];
}
REP(i,left,r[id[left]])
{
result+=arr[i];
}
REP(i,l[id[right]],right)
{
result+=arr[i];
}
return result;
}
int main()
{
scanf("%d",&n);
REP(i,1,n)
{
scanf("%lld",&arr[i]);
}
len=sqrt(n);
len_=len*3;
id[1]=0;
l[0]=1;
REP(i,2,n)
{
id[i]=(i-1)/len;
if(id[i]^id[i-1])
{
l[id[i]]=i;
r[id[i-1]]=i-1;
}
}
r[id[n]]=n;
REP(i,0,id[n])//预处理一下每个块要维护的内容
{
ChangeBlock(i);
}
int opt,left,right,c;
REP(i,1,n)
{
scanf("%d%d%d%d",&opt,&left,&right,&c);
if(opt==0)
{
Updata(left,right);
}
if(opt==1)
{
printf("%lld\n",Query(left,right));
}
}
return 0;
}
数列分块入门6
题目描述
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及单点插入,单点询问,数据随机生成。
分析
单点插入这个东西不是平衡树裸题吗,所以我们要用分块来解决他,考虑对于每个块开一个vector,然后从头开始暴力遍历,然后找到需要插入的位置所在的块,暴力插入就可以了,因为数据是随机的,所以也是可以过的.
代码
#include
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int MAXN=2e5+5;
const int MAX_SQRTN=100005;//稍微开大一点
int n;
int len;
int block_cnt=0;
int to[MAX_SQRTN];
vectorvec[MAX_SQRTN];//对于每个块开一个vector存
void Insert(int place,int num)//插入一个元素
{
int now=0;
int id=block_cnt;
for(id=1;id;id=to[id])//先遍历所有的块找到需要插入的元素所在的块和在该块中的位置
{
now+=vec[id].size();
if(now>=place)
{
break;
}
}
now-=vec[id].size();
now=place-now;
vec[id].insert(vec[id].begin()+now-1,num);//暴力插入这个块中
}
int Query(int place)//查询部分
{
int now=0;
int id;
for(id=1;id;id=to[id])//仍然是暴力遍历所有的块,找到查询的数所在的块
{
now+=vec[id].size();
if(now>=place)
{
break;
}
}
now-=vec[id].size();
now=place-now;
return vec[id][now-1];//直接输出即可
}
int main()
{
scanf("%d",&n);
len=sqrt(n);
int num;
REP(i,1,n)
{
scanf("%d",&num);
vec[(i-1)/len+1].push_back(num);
}
block_cnt=(n-1)/len+1;
REP(i,1,block_cnt-1)//用一个链表存储,方便优化(
{
to[i]=i+1;
}
int opt,l,r,c;
REP(i,1,n)
{
scanf("%d%d%d%d",&opt,&l,&r,&c);
if(opt==0)
{
Insert(l,r);
}
if(opt==1)
{
printf("%d\n",Query(r));
}
}
return 0;
}
但是这个做法最数据不随机的时候可能会出问题(如果全部都是插入一个块内,那么暴力插入的复杂度就会变成 \(\mathcal{O}(n)\)),所以需要考虑去优化这个东西.
优化
可以发现复杂度下降的原因是块过长导致的,所以在一个块太长时把它拆成两个块自然就不会有问题了,这个就是使用链表维护的好处,可以资瓷 \(\mathcal{O}(1)\) 插入一个块.
优化后的代码
#include
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int MAXN=2e5+5;
const int MAX_SQRTN=100005;
int n;
int len,len_;
int block_cnt=0;
int to[MAX_SQRTN];
vectorvec[MAX_SQRTN];
void Insert(int place,int num)
{
int now=0;
int id=block_cnt;
for(id=1;id;id=to[id])
{
now+=vec[id].size();
if(now>=place)
{
break;
}
}
now-=vec[id].size();
now=place-now;
vec[id].insert(vec[id].begin()+now-1,num);
if(len_=place)
{
break;
}
}
now-=vec[id].size();
now=place-now;
return vec[id][now-1];
}
int main()
{
scanf("%d",&n);
len=max((int)sqrt(n*1.5),2);
len_=len*3;
int num;
REP(i,1,n)
{
scanf("%d",&num);
vec[(i-1)/len+1].push_back(num);
}
block_cnt=(n-1)/len+1;
REP(i,1,block_cnt-1)
{
to[i]=i+1;
}
int opt,l,r,c;
REP(i,1,n)
{
scanf("%d%d%d%d",&opt,&l,&r,&c);
if(opt==0)
{
Insert(l,r);
}
if(opt==1)
{
printf("%d\n",Query(r));
}
}
return 0;
}
数列分块入门7
题目描述
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间乘法,区间加法,单点询问。
分析
看起来和只有区间加的做法差不多,但是既有加法又有乘法看起来就很难搞,在只有加法的做法中相当于有一个类似标记永久化的标记,但是既有加,又有乘,这个东西是没法"标记永久化"的,所以考虑在暴力修改的时候下传一下标记,再取修改.时间复杂度 \(\mathcal{O}(n\sqrt{n})\)
代码
#include
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int MAXN=1e5+5;
const int SQRT_MAXN=505;
const long long MOD=10007;
int n;
int len,len_;
int id[MAXN];
int l[MAXN],r[MAXN];
long long lazy[SQRT_MAXN];
long long lazy2[SQRT_MAXN];
long long arr[MAXN];
void Clean(int id)//下传标记
{
REP(i,l[id],r[id])
{
arr[i]=(arr[i]*lazy2[id]+lazy[id])%MOD;
}
lazy[id]=0;//清空标记
lazy2[id]=1;
}
void Updata1(int left,int right,long long add)//区间加
{
if(id[left]==id[right])//在同一个块内就暴力修改
{
Clean(id[left]);//先下传标记
REP(i,left,right)
{
arr[i]+=add;
}
return;
}
REP(i,id[left]+1,id[right]-1)//完整的块直接修改标记
{
lazy[i]+=add;
lazy[i]%=MOD;
}
Clean(id[left]);//在两边不完整的块修改的时候也要先下传标记再修改
REP(i,left,r[id[left]])
{
arr[i]+=add;
}
Clean(id[right]);
REP(i,l[id[right]],right)
{
arr[i]+=add;
}
}
void Updata2(int left,int right,long long pow)//区间乘
{
if(id[left]==id[right])//同一个块内暴力修改,和加法做法基本相同
{
Clean(id[left]);
REP(i,left,right)
{
arr[i]=arr[i]*pow%MOD;
}
return;
}
REP(i,id[left]+1,id[right]-1)
{
lazy2[i]=(lazy2[i]*pow)%MOD;
lazy[i]=(lazy[i]*pow)%MOD;//注意加法标记也需要乘上这个数
lazy[i]%=MOD;
}
Clean(id[left]);
REP(i,left,r[id[left]])
{
arr[i]=arr[i]*pow%MOD;
}
Clean(id[right]);
REP(i,l[id[right]],right)
{
arr[i]=arr[i]*pow%MOD;
}
}
long long Query(int place)//单点查询,直接加上(乘上)标记就好了,注意需要先乘后加
{
return (arr[place]*lazy2[id[place]]+lazy[id[place]])%MOD;
}
int main()
{
scanf("%d",&n);
REP(i,1,n)
{
scanf("%lld",&arr[i]);
}
len=sqrt(n);
len_=len*3;
id[1]=0;
l[0]=1;
REP(i,2,n)
{
id[i]=(i-1)/len;
if(id[i]^id[i-1])
{
l[id[i]]=i;
r[id[i-1]]=i-1;
}
}
REP(i,0,id[n])//需要清空标记
{
lazy[i]=0;
lazy2[i]=1;
}
r[id[n]]=n;
int opt,left,right;
long long x,answer;
REP(i,1,n)
{
scanf("%d%d%d%lld",&opt,&left,&right,&x);
if(opt==0)
{
Updata1(left,right,x);
}
if(opt==1)
{
Updata2(left,right,x);
}
if(opt==2)
{
printf("%lld\n",Query(right));
}
}
return 0;
}
数列分块入门8
题目描述
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间询问等于一个数 \(c\) 的元素,并将这个区间的所有元素改为 \(c\)。
分析
考虑记录一下当前块内的数是否都是一个相同的数,在操作时,如果全部是相同的数,那么如果等于 \(c\),就直接在答案中加上这个块的块长,如果不等于 \(c\),那么就直接修改为 \(c\),如果块内的数并不是全部相同就暴力修改.
关于时间复杂度的证明:先不管开始时的序列,考虑对于每次操作最多只会生成两块块内数并不是全部相同的块,而这修改这两个块的时间复杂度为 \(\mathcal{O}{\sqrt{n}}\),而开始时的序列相当于 \(\sqrt{n}\) 个块内有不同数的块,所以时间复杂度为 \(\mathcal{O}(n\sqrt{n})\).
代码
#include
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int MAXN=1e5+5;
const int SQRT_MAXN=505;
const long long INF=1e18;
int n;
int len,len_;
int id[MAXN];
int l[MAXN],r[MAXN];
long long color[SQRT_MAXN];
long long arr[MAXN];
void ChangeBlock(int id)
{
if(color[id]!=INF)
{
REP(i,l[id],r[id])
{
arr[i]=color[id];
}
color[id]=INF;
}
}
long long Do(int left,int right,int c)
{
if(id[left]==id[right])
{
ChangeBlock(id[left]);
long long result=0;
REP(i,left,right)
{
result+=arr[i]==c;
arr[i]=c;
}
return result;
}
long long result=0;
REP(i,id[left]+1,id[right]-1)
{
if(color[i]^INF)
{
if(color[i]==c)
{
result+=r[i]-l[i]+1;
}
}
else
{
ChangeBlock(i);
REP(j,l[i],r[i])
{
result+=arr[j]==c;
}
}
color[i]=c;
}
ChangeBlock(id[left]);
REP(i,left,r[id[left]])
{
result+=arr[i]==c;
arr[i]=c;
}
ChangeBlock(id[right]);
REP(i,l[id[right]],right)
{
result+=arr[i]==c;
arr[i]=c;
}
return result;
}
int main()
{
scanf("%d",&n);
REP(i,1,n)
{
scanf("%lld",&arr[i]);
}
len=sqrt(n);
len_=len*3;
id[1]=0;
l[0]=1;
REP(i,2,n)
{
id[i]=(i-1)/len;
if(id[i]^id[i-1])
{
l[id[i]]=i;
r[id[i-1]]=i-1;
}
}
r[id[n]]=n;
REP(i,0,id[n])
{
color[i]=INF;
}
int left,right,c;
REP(i,1,n)
{
scanf("%d%d%d",&left,&right,&c);
printf("%d\n",Do(left,right,c));
}
return 0;
}
数列分块入门9
题目描述
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及询问区间的最小众数。
分析
数列分块经典题,线段树做不到系列.
考虑成为众数有哪几种情况:
- 这个数为中间完整的块的众数.
- 这个数为两边的不完整的块内的数.
那么考虑先离散化,然后考虑记录下 \(max_{i,j}\) 表示在 \(i\) 到 \(j\) 的块中众数出现的次数,\(num_{i,j}\) 表示 \(i\) 到 \(j\) 的块中的众数.
然后考虑如何快速计算两边的数在这段区间内的出现次数,考虑用前缀和 \(sum_{i,j}\) 表示前 \(i\) 个块中 \(j\) 出现的次数,然后暴力统计出这些数在两边的块内的出现次数就可以快速快速计算了.时间复杂度 \(\mathcal{O}(n\sqrt{n})\).
代码
#include
#define REP(i,first,last) for(register int i=first;i<=last;++i)
#define DOW(i,first,last) for(register int i=first;i>=last;--i)
using namespace std;
const int MAXN=1e5+5;
const int SQRT_MAXN=505;
int n,m;
int len,len_;
int num_cnt=0;
int sum[SQRT_MAXN][MAXN];//前缀和
int id[MAXN];
int l[MAXN],r[MAXN];
int arr[MAXN];
int tot[MAXN];
int number[MAXN];//记录原来的数
struct Block
{
int max,num;
}bl[SQRT_MAXN][SQRT_MAXN];
struct Sor
{
int num,id;
}sor[MAXN];
bool Cmp(Sor a,Sor b)
{
return a.nummax||(tot[arr[i]]==max&&arr[i]max||(now==max&&arr[i]max||(now==max&&arr[i]bl[i][j].max
||(tot[arr[k]]==bl[i][j].max&&arr[k]<=bl[i][j].num))
{
bl[i][j].max=tot[arr[k]];
bl[i][j].num=arr[k];
}
}
}
}
int left,right;
REP(i,1,n)
{
scanf("%d%d",&left,&right);
printf("%d\n",number[Query(left,right)]);
}
return 0;
}