[python刷题模板] 倍增BinaryLifting
-
- [一、 算法&数据结构](#一、 算法&数据结构)
-
- [1. 描述](#1. 描述)
- [2. 复杂度分析](#2. 复杂度分析)
- [3. 常见应用](#3. 常见应用)
- [4. 常用优化](#4. 常用优化)
- [二、 模板代码](#二、 模板代码)
-
- [1. 1483. 树节点的第 K 个祖先(LCA前置模板)](#1. 1483. 树节点的第 K 个祖先(LCA前置模板))
- [2. 在有限状态中转移(957. N 天后的牢房)](#2. 在有限状态中转移(957. N 天后的牢房))
- [3. 需要计算区间贡献(2836. 在传球游戏中最大化函数值)](#3. 需要计算区间贡献(2836. 在传球游戏中最大化函数值))
- 三、其他
- 四、更多例题
- 五、参考链接
一、 算法&数据结构
1. 描述
倍增是一种优化复杂度的思想,通过把区间压缩到二进制下标的方式,可以大量的合并信息。这也要求区域内的贡献通常是均匀的。
查询时,把路径用二进制分解,那么就可以快速到达目标。
通常,由于初始化需要nlogn的时间,需求应该是离线的。
- 定义pa[i][j]为节点i的距离2^j位置的节点(可能还需要一个f数组记录这个路径上的贡献值)。
- 初始化pa[i][0]为i下一个(相邻的)节点,在lca上就是父节点,可能需要dfs之类来初始化这个。
- 转移:
python
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二进制分解,同时进行跳跃:
python
u = s = 0
for j in range(k.bit_length()):
if k>>j&1:
x = pa[u][j]
s += f[u][j]
2. 复杂度分析
- 查询query, O(log~2~n)
- 初始化,O(nlog~2~n)
3. 常见应用
- 步数很大时,快速寻找目标位置:LCA
- 快速计算区间值:求和等
- 稀疏表ST也用了倍增的思想。
4. 常用优化
- m可以初始化为:
m = k.bit_length()
,代表把k步压二进制最多压成这么多位。注意转移时只要转移range(m-1)。 - 如果是lca,计算kth可能越过根时,从高位开始计算可以更快的跳出。
- python由于机器缓存的原因,开数组时通常nlg 比lgn强。
二、 模板代码
1. 1483. 树节点的第 K 个祖先(LCA前置模板)
- 这是离线lca的前置部分,目的是快速求每个节点的第kth个祖先。
- 求lca的话,把两个节点先调整到同高度,然后从m-1开始向下尝试,大跨步跳即可。复杂度m=log树高
python
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模拟找循环节。
- 但依然由于步数很大,可以考虑用倍增。
- 提前预处理出来每个状态转移的下一个状态是谁,然后开始倍增。
python
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. 在传球游戏中最大化函数值)
- 周赛T4,比赛中卡住了,后来学习了一下倍增。不亏。
python
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
三、其他
- 倍增的lca比较好写,但是依然有一定码量,且只能离线。