数据结构——分块
数据结构——分块
1.基本思想
分块思想是通过适当地划分,预处理一部分信息并保存下来,用空间换取时间,达到时空平衡。事实上,分块比线段树等数据结构朴素得多,基本上算是“优化的暴力”。但是它更加通用,且更易实现。
何为“适当的划分”:玄学 数学方法推导
2.题型分析
1.数列分块
已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上x
2.求出某区间每一个数的和
对于100%的数据:N<=100000,M<=100000
显然这道题可以用线段树做。但是,随手百来行真的好吗?如果要加上其他的操作,就更加麻烦了。
看看数据范围——1e5,\(O(n\sqrt{n})\)貌似能卡过。。。
\(solution:\)
把数列分成若干个长度小于等于\(\sqrt{n}\) 的段,显然 ,第\(i\)段的左端点为\((i-1)*\sqrt{n}\),右端点为\(i*\sqrt{n}\) 。
另外,预处理出数组\(sum[i]\),表示第\(i\)段的区间和;\(add[i]\)表示增量标记(类比线段树)。
void pre()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
int t=sqrt(n); //要分的块数
for(int i=1;i<=t;i++)
L[i]=(i-1)*t+1,R[i]=i*t;
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[i]表示第i个数所在的块的编号
pos[j]=i,sum[i]+=a[j];
}
对于一个修改\([l,r]\),则会有两种情况:
1.\(l\)和\(r\)在同一区间内,这时候直接暴力修改就好。
2.长这样
对于整块(上图标红色),直接修改\(add\)标记,对于两端不足整块的部分(上图标绿色),暴力更新。
void mdf(int l,int r,ll d)
{
int p=pos[l],q=pos[r];
if(p==q)
for(int i=l;i<=r;i++)a[i]+=d,sum[p]+=d;
else
{
for(int i=p+1;i<=q-1;i++)add[i]+=d;
for(int i=l;i<=R[p];i++)a[i]+=d,sum[p]+=d;
for(int i=L[q];i<=r;i++)a[i]+=d,sum[q]+=d;
}
}
修改操作可以类比
ll ask(int l,int r)
{
int p=pos[l],q=pos[r];
ll ans=0;
if(p==q)
for(int i=l;i<=r;i++)ans+=a[i]+add[p];
else
{
for(int i=p+1;i<=q-1;i++)ans+=sum[i]+add[i]*(R[i]-L[i]+1);
for(int i=l;i<=R[p];i++)ans+=a[i]+add[p];
for(int i=L[q];i<=r;i++)ans+=a[i]+add[q];
}
return ans;
}
自我感觉码风还是比较好的
洛谷上跑的飞快,十个点只比线段树慢0.2s...代码
理解了代码后就不难发现,这种分块中对于整段的修改用\(add\)记录,不足整段的暴力修改。其实,大部分常见的分块思想都可以用"大段维护,局部朴素"来形容。
因为分块简单粗暴,个人感觉还是比较好理解的。
emmmmm NOI+难度的题目考虑一下?
2.分块排序
P.S.排序分块这名字其实是我自己取的。。。个人理解。有什么不妥之处还请指教。
在很多题目中,我们发现,对元素进行排序能有效地降低复杂度。因为这样可以使你查找合法元素的时候有迹可循,而不是去遍历。但是,对于有些题目,要处理的元素有多个关键字,这时候排序单关键字就没多大用处了。让多个关键字有序又显然不可能。怎么办呢?我们可以采用一个折中的方法——分块排序。
简单说,就是先将元素按某一关键字\(a\)排序后,分块,再对块内元素以关键字\(b\)排序,从而达到整体上\(a\)有序,局部\(b\)有序的效果。
说起来抽象,放题目讲吧。
题目描述
在一片广袤无垠的原野上,散落着N块磁石。每个磁石的性质可以用一个五元组(x,y,m,p,r)描述,其中x,y表示其坐标,m是磁石的质量,p是磁力,r是吸引半径。若磁石A与磁石B的距离不大于磁石A的吸引半径,并且磁石B的质量不大于磁石A的磁力,那么A可以吸引B。
小取酒带着一块自己的磁石L来到了这篇原野的(x0,y0)处,我们可以视为磁石L的坐标为(x0,y0)。小取酒手持磁石L并保持原地不动,所有可以被L吸引的磁石将会被吸引过来。在每个时刻,他可以选择更换任意一块自己已经获得的磁石(当然也可以是自己最初携带的L磁石)在(x0,y0)处吸引更多的磁石。小取酒想知道,他最多能获得多少块磁石呢?
- 对于100%的数据,1<=N<=250000,-10^9 <=x,y <=10^9,1 <=m,p,r<=10^9。
这道题给的磁石是个五元组,怎么办呢?
dalao当然可以用平衡树之类的暴力维护,但码起来还是比较复杂的
仔细分析,其实我们可以发现,这个五元组,用起来其实缩水成了两个维度!
即 质量\(\leq\)磁力,距离\(\leq\)吸引半径
那么问题就简单多了。
先用个队列存手里的磁石。
我们首先把磁石按质量\(sort\)一遍,分\(\sqrt{N}\)份,然后在每段内部,再重新按照距离排序。
每次拿手里的磁石(记为\(H\))去吸引别的磁石时,从前向后一段段扫,有以下3种情况:
1.本段所有磁石的质量都小于\(H\)的质量(\(Maxm\leq H.m\)):在此段内,从前往后依次吸引距离内的磁石
2.本段磁石质量有的大于\(H\)有的小于\(H\):此时对这一段暴力扫
3.本段磁石质量均大于\(H\):愉快地不管这一段(吸不动了)
并且,因为我们是排了序的,所以必然存在一个正整数\(k\)使得:第\(~1~k-1\)段为第一种情况,第\(k\)段为第二种,\(k\)段后面的则是第三种情况!
做完了。
愉快上代码:
struct node
{int x,y,m,p,r;double dis;}a[N];
int xx,yy,p0,r0;
bool cmp1(const node &a,const node &b)
{return a.m<b.m;}
double dist(node a)
{return sqrt((a.x-xx)*(a.x-xx)+(a.y-yy)*(a.y-yy));}
bool cmp2(const node &a,const node &b)
{return a.dis<b.dis;}
int L[S],R[S],maxm[S]; //maxm保存每块质量最大值,便于判断情况
bool vis[N]; //vis表示是否被取走过
int n,t,ans;
queue<node> q;
void pre() //预处理
{
sort(a+1,a+1+n,cmp1);
for(int i=1;i<=t;i++)
L[i]=(i-1)*t+1,R[i]=i*t,maxm[i]=a[R[i]].m;
if(R[t]<n)
t++,L[t]=R[t-1]+1,R[t]=n,maxm[t]=a[n].m;
for(int i=1;i<=t;i++)
sort(a+L[i],a+R[i]+1,cmp2);
}
signed main()
{
scanf("%d%d%d%d%d",&xx,&yy,&p0,&r0,&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d%d%d%d",&a[i].x,&a[i].y,&a[i].m,&a[i].p,&a[i].r);
a[i].dis=dist(a[i]);
}
t=sqrt(n);
pre();
q.push({xx,yy,0,p0,r0});
while(q.size())
{
node now=q.front();q.pop();
for(int i=1;i<=t;i++)
{
if(now.p>=maxm[i]) //第一种情况
{
for(int j=L[i];j<=R[i];j++)
{
if(vis[j]) continue;
if(now.r>=a[j].dis)
{
ans++;q.push(a[j]);
L[i]=j+1;vis[j]=1; //细节:吸走的磁石要去掉
}
else break;
}
}
else //第二种情况
{
for(int j=L[i];j<=R[i];j++)
{
if(now.r>=a[j].dis && now.p>=a[j].m && !vis[j])
ans++,q.push(a[j]),vis[j]=1;
}
break; //之后一定是第三种情况,可以跳过
}
}
}
printf("%d\n",ans);
return 0;
}
忽略这鬼畜的缩进
3.莫队算法
莫队算法其实可以说是分块的一个延伸应用了。
大致说来,莫队算法是一个离线回答区间问题的算法。它通过几个指针的移动,通过上一个询问的结果,来计算相邻询问的答案。
延伸开来有很多东西。。。本蒟蒻太弱,不能理解其十分之一,挂上dalao的博客慢慢学习吧。
P.S.自己总结的几点注意事项:
1.初始化\(l=1\)和\(r=0\)的原因:\(l=r\)时,虽然要求输出0,但其实长度区间为1的区间对答案也是有贡献的。所以r指针右移的第一步是对ans有影响的
2.块的大小一直都是玄学。。。所以自造大数据对拍吧 我是不会告诉你我不会对拍的
3.带修莫队中有\(s[]\)和\(now[]\)两个储存序列的数组,其实在莫队修改中修改的是\(s\)数组,\(now\)数组只是为了记录每个修改的原数和新数
4.\(sum[]\)数组。。。就是记录每个数出现次数的那个。不是\(N\)的大小,而是要开成输入数最大值的大小