关于分块
关于分块,其主要思想是大局维护,局部朴素;即吧一个序列分成若干个小块,对任意区间进行操作的时候:如果区间内包含完整块,就进行快速的整体修改;若所修改区间内的一部分是某个个块中的一部分,则直接进行暴力修改就行。根据有关数学知识,可以证明分成 n \sqrt n n的时间效率最高,一般的时间复杂度则是 O ( n n ) O(n\sqrt n) O(nn)
具体内容,我们可以通过六道分块习题进行理解。
分块入门1
给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。
这道题就是分块一个灰常简单的模板了,具体可以这么做:
- 设修改的区间为 [ l , r ] [l,r] [l,r],此时在分块中预处理了每一个区间的左端点和右端点,当前枚举为第i个区间。
- 若区间 i i i在 [ l , r ] [l,r] [l,r]以内,则直接用数组 a d d [ i ] add[i] add[i]进行区间的整体操作,表示第 i i i个区间的所有数的变化量,只需要累加一次。
- 若区间i的一部分在 [ l , r ] [l,r] [l,r]以内,暴力修改即可。直接在a数组上修改。
- 对于查询的点x,输出的值为:
a
[
i
]
+
a
d
d
[
p
o
s
i
]
a[i]+add[pos_i]
a[i]+add[posi],
p
o
s
i
pos_i
posi表示
i
i
i所属的块的编号。
代码如下:
(注意:代码可能与上述讲解的数组含义有所出入)
例如:上面的add是sum,而下面的add是单点变化量,等同于在原数组上做修改。
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[100000];
int L[100000];
int R[100000];
int add[100000];
int sum[100000];
int num[100000];
void Plus(int l,int r,int c)
{
for (int i=1;i<=m;++i)
{
if (l>R[i] || r<L[i]) continue;
if (l<=L[i] && r>=R[i]) sum[i]+=c;
else
{
for (int j=max(L[i],l);j<=min(R[i],r);++j) add[j]+=c;
}
}
}
void ask(int x)
{
printf("%d\n",a[x]+sum[num[x]]+add[x]);
}
int main(void)
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%d",&n);
for (int i=1;i<=n;++i) scanf("%d",a+i);
m=sqrt(n);
for (int i=1;i<=m;++i)
{
L[i]=sqrt(n)*(i-1)+1;
R[i]=sqrt(n)*i;
}
if (R[m]<n)
{
m++;
L[m]=R[m-1]+1;
R[m]=n;
}
for (int i=1;i<=m;++i)
for (int j=L[i];j<=R[i];++j)
num[j]=i;
for (int i=1,opt,l,r,c;i<=n;++i)
{
scanf("%d %d %d %d",&opt,&l,&r,&c);
if (opt == 0) Plus(l,r,c);
if (opt == 1) ask(r);
}
return 0;
}
分块入门2
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。
区间加法的方法同分块1,我们来考虑如何求小于x的元素个数。
对于每一个块,我们可以在一开始分别进行排序,若不进行修改操作就可以直接用二分查找得到答案。
对于区间修改来说,如果是整块的修改则没有关系,因为仍然保证有序性。
如果是两端零零散散的修改,在修改完之后再对这些块分别进行排序。
这个就可以用过二分查找或者lowerbound函数在log的复杂度内解决问题了。
因为分成的块是logn块,时间复杂度是 O ( n n + 2 ∗ n l o g n ) O(n\sqrt n+2*\sqrt n \ log\sqrt n) O(nn+2∗n logn)
代码如下:
#include<bits/stdc++.h>
#define find lower_bound
using namespace std;
int n,m;
int a[100000];
int L[100000];
int R[100000];
int pos[100000];
int sum[100000];
vector<int>num[100000];
void reset(int x)
{
num[x].clear();
for (int i=L[x];i<=R[x];++i)
num[x].push_back(a[i]);
sort(num[x].begin(),num[x].end());
}
void change(int l,int r,int v)
{
for (int i=1;i<=m;++i)
{
if (l>R[i] || r<L[i]) continue;
if (l<=L[i] && r>=R[i]) sum[i]+=v;
else
{
for (int j=max(l,L[i]);j<=min(R[i],r);++j)
a[j]+=v;
reset(i);
}
}
}
void print(int l,int r,int k)
{
int ans=0;
for (int i=1;i<=m;++i)
{
if (l>R[i] || r<L[i]) continue;
if (l<=L[i] && r>=R[i])
ans += find (num[i].begin(), num[i].end(), k*k-sum[i]) - num[i].begin();
else
for (int j=max(l,L[i]);j<=min(R[i],r);++j)
if (a[j]+sum[pos[j]]<k*k) ans++;
}
printf("%d\n",ans);
}
int main(void)
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%d",&n);
for (int i=1;i<=n;++i)
scanf("%d",a+i);
m=sqrt(n);
for (int i=1;i<=m;++i)
{
L[i]=sqrt(n)*(i-1)+1;
R[i]=sqrt(n)*i;
}
if (R[m] < n)
{
m++;
L[m]=R[m-1]+1;
R[m]=n;
}
for (int i=1;i<=m;++i)
for (int j=L[i];j<=R[i];++j)
pos[j]=i;
for (int i=1;i<=m;++i) reset(i);
for (int i=1,opt,l,r,c;i<=n;++i)
{
scanf("%d %d %d %d",&opt,&l,&r,&c);
if (opt == 0) change(l,r,c);
if (opt == 1) print(l,r,c);
}
return 0;
}
分块入门3
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
加法修改和排序和分块2一样,在输出答案的时候需要做一定的修改。
- 若是整块的区间,则通过lower_bound找到大于等于x的最小值,向后减去1则是小于x的最大值。
- 不完整暴力即可。
代码如下:
#include<bits/stdc++.h>
#define find lower_bound
using namespace std;
int n,m;
int a[100000];
int L[100000];
int R[100000];
int pos[100000];
int sum[100000];
vector<int>num[100000];
void reset(int x)
{
num[x].clear();
for (int i=L[x];i<=R[x];++i)
num[x].push_back(a[i]);
sort(num[x].begin(),num[x].end());
}
void change(int l,int r,int v)
{
for (int i=1;i<=m;++i)
{
if (l>R[i] || r<L[i]) continue;
if (l<=L[i] && r>=R[i]) sum[i]+=v;
else
{
for (int j=max(l,L[i]);j<=min(R[i],r);++j)
a[j]+=v;
reset(i);
}
}
}
void print(int l,int r,int k)
{
int ans=-INT_MAX;
for (int i=1;i<=m;++i)
{
if (l>R[i] || r<L[i]) continue;
if (l<=L[i] && r>=R[i])
{
int p=find (num[i].begin() ,num[i].end() ,k-sum[i]) - num[i].begin();
if (p != 0) ans=max(ans,num[i][p-1]+sum[i]);
}
else
{
for (int j=max(l,L[i]);j<=min(R[i],r);++j)
if (a[j]+sum[i]<k) ans=max(ans,a[j]+sum[i]);
}
}
printf("%d\n",ans == -INT_MAX ? -1 : ans);
}
int main(void)
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%d",&n);
for (int i=1;i<=n;++i)
scanf("%d",a+i);
m=sqrt(n);
for (int i=1;i<=m;++i)
{
L[i]=sqrt(n)*(i-1)+1;
R[i]=sqrt(n)*i;
}
if (R[m] < n)
{
m++;
L[m]=R[m-1]+1;
R[m]=n;
}
for (int i=1;i<=m;++i)
for (int j=L[i];j<=R[i];++j)
pos[j]=i;
for (int i=1;i<=m;++i) reset(i);
for (int i=1,opt,l,r,c;i<=n;++i)
{
scanf("%d %d %d %d",&opt,&l,&r,&c);
if (opt == 0) change(l,r,c);
if (opt == 1) print(l,r,c);
}
return 0;
}
分块入门4
给出一个长为n的数列,以及n个操作,操作涉及区间加法,区间求和。
其实做法和分块1还是一样的,在统计答案的时候改一下就好了。
具体做法如下:
- 遇到整块修改的, a d d i + = v add_i+=v addi+=v,i表示块的编号,v表示修改量,进行整体修改
- 遇到非整块修改的, s u m p o s i + = v , a [ i ] + = v sum_{pos_i}+=v,a[i]+=v sumposi+=v,a[i]+=v其中sum表示区间和,a表示原始序列的基础上修改的序列。
遇到查询的时候:
- 如果是整块的,答案是: a d d [ i ] ∗ ( R [ i ] − L [ i ] + 1 ) + s u m [ i ] add[i]*(R[i]-L[i]+1)+sum[i] add[i]∗(R[i]−L[i]+1)+sum[i],表示整体修改所增加的值加上暴力修改所增加的值。
- 如果是某一块的一部分,暴力喽。
注意,sum数组在未修改之前也需要预处理。
代码如下:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL n,m;
LL a[100000];
LL L[100000];
LL R[100000];
LL sum[100000];
LL add[100000];
LL pos[100000];
void change(LL l,LL r,LL c)
{
for (LL i=1;i<=m;++i)
{
if (r<L[i] || l>R[i]) continue;
if (l<=L[i] && r>=R[i]) add[i]+=c;
else for (LL j=max(l,L[i]);j<=min(r,R[i]);++j)
sum[i]+=c,a[j]+=c;
}
}
void getans(LL l,LL r,LL P)
{
LL ans=0;
for (LL i=1;i<=m;++i)
{
if (r<L[i] || l>R[i]) continue;
if (l<=L[i] && r>=R[i]) ans+=sum[i]+(R[i]-L[i]+1)*add[i],ans%=P;
else for (LL j=max(l,L[i]);j<=min(r,R[i]);++j)
ans+=(add[i]+a[j]),ans%=P;
}
printf("%lld\n",ans);
}
int main(void)
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%lld",&n);
for (LL i=1;i<=n;++i)
scanf("%lld",a+i);
m=sqrt(n);
for (LL i=1;i<=m;++i)
{
L[i]=sqrt(n)*(i-1)+1;
R[i]=sqrt(n)*i;
}
if (R[m]<n)
{
m++;
L[m]=R[m-1]+1;
R[m]=n;
}
for (LL i=1;i<=m;++i)
for (LL j=L[i];j<=R[i];++j)
sum[i]+=a[j],pos[j]=i;
for (LL i=1,opt,l,r,c;i<=n;++i)
{
scanf("%lld %lld %lld %lld",&opt,&l,&r,&c);
if (opt == 0) change(l,r,c);
if (opt == 1) getans(l,r,c+1);
}
return 0;
}
分块入门5
给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。
我们发现,当一个数进行若干次开方后会不断在0或1中循环,且开方次数很少,我们便可以利用这一性质来解决此题。
因此对于修改的区间:
- 如果区间是完整的:如果区间全部变成0/1,跳过;如果不是,则暴力修改。
- 如果是不完整的则暴力修改。
然后暴力用区间和维护即可。
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[100000];
int L[100000];
int R[100000];
int add[100000];
int pos[100000];
int sum[100000];
int flag[100000];
void sqrtit(int x)
{
if (flag[x] == 1) return;
int flg=1;
sum[x]=0;
for (int i=L[x];i<=R[x];++i)
{
a[i]=sqrt(a[i]);
sum[x]+=a[i];
if (a[i]>1) flg=0;
}
flag[x]|=flg;
return;
}
void change(int l,int r)
{
for (int i=1;i<=m;++i)
{
if (l>R[i] || r<L[i]) continue;
if (l<=L[i] && r>=R[i]) sqrtit(i);
else for (int j=max(l,L[i]);j<=min(r,R[i]);++j)
{
sum[i]-=a[j];
a[j]=sqrt(a[j]);
sum[i]+=a[j];
}
}
}
void getans(int l,int r)
{
int ans=0;
for (int i=1;i<=m;++i)
{
if (l>R[i] || r<L[i]) continue;
if (l<=L[i] && r>=R[i]) ans+=sum[i];
else for (int j=max(l,L[i]);j<=min(r,R[i]);++j) ans+=a[j];
}
printf("%d\n",ans);
}
int main(void)
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%d",&n);
for (int i=1;i<=n;++i) scanf("%d",a+i);
m=sqrt(n);
for (int i=1;i<=m;++i)
{
L[i]=(i-1)*sqrt(n)+1;
R[i]=i*sqrt(n);
}
if (R[m]<n)
{
m++;
L[m]=R[m-1]+1;
R[m]=n;
}
for (int i=1;i<=m;++i)
for (int j=L[i];j<=R[i];++j)
pos[j]=i,sum[i]+=a[j];
for (int i=1,l,r,opt,c;i<=n;++i)
{
scanf("%d %d %d %d",&opt,&l,&r,&c);
if (opt == 0) change(l,r);
if (opt == 1) getans(l,r);
}
return 0;
}
分块入门6
给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。
我们把序列分成 n \sqrt n n,每次根据查出的位置找到对应的块插入即可。
我们在这里插入使用vector,因为vector支持线性数组的中线插入。
这样的实现复杂度平均好,但是如果某一个块持续插入的话时间复杂度会退化为 O ( n 2 ) O(n^2) O(n2)。
我们在这里引入一个新的思想:重构分块。
即如果某一个块的个数超级超级大了,就把当前的所有块解散,然后重新再分;这样就不会被特殊的数据卡了。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,m,now,top;
int a[1000000];
int st[1000000];
vector<int>num[1000000];
void again( )
{
top=0;
for (int i=1;i<=m;++i)
{
for (int j=0;j<num[i].size();++j)
st[++top]=num[i][j];
num[i].clear();
}
m=sqrt(top),now=0;
for (int i=1;i<=m;++i)
for (int j=1;j<=m;++j)
num[i].push_back(st[++ now]);
if (now < top)
{
m ++;
while (now<top) num[m].push_back(st[++ now]);
}
}
void insert(int l,int r)
{
int i=1;
while (num[i].size()<l)
{
l-=num[i].size();
i ++;
}
num[i].insert(num[i].begin()+l-1,r);
if (num[i].size()>sqrt(n)*20) again();
}
void output(int r)
{
int i=1;
while (r>num[i].size())
{
r-=num[i].size();
i ++;
}
printf("%d\n",num[i][r-1]);
}
int main(void)
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%d",&n);
for (int i=1;i<=n;++i) scanf("%d",a+i);
m=sqrt(n),now=0;
for (int i=1;i<=m;++i)
for (int j=1;j<=m;++j)
num[i].push_back(a[++ now]);
if (now < n)
{
m ++;
while (now<n) num[m].push_back(a[++now]);
}
for (int i=1,opt,l,r,c;i<=n;++i)
{
scanf("%d %d %d %d",&opt,&l,&r,&c);
if (opt == 0) insert(l,r);
if (opt == 1) output(r);
}
return 0;
}