倍增及其简单应用

一、倍增

    倍增是一种特殊的递推、查找方式。如果说你从第一个开始一个一个递推或者一个一个查找会超时,倍增就是一个很好的解决方案。倍增的原理也很简单:每一次往前找2的整数次幂个——例如第一次找一个,第二次2个,第3次4个,第4次8个——直到找到目标数字为止。如果总共有n个数字,这个算法只需要logn的效率。另外,STL的动态数组vector也是通过倍增来申请空间的:第一次申请一个,用完之后再申请1个,然后是2个,4个,8个……一般来讲,在使用倍增时我们的做法是倒序枚举2的整数次幂为步长,能前进(累加)则前进(累加)。

【例1 似曾相识的倍增模板题】

给定一个有n (n<=105)个数的有序递增数列和m (m<=105)个询问,每次询问要你找到某个数字在不在这个数列里,在数列的哪里。

 

分析:这个本来是二分查找的模板,现在我们用倍增解决。出发的位置是0。首先枚举2的整数次幂的步长,从220开始,下一个是219,218,直到20=1。当现在位置是i,加上步长变成了(i+2j),判断(i+2j)比询问的数字大还是小。如果大了,就说明不能往前走;否则就可以往前走。

例如如果要找的点在第115的位置,那么枚举220,219,…都不能往前走;只有枚举到26时才可以(此时来到了64号位置),下一个是枚举25也可以走(此时在96),然后是24还是可以走(此时在112);不过枚举23时就不能走了(因为这样会到达120),22也不行(会到达116);最后枚举21可以走(114),20可以走(115),你就找到115了。

实际上倍增的这种做法与二进制划分是密切相关的,因为任何一个数都可以写成若干个2的整数次幂相加的形式,所以倍增一定能在logn的时间内找到答案。倍增还有别的一些走法,例如先从小到大,再从大到小枚举步长,但是都没有上一种做法简单。

 

二、ST表解决RMQ问题

RMQ是区间最值问题的别名:给你一个数组 ,其中有n个数字,现在给你m个询问,给你区间[l,r],问你在这个区间内的最大值为多少?如果是暴力做法,每次一个for,nm的时间内才可以解决。但是区间最值问题满足区间可加性,就是说知道一个区间左半边的max和右半边的max,就可以知道整个区间的max。当然min也一样。ST表就需要这种性质,联系倍增,可以在nlogn的预处理之后O(1)回答每个询问。(为了方便,接下来的讲解都以max为例)

    建立:设st[i][j]为从i出发,向前2j的范围内的最大值。也就是区间[i,i+2j-1]里的max。然后你可以像dp那样去推:

初始条件:st[i][0]=a[i]

递推方程:st[i][j]=max{st[i][j-1],st[i+2j-1][j-1]}

递推边界:i和(i+2j-1)属于[1,n],j不超过(1+log2n)。

初始条件和递推边界很容易理解,这个递推方程是怎么回事呢?由于st[i][j]管的是区间[i,i+2j-1],st[i][j-1]管的就是[i,i+2j-1-1],而st[i+2j-1][j-1]管的就是区间[i+2j-1,i+2j-1],两个小区间的并集恰好就是大区间。根据max的区间可加性,在两个小区间的max里再取个max,就能得到大区间的max。

枚举的时候也要注意,j放在外边,从1到1+logn顺序枚举。还有就是2i请写成(1<<i),不要写快速幂,更不要用pow函数。球球李门。

    查询:我们现在知道了任意一个长度为2的整数次幂的区间的max,但是他问我的区间长度不一定是2的整数次幂,不能直接来自st数组。所以我们要利用多个max。如果他问我的是区间[l,r],长度len=r-l+1,那么我们整一个t=log2len(向下取整),区间[l,l+2t-1]肯定是[l,r]的子集;区间[r-2t+1,r]也是。而且这两个子区间的并集就是[l,r]。我们欣喜地发现,前一个区间就是st[l][t],后一个就是st[r-2t+1][t]。所以[l,r]的答案就是max{st[l][t], st[r-2t+1][t]}。

    解决RMQ的数据结构要多少有多少,除了ST表还有线段树,树状数组,区间fhq,区间splay,甚至还有主席树,划分树,归并树,笛卡尔树。但是ST表绝对是查询速度最快的,如果查询次数特别多,ST表是最优甚至唯一的选择。

 

【例1 Flowerpot S】

    老板需要你帮忙浇花。给出N滴水的坐标,y表示水滴的高度,x表示它下落到x轴的位置。每滴水以每秒1个单位长度的速度下落。你需要把花盆放在x轴上的某个位置,使得从被花盆接着的第1滴水开始,到被花盆接着的最后1滴水结束,之间的时间差至少为D。

我们认为,只要水滴落到x轴上,与花盆的边沿对齐,就认为被接住。给出N滴水的坐标和D的大小,请算出最小的花盆的宽度W。1≤N≤100000,0≤x,y≤106

 

    分析:首先二分答案,二分花盆宽度,每次要测试花盆的宽度是否合乎要求。比如说当前的花盆宽度是mid,那么for循环枚举花盆的位置:1~mid+1,2~mid+2,……对于位置在i~mid+i的花盆,可以查询横坐标在[i,mid+i]里所有水滴的y坐标的max,与所有y坐标的min的差值。查询max和min就是ST表的任务:在二分之前先按照横坐标为数组下标,预处理一遍所有的水滴建立ST表;每次查询可以O(1)回答。整体的时间复杂度只有xlogx级别,与该题正解单调队列同级。

 

三、倍增LCA

    树上最近公共祖先(LCA):在有根树中,找出某两个结点u和v最近的公共祖先(另一种说法,离树根最远的公共祖先)。利用ST表实际上可以解决LCA问题,但是较麻烦,有兴趣的同学可以查询国家集训队论文《RMQ与LCA问题》(作者郭华阳)

    最常见的LCA就是用倍增解决。但是最简单的LCA是暴力。暴力LCA只需要先深搜一遍获得每个点的父亲节点fa[x]和深度dep[x],接着对于每个询问都暴力往上跳。考虑到有些人连暴力都写不来,这里详细讲一讲暴力怎么写:

第一步:u和v深度可能不一样,先把它们深度跳成一样的。不妨设dep[u]>dep[v],那么只要dep[u]偏大,就不断地让u=fa[u],直至dep[u]==dep[v]。

第二步:深度一样了,但是要特判一下u和v是不是同一个点。如果是同一个点,那直接输出这个点,就是lca。否则转到第三步。

第三步:u和v同时往上跳,直到相遇。也就是说在u!=v的条件下一直让u=fa[u],v =fa[v]。相遇的那个点就是LCA。

    暴力的时间复杂度是nm,但是如果树是随机的,暴力的时间只有log级别。因为对于一棵随机树,树的深度只有logn级别。

    倍增LCA实际上就是暴力优化。首先也需要深搜一遍得到fa数组和dep数组。然后设pare[i][j]是节点i的2j级祖先,就可以类似dp也类似st表的办法进行递推:

初始条件:pare[i][0]=fa[i]

递推方程:pare[i][j]=pare[pare[i][j-1]][j-1]

递推边界:i属于[1,n],j不超过(1+log2n)。

    递推方程可能比较难以理解。方程的字面意思是“i的2j-1级祖先的2j-1级祖先是i的2j级祖先”,非常拗口,只能举例说明。如果j=1,那么pare[i][j]是i的2级祖先,也就是i的爷爷;而i的爷爷是i的父亲的父亲,也就是i的一级祖先的一级祖先;同理j=2的时候,意味着i的高祖(?)是i的爷爷的爷爷;以此类推。如果实在不理解,你就先背下来,以后慢慢就懂了。

    递推的时候要注意,j放在外面循环,从1到1+logn;里边枚举i只需要简单的1~n枚举就好了,不要想那些有的没的。

    预处理完了之后,查询操作和暴力也比较相似,分为三步:

第一步:u和v深度可能不一样,先把它们深度跳成一样的。不妨设dep[u]>dep[v],那么只要dep[u]偏大,就不断地让u往上跳。往上跳的时候倒序枚举j,如果pare[u][j]的深度小于等于v,就可以跳,否则不能跳——和倍增查找是一样的。跳到最后,u和v的深度就会一样。

第二步:深度一样了,但是要特判一下u和v是不是同一个点。如果是同一个点,那直接输出这个点,就是lca。否则转到第三步。

    第三步:u和v同时往上跳。同样是倒序枚举j,如果跳了之后是一样的,说明这就是个公共祖先,但是不知道是不是最近,所以不能跳;如果跳了之后不一样,那就可以放心大胆地跳了。转化成程序语言就是,如果pare[u][j]!=pare[v][j],则u=pare[u][j], v=pare[v][j]。和倍增查找也是一样的。

    第四步:此时u和v只差一步就相遇了,输出它们的父亲就好。

    实际上倍增lca的pare数组还可以用来求i的任意j级祖先,和倍增查找的过程完全一致,这里就不再赘述了。

 

【例1 树上路径和】

给定一棵n个点的树和m次询问,每个点有一个权值,每次询问给你u,v两个点,让你求出u,v路径上每个点的权值之和

 

分析:这也是倍增LCA的模板。首先我们要知道u到v的路径是u->lca(u,v)->v。分为两条,一个是u到lca,一个是lca到v。在预处理的时候,顺便处理一个sum数组:sum[i][j]表示节点i到它的2j级祖先路径上所有点的权值之和(不包括节点i本身),有sum[i][0]=val[fa[i]]。递推方程sum[i][j]=sum[i][j-1]+sum[pare[i][j-1]][j-1]。

查询操作也只需要在lca的模板上稍作修改。首先记录一个ans=val[u]+val[v]。第一步调节深度一致的时候,有u=pare[u][j],同时在ans里累加上sum[u][j]。第二步几乎是一样的,但是要在ans里减去val[v]。第三步也是在跳的同时累加sum。第四步就把ans加上val[fa[v]],输出最终的ans。

不过如果这题数据随机的话,暴力也是mlogn的效率。

posted @ 2022-03-18 14:51  chyuhoin  阅读(675)  评论(0)    收藏  举报