• 数据结构——分块


    数据结构——分块

    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的博客慢慢学习吧。

    大米饼 莫队算法

    P.S.自己总结的几点注意事项:

    1.初始化(l=1)(r=0)的原因:(l=r)时,虽然要求输出0,但其实长度区间为1的区间对答案也是有贡献的。所以r指针右移的第一步是对ans有影响的

    2.块的大小一直都是玄学。。。所以自造大数据对拍吧 我是不会告诉你我不会对拍的

    3.带修莫队中有(s[])(now[])两个储存序列的数组,其实在莫队修改中修改的是(s)数组,(now)数组只是为了记录每个修改的原数和新数

    4.(sum[])数组。。。就是记录每个数出现次数的那个。不是(N)的大小,而是要开成输入数最大值的大小

    Desperados no way back.
  • 相关阅读:
    CF div2 332 A
    vector resize 错误用法
    linux命令之 chown
    dlmalloc 编译 链接
    C++ 类 访问限制
    C++ 编译多态 运行多态
    libevent 编译 Windows
    浮点数小记
    NYOJ 435 棋盘覆盖(二)
    HDU 3555 Bomb 简单数位DP
  • 原文地址:https://www.cnblogs.com/Zerosking/p/10066754.html
Copyright © 2020-2023  润新知