Binary Lifting
快速幂技术
快速幂技术是一种非常高效且简单的优化技术。对于环U上的某个元素x,我们要求xn,我们可以用快速幂技术仅用O(log2n)次乘法运算即可高效完成这个过程。
快速幂的原理如下:
{xn=(xn2)2,2∣nxn=(xn−12)2⋅x,else即根据n的奇偶性走两套逻辑。定义T(n)表示指数为n所需要执行的运算次数,可以发现T(n)=T(n/2)+O(1),因此有T(n)=O(log2n)。
快速幂能适用于一切满足结合律的运算,不仅限于乘法运算。下面给出一些例题:
题目1:要求仅用加法以及位运算计算两个整数的乘积a⋅b。
可以发现a⋅b=∑bi=1a,而加法也是满足结合律的,因此可以用快速幂来优化。
题目2:计算斐波那契数列的第1018项的后四位数字。
由于斐波那契是线性递推数列,因此我们可以用矩阵来表示递推关系。而矩阵的乘法运算是满足结合律的。
题目3:有n个石头(n≤109),两个人进行比赛,每个人每次可以拿走a1,a2,…,am块石头,这里a1,…,am≤20且一定包含1。拿走最后一块石头的人为输家。问在最优决策的情况下,哪一方一定会获胜。
可以用DP计算,但是n太大了。我们发现每个状态都仅有最多前面20个状态所完全决定,因此我们可以定义一个函数f:Z202→Z202,暴力算出前20个状态,其向量形式记作s,而我们要计算的实际上是fn−19(s)。由于函数的复合满足结合律,因此可以用快速幂加速。每次函数复合的时间复杂度为O(220),总的时间复杂度为O(20+220log2n)。
倍增技术
给定一个有向图G=(V,E),每个结点只有唯一的出边,记next(u)表示u沿着唯一的出边移动一步后抵达的位置,记link(u,i)表示从u出发,通过少于i次的next转移能抵达的所有结点的集合。
在图上进行倍增实际上就是我们维护另外一个序列jump(u,i),它表示u沿着出边移动2i步所在的位置。很显然有jump(u,0)=next(u),且jump(u,i)=jump(jump(u,i−1),i−1)。因此我们可以O(Vlog2V)求解出所有整个jump。
可以发现我们可以在做jump的时候同时进行二分,类似在线段树上进行二分。
并且如果仅仅为了二分的话,倍增实际上可以做到O(n)的预处理空间复杂度和处理时间复杂度的。详情可以参考这篇文章。
题目1:给定n个区间s1,…,sn,如果两个区间独立,当且仅当它们没有交点。现在要求回答q个请求,每个请求给定L,R,要求仅考虑落在L,R中的区间,要求找到它们的最大独立集大小。
首先我们可以认为给定的区间没有包含关系(否则,较小的区间肯定更优,我们可以删除那些包含其它区间的区间)。
实际上对于每个请求,我们可以发现存在一个贪心算法,就是我们给定左边界L后,找到一个左边界大于L的区间,同时要求它的右边界最小。由于区间没有包含关系,因此等价于找到左边界最小(但是大于L)的区间[l,r],之后我们把L更新为r。重复这个过程,直到找不到满足条件的区间为止。
之后考虑如何处理多个请求。
我们可以先让区间按照左边界进行排序,现在问题变成了仅考虑sl,sl+1,…,sr,最大独立集大小。记next(i)表示si右边最小的下标j,满足sj的左边界大于si的右边界。之后在上面建立倍增关系。那么问题就变成了从sl出发最多可以移动多少步而不越界,这个可以在jump数组上进行二分解决。每个请求可以O(log2n)解决。
倍增结构
读这部分内容之前需要先读倍增技术部分。
倍增技术的强大是基于一个很简单的倍增结构。这个结构实际上还有额外的用处,我们可以将jump(u,i)视作一个结点,它覆盖了所有从link(u,2i)上的结点,称i为这个结点的高度。
那么如果现在有这样的一些问题,我们需要处理若干个请求,每个请求要求修改路径link(u,l)上的所有结点。在所有请求完成后,要求输出所有结点的权值。
我们实际上可以发现u,v对应的区间可以截断为O(log2n)个倍增结构上的结点,我们只需要在这些结点上打上标记就可以了。并且考虑到标记只需要从高度较大的结点下推到高度较小的结点,因此在最后阶段我们可以从高到低处理结点。
当然这个问题平平无奇,LCT数据结构也可以做到O(nlog2n),但是倍增结构可以处理存在环的情况,而LCT就需要比较复杂的特殊处理。
现在再考虑一个问题,给定n个结点,之后有q个请求。每个请求给定两端等长区间(a,b)和(c,d),表示对于所有0≤i≤b−a,结点a+i和c+i在同一个连通块中。接下来要求计算最多可能存在多少个连通块。
我们可以考虑把并查集的合并关系维护到我们的倍增结构上去,这样的话每次合并只涉及到O(log2n)个倍增结构上的顶点。类似的最后先处理高度较大的顶点下推合并关系即可。
再来考虑一个问题。现在有n个人,以及一颗大小为m的树。第i个人可以居住在ui和vi之间的路径上的任意一个顶点中,且一个顶点最多居住一个人。现在希望让尽可能多人居住在树上,问最多有多少人可以居住在树上。
可以发现每个结点只有一个父亲,因此我们把next(u)设置为u的父亲。之后我们建立网络流,将第i个人向路径ui到vi上的所有结点连一条边,借助倍增结构,我们可以只需要向O(log2m)个结点连边。这样我们得到了一个包含O(mlog2m+n)个结点和O((n+m)log2m)条边的网络。之后求最大流即可。