我们在做线段树等问题时,会通过将区间分成2部分,再将每一部分二分,成了一个较为严谨的树状结构。
而分块在复杂度或是结构上很不严谨,它的本质上只是将数据分成了一个个块状区域,每一个区域大小都由君定。
但分块与线段树的核心思想都是一样的:将本来整合的总体信息区域化
明显的,当我们综合信息时,我们希望信息越整体越好,这样我们调用就越方便,但是在我们修改信息时我们则更希望信息区域性更强,这样我们的修改影响就越小。
所以我们往往就得在这两者间做一个权衡,避免走向两个方向中任意一个极端,所以就需要将信息区域化。
那么在每次询问时,由于信息的区域性性,我们需要将一个个块访问一遍,将所求信息综合,这样的时间复杂度最高应该是o(分块数+次大块大小+最大块大小)。
而在修改时,为维护区域信息的整体性,我们需要将这个块信息重新修改,时间复杂度最高明显是o(最大块大小)。
至于空间复杂度,因为一个点信息只会隶属于一个块,所以我们并不需要去特意维护,复杂度不会改变多少。
那么怎么样分块能使我们在修改 查询 两者间取得最优的平衡呢?
我们可以将每个块的大小定为根号n,很明显这里是用了基本不等式的思想,也是主流思想。
当然前面说过了,区域大小(即分块大小)是任由君意的,只要能过即可,由于分块思想很简单,在下也就多嘴到此了。
例题:hnoi2010 bzoj2002弹飞绵羊
to[i]表示从i一直弹直到进入下一个块的时候第一个位置在哪(就是它能到得下一块的第一个位置啦在下好啰嗦啊)
step[i]表示从I弹到to[i]所需的步数。
很明显能影响to[i]与step[i]的只可能是块内的点的改变。
#include<iostream>
#include<string.h>
#include<algorithm>
#include<math.h>
#include<stdio.h>
using namespace std;
const int N=200050;
int n,m;
int k[N];
int to[N],step[N];
int num,fakes,faken,size[N],pre[N];
int lei,loc,v,ans,loc2;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&k[i]);
fakes=(int)sqrt(n);
faken=n;
while(faken>0)
{
size[++num]=min(fakes,faken);
faken-=fakes;
}
for(int i=1;i<=num;i++)
{
for(int j=size[i];j>=1;j--)
{
if(j+pre[i]+k[j+pre[i]]>pre[i]+size[i])
{
to[j+pre[i]]=j+pre[i]+k[j+pre[i]];
step[j+pre[i]]=1;
}
else
{
to[j+pre[i]]=to[j+pre[i]+k[j+pre[i]]];
step[j+pre[i]]=step[j+pre[i]+k[j+pre[i]]]+1;
}
}
pre[i+1]=pre[i]+size[i];
}
scanf("%d",&m);
while(m--)
{
scanf("%d%d",&lei,&loc);
loc++;
if(lei==1)
{
ans=0;
while(loc<=n)
{
ans+=step[loc];
loc=to[loc];
}
printf("%d\n",ans);
}
else
{
scanf("%d",&v);
loc2=0;
for(loc2=1;loc2<=num;loc2++)
if(pre[loc2]>=loc)
break;
loc2--;
k[loc]=v;
for(int i=loc-pre[loc2];i>=1;i--)
{
if(i+pre[loc2]+k[i+pre[loc2]]>pre[loc2]+size[loc2])
{
to[i+pre[loc2]]=i+pre[loc2]+k[i+pre[loc2]];
step[i+pre[loc2]]=1;
}
else
{
to[i+pre[loc2]]=to[i+pre[loc2]+k[i+pre[loc2]]];
step[i+pre[loc2]]=step[i+pre[loc2]+k[i+pre[loc2]]]+1;
}
}
}
}
}
分块的更多应用,详见 数列分块入门1-9 by hzwer;
然后就是莫队了。
首先解释下这个高端的名字,这个算法是由之前的国家队队长神犇莫涛先生发明的,orz大佬。
嗯,所以也别太在意那个 莫 字,我以前每次看到莫队这个词的时候,觉得带“莫”字的都是高端算法,像是莫比乌斯啊之类的,感觉 莫 简直象征着一种神秘。
队 我开始以为是一种类似于队列的数据结构的表示,但在写下边博客时才明白 队 是指队长。。。。///orz;
其实,莫队算法并不难,但它确实很神,而莫队的题往往都难在了转移位置时改变信息上。
这里讲的莫队不包含树上莫队,也不包含可修改莫队。
所以在下所要说的莫队,是只兹瓷一堆询问。
在刚开始时接触这种多询问时,很多人都想通过一种排序的方法来优化解决询问,比如有了[l,r]的询问,而此时我们能很快的解决[l,r+1],那我们要是一直将r一直往后延伸,更大的询问不也就解决了吗?
是的,这样我们确实得到一个不错的做法,这样的做法确实是一种优化,复杂度上界会变成o(n*n),第一个n是左端点,第二个是往右依次统计。
我们这里明显的优化在于,我们使许多被多个区间访问的公共区间只访问了一遍。
但是啊,当n又变大了。。。这个做法便显得束手无策;
所以我们可以考虑让左端点也动起来,那会不会更快呢?
好像是可以,可是左端点来回动的话,第一个没办法确定排序顺序了,第二个要是左端点右端点都来回走,那实际上我们保住的公共区间大小也就小的可怜了。
所以就要用到莫队算法了(对就是在写到这里时明白了“队”的含义,去补上面那行)。
我们将左端点以根号n分块,在排序时以左端点所在块编号为第一关键字,以右端点为第二关键字,小值优先排序。
与传统的 左端点为第一关键字 右端点为第二关键字 相比,我们给予了左端点移动的空间(大小为根号n),这样右端点的移动就变少了(因为右端点移动时“服务”的对象从只针对一个左端点变成了针对一个块内的左端点)。
来统计一下这个算法的复杂度:
在左端点在相同块的条件下,我们最多移动右端点n次,一共有根号n个块,所以右端点移动的总次数变成了o(n*根号n)
对于那些与上一个询问左端点不在相同块的询问,左端点最多移动n次,因为我们是按照左端点位置排序的,所以这样的询问最多有根号n个,总次数为o(n*根号n)
而左端点在同一个块内一次最多移动根号n次,所以对于那些与上一个询问左端点所在块相同的询问,移动范围最大也就根号n,这样的询问最多有m个,那么总次数为o(m*根号n)(注意m一般与n大小相近,若m大到爆炸,那我们便要预处理一下每个左端点到块末)
所以综上,复杂度大概整体是o(n*根号n)的。
明显这就是一个“平衡”把握的很好的例子。
例题:
bzoj2038
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<string.h>
using namespace std;
const int N=50050;
int n,m;
int col[N],num;
struct que
{
int l,r,lx;
int id;
}q[N];
int cmp(que a,que b)
{
if(a.l==b.l)
return a.r<b.r;
return a.l<b.l;
}
int cmp1(que a,que b)
{
if(a.lx==b.lx)
return a.r<b.r;
return a.lx<b.lx;
}
int faken,fakes,pre[N],size[N];
int sum[N];
long long anss[N],ansx[N];
long long gcd(long long a,long long b)
{
if(b==0)
return a;
return gcd(b,a%b);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&col[i]);
for(int i=1;i<=m;i++)
scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
sort(q+1,q+m+1,cmp);
fakes=(int)sqrt(n);
faken=n;
while(faken>0)
{
size[++num]=min(fakes,faken);
faken-=fakes;
pre[num]=pre[num-1]+size[num];
}
int loc2=0;
for(int i=1;i<=m;i++)
{
while(pre[loc2]<q[i].l)
loc2++;
q[i].lx=loc2;
}
sort(q+1,q+m+1,cmp1);
int l=0,r=0;
long long su=0;
for(int i=1;i<=m;i++)
{
while(q[i].r>r)
{
r++;
su+=(long long)sum[col[r]];
sum[col[r]]++;
}
while(q[i].r<r)
{
sum[col[r]]--;
su-=(long long)sum[col[r]];
r--;
}
while(q[i].l<l)
{
l--;
su+=(long long)sum[col[l]];
sum[col[l]]++;
}//4540罪(wrong)恶(answer)之源
while(q[i].l>l)
{
if(l!=0)
sum[col[l]]--;
su-=(long long)sum[col[l]];
l++;
}
anss[q[i].id]=su;
ansx[q[i].id]=(long long)(r-l)*(long long)(r-l+1)/2LL;
}
for(int i=1;i<=m;i++)
{
if(anss[i]==0)
printf("0/1\n");
else
{
long long d=gcd(anss[i],ansx[i]);
printf("%lld/%lld\n",anss[i]/d,ansx[i]/d);
}
}
}
bzoj3289
#include<iostream>
#include<string.h>
#include<algorithm>
#include<stdio.h>
#include<math.h>
using namespace std;
const int N=50050;
int n,m;
long long la[N];
long long la2[N];
long long ans[N];
int faken,fakes,size[N],num,pre[N];
int discret(long long x)
{
return lower_bound(la2+1,la2+n+1,x)-la2;
}
int c[N];
int lowbit(int x)
{
return x&(-x);
}
void plu(int x,int v)
{
while(x<=n)
{
c[x]+=v;
x+=lowbit(x);
}
}
int sum(int x)
{
int res=0;
while(x>0)
{
res+=c[x];
x-=lowbit(x);
}
return res;
}
struct query
{
int l,r;
int id;
int lx;
}q[N];
int cmp(query a,query b)
{
if(a.lx==b.lx) return a.r<b.r;
return a.lx<b.lx;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%lld",&la[i]),la2[i]=la[i];
sort(la2+1,la2+n+1);
for(int i=1;i<=n;i++)
la[i]=discret(la[i]);
int fakes=(int)sqrt(n);
faken=n;
while(faken>0)
size[++num]=min(faken,fakes),faken-=fakes,pre[num]=pre[num-1]+size[num];
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&q[i].l,&q[i].r);
q[i].id=i;
q[i].lx=lower_bound(pre+1,pre+num+1,q[i].l)-pre;
}
sort(q+1,q+m+1,cmp);
int l=0,r=0;
long long su=0;
for(int i=1;i<=m;i++)
{
while(r<q[i].r)
{
r++;
su+=(long long)sum(n)-(long long)sum(la[r]);
plu(la[r],1);
}
while(r>q[i].r)
{
plu(la[r],-1);
su-=(long long)sum(n)-(long long)sum(la[r]);
r--;
}
while(l>q[i].l)
{
l--;
su+=(long long)sum(la[l]-1);
plu(la[l],1);
}//小朋友们别学这样写,这样写别的题可能会挂,这个应该顶替楼上的位置
while(l<q[i].l)
{
if(l!=0)
plu(la[l],-1);
su-=(long long)sum(la[l]-1);
l++;
}
ans[q[i].id]=su;
}
for(int i=1;i<=m;i++)
printf("%lld\n",ans[i]);
}
bzoj4540
#include<iostream>
#include<string.h>
#include<algorithm>
#include<math.h>
#include<stdio.h>
#include<stack>
#define lson rt*2
#define rson rt*2+1
#define mid (l+r)/2
using namespace std;
const int N=100050;
long long a[N];
int s[2*N],t=0;
long long suml[N],sumr[N];
int n,m;
int minx[N][32];
int er[31]={1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,16777216,33554432,67108864,134217728,268435456,536870912,1073741824};
int len[N];
void build()
{
for(int i=1;i<=n;i++)
minx[i][0]=i;
for(int j=1;j<=18;j++)
{
if(er[j]>n)
break;
for(int i=1;i<=n-er[j]+1;i++)
{
int a1=minx[i][j-1],a2=minx[i+er[j-1]][j-1];
if(a[a1]<=a[a2]||a2==0)
minx[i][j]=a1;
else
minx[i][j]=a2;
}
}
int xx=1,nu=0;
for(int i=1;i<=n;i++)
{
if(i>=2*xx)
nu++,xx*=2;
len[i]=nu;
}
}
int query1(int l,int r)
{
int a1=minx[l][len[r-l+1]],a2=minx[r-er[len[r-l+1]]+1][len[r-l+1]];
// cout<<l<<"-"<<r<<": "<<l<<" "<<len[r-l+1]<<" "<<r-er[len[r-l+1]]+1<<" "<<len[r-l+1]<<endl;
if(a[a1]<=a[a2])
return a1;
else
return a2;
}
struct query
{
int l,r,lx,id;
}q[N];
int cmp(query a,query b)
{
if(a.lx==b.lx) return a.r<b.r;
return a.lx<b.lx;
}
long long ans=0;
int faken,fakes,num;
int size[N],pre[N];
void gengl(int l,int r,int lei)
{
int xx=query1(l,r);
long long suan=(long long)(r-xx+1)*a[xx];
suan+=sumr[l]-sumr[xx];
ans+=suan*lei;
}
void gengr(int l,int r,int lei)
{
int xx=query1(l,r);
long long suan=(long long)(xx-l+1)*a[xx];
suan+=suml[r]-suml[xx];
ans+=suan*lei;
}
int l1[N],r1[N];
long long finans[N];
int cmp2(query a,query b)
{
if(a.l==b.l)
return a.r<b.r;
return a.l<b.l;
}
int main()
{
scanf("%d",&n);
scanf("%d",&m);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
build();
for(int i=1;i<=n;i++)
{
while(t>0&&a[s[t]]>a[i])
r1[s[t]]=i,t--;
l1[i]=s[t];
s[++t]=i;
}
while(t>0) r1[s[t--]]=n+1;
for(int i=1;i<=n;i++)
suml[i]=suml[l1[i]]+(long long)(i-l1[i])*a[i];
for(int i=n;i>=1;i--)
sumr[i]=sumr[r1[i]]+(long long)(r1[i]-i)*a[i];
//
faken=n;
fakes=sqrt(n);
while(faken>0)
size[++num]=min(faken,fakes),faken-=fakes,pre[num]=pre[num-1]+size[num];
for(int i=1;i<=m;i++)
{
scanf("%d%d",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q+1,q+m+1,cmp2);
int loc2=1;
for(int i=1;i<=m;i++)
{
while(pre[loc2]<q[i].l)
loc2++;
q[i].lx=loc2;
}
sort(q+1,q+m+1,cmp);
//
int l=1,r=1;
ans=a[1];
for(int i=1;i<=m;i++)
{
while(r<q[i].r) r++,gengr(l,r,1);
while(l>q[i].l) l--,gengl(l,r,1);
while(q[i].r<r) gengr(l,r,-1),r--;
while(l<q[i].l) gengl(l,r,-1),l++;
finans[q[i].id]=ans;
}
for(int i=1;i<=m;i++)
printf("%lld\n",finans[i]);
}
为了做这道题还复习了一下st表,发现自己rmq基本全忘了。。。
(ac的感动。。。因为来之不易。。。前面t是因为用了线段树,后面一直wa其实是因为在移动l,r时顺序不对,应该先扩展该扩展的再缩小该减缩的,不然中间会出现不合法情况,有的题目会使答案错到天上去。。)