数据结构——分块

Posted zerosking

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构——分块相关的知识,希望对你有一定的参考价值。

数据结构——分块

1.基本思想

? 分块思想是通过适当地划分,预处理一部分信息并保存下来,用空间换取时间,达到时空平衡。事实上,分块比线段树等数据结构朴素得多,基本上算是“优化的暴力”。但是它更加通用,且更易实现。

? 何为“适当的划分”:玄学 数学方法推导

2.题型分析

1.数列分块

已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数加上x

2.求出某区间每一个数的和

对于100%的数据:N<=100000,M<=100000

显然这道题可以用线段树做。但是,随手百来行真的好吗?如果要加上其他的操作,就更加麻烦了。

看看数据范围——1e5,(O(nsqrt{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)的质量((Maxmleq 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
",ans);
    return 0;
}

忽略这鬼畜的缩进

3.莫队算法

莫队算法其实可以说是分块的一个延伸应用了。

大致说来,莫队算法是一个离线回答区间问题的算法。它通过几个指针的移动,通过上一个询问的结果,来计算相邻询问的答案。

延伸开来有很多东西。。。本蒟蒻太弱,不能理解其十分之一,挂上dalao的博客慢慢学习吧。

大米饼 莫队算法


以上是关于数据结构——分块的主要内容,如果未能解决你的问题,请参考以下文章

分块入门

[数据结构学习]分块与树状数组

如何在 Python 数据框中分块读取数据?

浅谈数列分块问题

分块代码

Loj 6282. 数列分块入门 6