[python刷题模板] 倍增BinaryLifting

一、 算法&数据结构

1. 描述

倍增是一种优化复杂度的思想,通过把区间压缩到二进制下标的方式,可以大量的合并信息。这也要求区域内的贡献通常是均匀的。
查询时,把路径用二进制分解,那么就可以快速到达目标。
通常,由于初始化需要nlogn的时间,需求应该是离线的。
  • 定义pa[i][j]为节点i的距离2^j位置的节点(可能还需要一个f数组记录这个路径上的贡献值)。
  • 初始化pa[i][0]为i下一个(相邻的)节点,在lca上就是父节点,可能需要dfs之类来初始化这个。
  • 转移:
for i in range(m-1):  # 外层优先遍历步数2^i
	for u in range(n):   # 内层转移节点
		p=pa[u][i];  # 当前节点的父节点
		pa[u][i + 1] = pa[p][i];  # 那么从u跨两个2^i步就到达p的2^i步
		f[u][i+1] = f[u][i]+f[p][i]  # 从u跨两个2^i,就是从u夸一次2^i,从p夸一次。
  • 答案:计算从u出发走k步,则把k二进制分解,同时进行跳跃:
u = s = 0
for j in range(k.bit_length()):
	if k>>j&1:
		x = pa[u][j]
		s += f[u][j]	

2. 复杂度分析

  1. 查询query, O(log2n)
  2. 初始化,O(nlog2n)

3. 常见应用

  1. 步数很大时,快速寻找目标位置:LCA
  2. 快速计算区间值:求和等
  3. 稀疏表ST也用了倍增的思想。

4. 常用优化

  1. m可以初始化为:m = k.bit_length(),代表把k步压二进制最多压成这么多位。注意转移时只要转移range(m-1)。
  2. 如果是lca,计算kth可能越过根时,从高位开始计算可以更快的跳出。
  3. python由于机器缓存的原因,开数组时通常nlg 比lgn强。

二、 模板代码

1. 1483. 树节点的第 K 个祖先(LCA前置模板)

例题: 1483. 树节点的第 K 个祖先

  • 这是离线lca的前置部分,目的是快速求每个节点的第kth个祖先。
  • 求lca的话,把两个节点先调整到同高度,然后从m-1开始向下尝试,大跨步跳即可。复杂度m=log树高
class TreeAncestor:

    def __init__(self, n: int, parent: List[int]):
        m = n.bit_length()
        self.pa = pa = [[-1]*m for _ in range(n)]
        for u,fa in enumerate(parent[1:],start=1):
            pa[u][0] = fa 
        
        for i in range(m-1):
            for u in range(n):
                if (p:=pa[u][i]) != -1:
                    pa[u][i+1] = pa[p][i]


    def getKthAncestor(self, u: int, k: int) -> int:
        for i in range(k.bit_length()):
            if k>>i&1:
                u = self.pa[u][i]
                if u == -1:
                    break
        return u
    
    def get_lca(self, x: int, y: int) -> int:
        """返回 x 和 y 的最近公共祖先(节点编号从 0 开始)
            思路是先让x,y处于同一层,通过kth跳。
            然后尝试迈大步(2^i步),若迈完发现变成同节点就不迈了,尝试2^(i-1)步。
            最后答案pa[x][0],即x、y一定在lca的直接儿子上,"""
        if self.depth[x] > self.depth[y]:
            x, y = y, x
        # 使 y 和 x 在同一深度
        y = self.get_kth_ancestor(y, self.depth[y] - self.depth[x])
        if y == x:
            return x
        for i in range(len(self.pa[x]) - 1, -1, -1):
            px, py = self.pa[x][i], self.pa[y][i]
            if px != py:
                x, y = px, py  # 同时上跳 2**i 步
        return self.pa[x][0]

2. 在有限状态中转移(957. N 天后的牢房)

链接: 957. N 天后的牢房

  • 由于只有8牢房,那么状态最多256个,必然可以很快进入循环节,所以这题其实正解是用vis模拟找循环节。
  • 但依然由于步数很大,可以考虑用倍增。
  • 提前预处理出来每个状态转移的下一个状态是谁,然后开始倍增。
class Solution:
    def prisonAfterNDays(self, cells: List[int], n: int) -> List[int]:
        s = int(''.join(map(str,cells)),2)
        m = n.bit_length()
        
        f = [[-1]*m for _ in range(1<<8)]
        for i in range(1<<8):
            p = 0
            for j in range(1,7):
                if (i>>(j-1)&1) == (i>>(j+1)&1):
                    p |= 1<<j 
            f[i][0] = p 
        for i in range(m-1):
            for j in range(1<<8):
                p = f[j][i]
                f[j][i+1] = f[p][i]

        for j in range(m):
            if n>>j&1:
                s = f[s][j]
        
        ans = bin(s)[2:]        
        ans = '0'*(8-len(ans))+ans        
        return [int(c) for c in ans]

3. 需要计算区间贡献(2836. 在传球游戏中最大化函数值)

链接: 2836. 在传球游戏中最大化函数值

  • 周赛T4,比赛中卡住了,后来学习了一下倍增。不亏。
class Solution:
    def getMaxFunctionValue(self, receiver: List[int], k: int) -> int:
        n = len(receiver)
        m = k.bit_length()
        f = [receiver] + [[0]*n for _ in range(m+1)]
        pa = [receiver] + [[0]*n for _ in range(m+1)]
      
        for i in range(m-1):
            for j in range(n):
                p = pa[i][j]
                pa[i+1][j] = pa[i][p]
                f[i+1][j] = f[i][p] + f[i][j]
        
        ans = 0
        for i,v in enumerate(receiver):
            s = i
            for j in range(k.bit_length()):
                if k >> j&1:
                    s += f[j][i]
                    i = pa[j][i]
            ans = max(ans,s)
        return ans

三、其他

  1. 倍增的lca比较好写,但是依然有一定码量,且只能离线。

四、更多例题

五、参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值