这讲师 简直了

luogu春令营 D4

根号平衡

那是啥?

有x次修改,单次复杂度为O(a)。

有y=kx次查询,单次复杂度为O(b)。

在满足一定条件的题里面,可以通过提高其中一边的复杂度,降低另一边的复杂度来达到更好的效果。

这时满足xa=kxb。

举个栗子?

就拿线段树来举例,如果不用线段树,修改是O(1),查询是O(n),一共有m次操作。

这时候你用线段树,把修改和查询都平衡成log n,就降低了整体的复杂度。

我们知道如果有一个长度为n的序列,把它分成满x叉树的形式,那么有logx(n)层。

线段树就是一个满二叉树,所以它有log(n)层。

而运用根号平衡的分块算法就是把两边都平衡为O(sqrt(n)),也就是建立一个sqrt(n)叉树,这棵树只有两层(算上根节点三层)。

分块

分块主要分为静态分块和动态分块。

静态分块指的是放一些关键点,预处理关键点到关键点的信息来加速查询的,不能支持修改。

目前认为:如果可以离线,静态分块是莫队算法的子集。

动态分块就是把序列分块,每块维护信息,支持修改。下面的分块指动态分块。

分块基础

要实现区间加,区间和,我们可以把sqrt(n)个元素放在一块维护。

我们一般把每次操作完整覆盖的块定义为”整块”,而没有完整覆盖的块定义为”零散块”

image

每次操作最多经过O(sqrt(n))个整块,最多2个零散块

分块的本质就是一个sqrt(n)叉树,第一层一个节点,第二层sqrt(n)个节点,第三层n个节点

image

对于上图的这个玩意,修改的时候整块打标记,零散块枚举+排序重构

block -> 块大小
belong[i] -> i属于的块编号
l[i] -> i块的左端点
r[i] -> i块的右端点

for( register int i = 1 ; i <= n ; i++ ) belong[i] = ( i - 1 ) / block;
for( register int i = 1 ; i <= n ; i++ )
    if( !l[ belong[i] ] )
        l[ belong[i] ] = i;
for( register int i = n ; i ; i-- )
    if( !r[ belong[i] ] )
        r[ belong[i] ] = i;

modify/find( int L , int R )//修改、查询
if( belong[l] == belong[r] )
    for( register int i = L ; i <= R ; i++ )
        ......
else
{
    for( register int i = L ; i <= r[ belong[L] ] ; i++ )
        ......
    for( register int i = l[ belong[R] ] ; i <= R ; i++ )
        ......
    for( register int i = belong[L] + 1 ; i <= belong[R] - 1 ; i++ )
        ......

那么我们再看一个例题,如果要区间加,然后求区间内小于x的数

修改同上,查询整块时,如果整块上带着标记y,那么就二分查找小于x-y的数,零散块直接暴力查询即可。

让我们来分析一下复杂度

序列长度为n,第二层x个块

修改:

整块O(1) 直接标记即可

零散块O(n/x) 这里按照最劣情况来计算,n/x为块的大小

查询:

整块 O(log(n/x)) 二分查找

零散块O(n/x) 暴力枚举每一块

这里要知道,分块的块大小不一定是sqrt(n),具体是要靠根号平衡来算的,让我们试一下

毒瘤非说平衡出来是sqrt(n log n),但是我们算的都是n/2……

下一个题暂且先当做sqrt(n log n)来

把求区间小于x的数改成求区间第k小,然后还用上一题的方法如何?

这样的复杂度就是O(m sqrt(n log n) logv),被毒瘤卡掉了(尽管我并不知道他这复杂度哪来的

这里我们把块大小设为sqrt(n)logn

查询的时候进行二分答案,在整块上和零散块上分别计算,然后看看够不够k-1个就行了

我们有n个小块,块大小为sqrt(n) logn,所以整块的数量撑死为sqrt(n)/log n,零散块撑死为sqrt(n) logn,整块部分查询复杂度为单次O(log (sqrt(n)logn)),零散块查询复杂度为O(sqrt(n)log n),再暴力零散块时间堪忧。

这里我们把零散的两个块先归并成一个假的块,然后在这里面进行二分

知道这样能提升速度就行,具体复杂度,呵呵,我再信毒瘤就鬼了……

根号平衡的应用

O(1)单点修改,O(sqrt(n))区间和

直接分块,妥妥的。

O(sqrt(n))单点修改,O(1)区间和

中间一层维护块外前缀和,最底下一层维护块内前缀和,查询只需调用单点和其对应的分块,修改就要修改该分块内后面的单点,以及后面的分块的前缀和。

O(sqrt(n))区间加,O(1)查单点

又是裸分块,不解释。

O(1)区间加,O(sqrt(n))查单点

前缀和+查分。对于区间[l,r],给l-1,r和其所在分块打标记,l-1打减标记,r打加标记,然后每次查询只查该点以及它的块中它后面的点,还有它后面的块即可。

O(1)插入一个数,O(sqrt(n))查询第k小

先离散化之后,对值域进行分块,单点修改,每个块保存有几个数,查询的时候从头开始往后跑,直到数够了为止。

O(sqrt(n))插入一个数,O(1)查询第k小

这个玩意?自己真不大能想出来……

毒瘤的课实在难懂,只能看看fym大佬的解法

钻研了好久终于懂了……

这个题还是值域分块,只不过对于每个块,我们要维护的是一个OV(有序表)。

对于每个块,我们都维护l[i]和r[i],表示这个块里面有第l[i]~r[i]名的元素

先说一下比较简单的查询,我们维护belong[i]表示第[i]名在哪个块

然后直接O(1)调用

vec[belong[k]][k-l[belong[k]]]

即可。

我们再看一下插入。

先离散一下原序列的数和要插入的数,然后值域分块,经过这样的预处理,我们可以保证每次插入都绝对会插入到这个点最后应该所在的位置,所以,每次插入只需要在块内重构+改变后面块的l和r,这就OK了。

分块例题:教主的魔法

莫队

简单莫队算法

假设我们可以O(x)插入或删除一个元素

那么对于已知区间[l,r],我们可以O(x)得到[l-1,r],[l+1,r],[l,r-1],[l,r+1]

那么对于两个区间[l1,r1],[l2,r2]

如果把这俩看做平面上的坐标,那么我们可以用这俩的曼哈顿距离abs*(l1-l2)+abs(r1-r2)(不是欧氏距离)

我们可以O(x*(abs(l1-l2)+abs(r1-r2)))由[l1,r1]得到[l2,r2]

这就是简单莫队算法的本质

莫队是一个必须离线的算法,如果强制在线就无能为力了

如果序列长n,有m个询问,我们可以用一种特殊的顺序依次处理每个询问,使sigma在一个可以接受的范围内。我们通常是对序列分块,然后把询问排序,排序的时候以左端点所在块编号为第一关键字,右端点所在块编号为第二关键字。

这样排序的复杂度是O(nsqrt(m))

普通莫队是可以进行大力卡常数的,这里毒瘤主要给了两个技巧:

  1. 排序按照奇偶分别排,虽然玄学但是可以快一倍
   inb cmp (node a,node b)
   {
       if (belong[a.l]^belong[b.l]) return belong[a.l]<belong[b.l];
       if (belong[a.l]&1) return a.r<b.r;
       return a.r>b.r;
   } 
  1. 调一下块的大小,同玄学,可以快10%左右
   block=n/sqrt(m*2/3);

板子题:小Z的袜子

赞 赏
蒟蒻的邮箱:xmzl200201@126.com