数据结构——分块

数据结构——分块

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\)有序的效果。

说起来抽象,放题目讲吧。

CH #46A 磁力块

题目描述

在一片广袤无垠的原野上,散落着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\)的大小,而是要开成输入数最大值的大小

posted @ 2018-12-04 20:45  Zerosking  阅读(1303)  评论(0编辑  收藏  举报