分块——美妙的暴力
前言
最近发现模拟赛的难度突然增大,需要用到的知识点变得很多很多(2-SAT、主席树、树链剖分,甚至还有LCT……),于是我想着来学学知识点,因为有备无患嘛
然后想到了若干年前有要用分块的题目听了大佬讲完之后糊里糊涂,又听别人说其实挺容易的,于是我便上OI-WIKI学了一下,发现思想真的很简单,代码也没有什么细节,简单简洁暴力,复杂度也可以接受(当然比起带log的算法还是差一点)
博客写的一般大佬勿喷……
分块是神马东东?
顾名思义,就是将一个区间分成很多块的意思,之后再分别对每个块进行整体处理,达到不用一个一个点进行操作的效果
例题引入1
#6280. 数列分块入门4
这道题大意就是有区间修改和区间求和并取模的操作
有的同学可能会说,很简单呀,用普通线段树不就很容易做出来了吗?
且慢,我们可以看到这道题取模的数值是不固定的,所以并不能用普通的线段树来进行处理(反正我是做不到,如果有大佬可以的话我也甘拜下风)
(什么,先求完全部和值再取模?这样做的话如果数值稍微大一点线段树就撑不住了。但是如果大很多的话分块也撑不住……)
思路
首先我们按
s
s
s个元素一组将序列分成若干块(即块长为s):
其中
b
[
i
]
b[i]
b[i]是第
i
i
i个块整体被加了多少,之后再记录一个
s
u
m
[
i
]
sum[i]
sum[i]表示第
i
i
i个块的和是多少
修改操作(以查询区间
[
l
,
r
]
[l,r]
[l,r]为例)
- 若 l l l和 r r r在同一个块内,那么直接暴力修改,之后更新块的区间和,最坏复杂度为 O ( s ) O(s) O(s)
- 若
l
l
l和
r
r
r不在同一个块内,则修改部分由三部分组成:以
l
l
l开头的不完整块,中间若干个完整块,以
r
r
r结尾的不完整块。
于是对于不完整的块,也是直接暴力修改,之后更新块的区间和;对于完整的块,直接更新 b b b数组的值和区间和就可以了,最坏复杂度为 O ( n s + s ) O(\dfrac{n}{s}+s) O(sn+s)
查询操作(以查询区间 [ l , r ] [l,r] [l,r]为例)
- 若 l l l和 r r r在同一个块内,那么直接利用 b b b数组和 a a a数组暴力求和就行了,最坏复杂度为 O ( s ) O(s) O(s)
- 若
l
l
l和
r
r
r不在同一个块内,则查询部分由三部分组成:以
l
l
l开头的不完整块,中间若干个完整块,以
r
r
r结尾的不完整块。
于是对于不完整的快,依然直接暴力求和;对于不完整的块,直接加上块的区间和就行了,最坏复杂度为 O ( n s + s ) O(\dfrac{n}{s}+s) O(sn+s)
块长怎么定?
相信很多同学上面这么简单的内容已经懂了,不过块长到底怎么定呢?
利用OI-WIKI的解释来说就是(虽然我也不知道到底是个啥):
这个复杂度相对来讲已经很优秀了,100000上下肯定没问题(如果多一个0估计就要用带log的算法了),主要是简单易懂,还很香……
CODE(由于非常简单,就不贴解释了)
#include<cstdio>
#include<string>
#include<cmath>
#define ll long long
#define N 50005
#define R register int
using namespace std;
ll n,len,a[N],sum[N],b[N],num[N];
inline void read(ll &x)
{
x=0;char ch=getchar();
while (!isdigit(ch)) ch=getchar();
while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
}
inline void change(ll l,ll r,ll k)//修改
{
ll st=num[l],en=num[r];
if (st==en)
{
for (R i=l;i<=r;++i)
a[i]+=k,sum[st]+=k;
return;
}
for (R i=l;num[i]==st;++i)
a[i]+=k,sum[st]+=k;
for (R i=st+1;i<en;++i)
b[i]+=k,sum[i]+=len*k;
for (R i=r;num[i]==en;--i)
a[i]+=k,sum[en]+=k;
}
inline void solve(ll l,ll r,ll mo)//查询
{
ll st=num[l],en=num[r],ans=0;
if (st==en)
{
for (R i=l;i<=r;++i)
ans=(ans+a[i]+b[st])%mo;
printf("%lld\n",ans);
return;
}
for (R i=l;num[i]==st;++i)
ans=(ans+a[i]+b[st])%mo;
for (R i=st+1;i<en;++i)
ans=(ans+sum[i])%mo;
for (R i=r;num[i]==en;--i)
ans=(ans+a[i]+b[en])%mo;
printf("%lld\n",ans);
}
int main()
{
read(n);len=sqrt(n);
for (R i=1;i<=n;++i)
read(a[i]),num[i]=(i-1)/len+1,sum[num[i]]+=a[i];
for (R i=1;i<=n;++i)
{
ll opt,l,r,c;read(opt);read(l);read(r);read(c);
if (opt==0) change(l,r,c);else solve(l,r,c+1);
}
return 0;
}
例题引入2
Description
在N(1<=N<=100000)个数A1…An组成的序列上进行M(1<=M<=100000)次操作,操作有两种:
(1)1 L R C:表示把A[L]到A[R]增加C(C的绝对值不超过10000);
(2)2 L R:询问A[L]到A[R]之间的最大值。
Input
第一行输入N(1<=N<=100000),表示序列的长度,接下来N行输入原始序列;接下来一行输入M(1<=M<=100000)表示操作的次数,接下来M行,每行为1 L R C或2 L R
Output
对于每个操作(2)输出对应的答案。
Sample Input
5
1
2
3
4
5
3
2 1 4
1 1 3 3
2 3 5
Sample Output
4
6
Hint
【限制】
保证序列中的所有的数都在longint范围内
好吧其实这就是一道线段树模板题
思路
修改操作
跟例题1差不多了,修改操作一模一样,只不过要维护一下区间最大值,而且因为有负数,所以在处理在同一个块或者开头和结尾时需要把这个块重新算一遍最大值
查询操作
跟例题1的查询差不多,非常简单
CODE
#include<cstdio>
#include<string>
#include<cmath>
#include<cstring>
#define N 100005
#define max(a,b) a>b?a:b
#define R register int
#define ll long long
using namespace std;
int n,a[N],b[N],mx[N],sum[N],len,id[N],m;
inline void read(int &x)
{
x=0;int f=1;char ch=getchar();
while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();x*=f;
}
inline void change(int l,int r,int k)//修改
{
int st=id[l],en=id[r];
if (st==en)
{
mx[st]=-0x3f3f3f3f;
for (R i=(st-1)*len+1;i<=st*len;++i)
{
if (i>=l && i<=r) a[i]+=k;
mx[st]=max(mx[st],a[i]+b[st]);
}
return;
}
mx[st]=-0x3f3f3f3f;
for (R i=(st-1)*len+1;i<=st*len;++i)
{
if (i>=l) a[i]+=k;
mx[st]=max(mx[st],a[i]+b[st]);
}
for (R i=st+1;i<en;++i)
b[i]+=k,mx[i]+=k;
mx[en]=-0x3f3f3f3f;
for (R i=en*len;i>=(en-1)*len+1;--i)
{
if (i<=r) a[i]+=k;
mx[en]=max(mx[en],a[i]+b[en]);
}
}
inline void solve(int l,int r)//查询
{
int st=id[l],en=id[r],ans=-0x3f3f3f3f;
if (st==en)
{
for (R i=l;i<=r;++i)
ans=max(ans,a[i]+b[st]);
printf("%d\n",ans);return;
}
for (R i=l;id[i]==st;++i)
ans=max(ans,a[i]+b[st]);
for (R i=st+1;i<en;++i)
ans=max(ans,mx[i]);
for (R i=r;id[i]==en;--i)
ans=max(ans,a[i]+b[en]);
printf("%d\n",ans);
}
int main()
{
read(n);len=sqrt(n);memset(mx,-0x3f3f3f3f,sizeof(mx));
for (R i=1;i<=n;++i)
read(a[i]),id[i]=(i-1)/len+1,mx[id[i]]=max(mx[id[i]],a[i]);
read(m);
for (R i=1;i<=m;++i)
{
int x,l,r,c;read(x);
if (x==1) read(l),read(r),read(c),change(l,r,c);
else read(l),read(r),solve(l,r);
}
return 0;
}
结语
怎么样,听了模板题的思路是不是感觉分块思想原来就这?是不是发现分块其实就是优美的暴力呢?