分块
高级的暴力,把序列分成一块一块的,利用加懒标记的方式进行区间操作。而没有被分成块的就直接暴力修改,所以时间复杂度方面感觉很玄学。
例一
给一个长度为n的序列,每次给l,r和c,表示把区间l~r都加c,或者输出a[r]的值
一开始只知道原理而没见过板子的时候
void fenkuai()
{
for(int i=1;i<=n;i+=m)kuaiz[++tot]=i;
kuaiz[++tot]=kuaiz[tot-1]+m;
}//n为序列长度,m为块的大小
写下这种幼稚的板子,用
k
u
a
i
z
[
t
o
t
]
kuaiz[tot]
kuaiz[tot] 记录每一块的左端点在序列中的下标。
然后
void xg(int x,int y,int z)
{
int p=lower_bound(kuaiz,kuaiz+tot,x)-kuaiz;
int q=upper_bound(kuaiz,kuaiz+tot,y)-kuaiz;
for(int i=p+1;i<q;i++)lan[i]+=z;
for(int i=x;i<kuaiz[p];i++)a[i]+=z;
for(int i=kuaiz[q-1];i<=y;i++)a[i]+=z;
return;
}
以这种错误的方式进行整块整块的懒标记,和非整块部分的直接修改,然后听取
W
A
WA
WA 声一片。(当然不排除思路没错但写法有误。)然后在高强度自闭下看了别人的写法。
后来也确实有人的实现跟我一开始所想的差不多
void change(int ll,int rr,long long d)
{
int p=pos[ll],q=pos[rr];
if(p==q)
{
for(int i=ll;i<=rr;i++) a[i]+=d;
sum[p]+=d*(rr-ll+1);
}
else
{
for(int i=p+1;i<q;i++) add[i]+=d;
for(int i=ll;i<=r[p];i++) a[i]+=d;
sum[p]+=d*(r[p]-ll+1);
for(int i=l[q];i<=rr;i++) a[i]+=d;
sum[q]+=d*(rr-l[q]+1);
}
}
以这种方式进行修改,初始分块时:
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int t=sqrt(n);
for(int i=1;i<=t;i++)
{
l[i]=(i-1)*sqrt(n)+1;
r[i]=i*sqrt(n);
}
if(r[t]<n)
{
t++;
l[t]=r[t-1]+1;
r[t]=n;
}
for(int i=1;i<=t;i++)
for(int j=l[i];j<=r[i];j++)
pos[j]=i,sum[i]+=a[j];
真是妙啊
以下为别人正确的基本分块板子
初始化分块:
for(int i=1;i<=n;i++){scanf("%d",&a[i]);wz[i]=(i-1)/m+1;}//wz[i]记录序列中下标为i的数在第几块
而修改(在此以直接加的为例):
void gai(int l,int r,int c)
{
if(wz[l]==wz[r]){for(int i=l;i<=r;i++)a[i]+=c;return;}
//不排除区间的左右端点在同一个块里,如果是这样就直接暴力修改,然后return;
for(int i=l;i<=wz[l]*m;i++)a[i]+=c;//左边不完整的一个块,就算是完整也直接暴力改
for(int i=wz[l]+1;i<=wz[r]-1;i++)lan[i]+=c;//中间完整的块直接加懒标记
for(int i=(wz[r]-1)*m+1;i<=r;i++)a[i]+=c;//右边
}
但是我后来写这个函数的时候经常搞错边界,于是就又学到一种写法:
void change(int l,int r,int x)
{
for(int i=l;i<=min(pos[l]*m,r);i++)//直接修改左边多出的一块,如果是一块就暴力把整个区间都修改了
a[i]+=x;
if(pos[l]!=pos[r])//如果不是一块上的
for(int i=(pos[r]-1)*m+1;i<=r;i++)//给右边多出的部分暴力修改
a[i]+=x;
for(int i=pos[l]+1;i<pos[r];i++)//如果同在一块,就跟不执行一样
del[i]+=x;//懒标记
}
输出:
printf("%d\n",a[r]+lan[wz[r]]);
然后正常写个输入输出啥的,该例题就直接
A
A
A 了
至于
m
m
m 要有多大,即每一块儿该有多大,一般是
n
\sqrt{n}
n 比较保险。
∙
\bullet\;
∙ 如果块太大了,被修改的区间经常不能包含整块,而还是要靠暴力,比方取极值长度为
n
n
n ,结果显而易见。
∙
\bullet\;
∙ 如果块太小,需要就算是懒标记也要标记很多次,同样取极值
1
1
1,每次加懒标记还不如直接取修改。
∙
\bullet\;
∙ 所以取
n
\sqrt{n}
n 最为合适,可以达到效果:有
n
\sqrt{n}
n 块,每块长度为
n
\sqrt{n}
n 。
当然不排除特殊情况,不同情况差别对待。
例二
给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。
用
S
T
L
STL
STL很容易去维护,然而这个题中难点在于:即使数据是随机生成的,也不排除往某个区间中猛插数,所以就需要加个特判,长度大于特定值时把分块重构一遍。这道题用线段树显然是不好搞的。
注:别人写法的板子是直接站得,所以变量和码风不一样
莫队
学完莫队,我才理解到为什么老师说信息学是目前最接近魔法的一门学科
莫队首先一般是离线操作,而我感觉最为玄学的应该是它的时间复杂度,我自己写的在
校
o
j
校oj
校oj 跑了
1800
m
s
1800ms
1800ms才
A
A
A 掉了,但是某谷上只有
72
72
72 分,于是校oj找了个跑
900
m
s
900ms
900ms 的代码,某谷中只有
65
65
65 分。
还有一点比较疑惑的是在
O
I
e
r
d
b
OIerdb
OIerdb 中搜索莫队的发明人——莫涛
不再废话了,莫队算是有点分块思想,而且必须是离线操作。在对一个序列进行多次区间操作的时候,我们可以首先把序列按每块
n
\sqrt{n}
n 分成
n
\sqrt{n}
n 块,然后对每次操作所选的区间进行排序,以左端点所在的块的下标为第一关键字,以右端点的下标为第二关键字,即排完顺序后左端点所在块靠前的靠前,若在同一个块中,则按右端点从小到大排序,分块的方法求出第一个区间的答案,然后由于第二个区间左端点在同一块内,所以先用
n
\sqrt{n}
n 不到的复杂度对左边进行修改,然后再看右边比前一个区间多出来的部分对答案造成的影响。
如果分块的板子跟上面一样,排序的代码大概就是这样
int get(int x){return (x-1)/t+1;}
bool mycmp(hhhc x,hhhc y)
{
int a=get(x.l),b=get(y.l);
return((a<b)||(a==b&&x.r<y.r));
}
通常的算区间变化后对答案有加或减的影响时:
for(int k=1,i=0,j=1;k<=m;k++)
{
int zz=hh[k].id,l=hh[k].l,r=hh[k].r;
while(i<r)cha(a[++i]);
while(i>r)shan(a[i--]);
while(j<l)shan(a[j++]);
while(j>l)cha(a[--j]);
ans[zz]=sum;
}
下面这两个函数适用于最后两个例题
void cha(int x)
{
if(!cnt[x])sum++;
cnt[x]++;
}
void shan(int x)
{
cnt[x]--;
if(!cnt[x])sum--;
}
例一
\;\;\;\;\;\;\;\;\;
[SDOI2009]HH的项链
例二
\;\;\;\;\;\;\;\;\;
[国家集训队]数颜色 / 维护队列