倍增算法讲解——序列、RMQ和LCA

news2024/11/25 20:21:31

倍增算法

文章目录

  • 倍增算法
    • 定义
    • 倍增在序列上的应用
      • 查找
        • 例一
        • 例二
      • 快速幂
    • RMQ(区间最值)
      • 天才的记忆
    • LCA(最近公共祖先)
      • 向上标记法
      • 树上倍增法
        • 祖孙询问
      • Tarjan算法
        • 距离
    • 总结

定义

倍增 从字面的上意思看就是成倍的增长 ,这是指我们在进行递推时,如果状态空间很大,通常的线性递推无法满足时间和空间复杂度的要求 ,那么我们就可以通过成倍的增长,只递推状态空间中在 2 的整数次幂位置上的值作为代表 。

倍增在序列上的应用

一般分为两个阶段,如图。
在这里插入图片描述
阶段1:倍增找到一个满足条件的边界
阶段2:将边界与目标的距离进行二进制划分。

查找

例一

在一个元素递增数组a中查找最大的小于一个数b的数字。
倍增做法:设定一个增长长度p和指示当前所指数字的指针k。先尝试a[k + p]是否满足小于b的条件,若满足条件,则p成2倍增长;否则p缩小范围,继续尝试,直到p缩小到0为止。

def binary_lift(a, b) :
	k, p = 0, 1
	while p : #如果还能扩展k范围则继续扩展
		if k + p - 1 <= len(a) and a[k + p - 1] < b : 
			k += p # 扩展范围
			p <<= 1 #倍增
		else :
			p >>= 1 # 二进制划分
	print(k)

例二

给定长度为 N N N的数列 A A A,然后在线询问若干次,每次给定一个整数 T T T,求出最大的 k k k,满足 ∑ i = 1 k A [ i ] ≤ T \sum^k_{i=1}A[i]\le T i=1kA[i]T

def binary_lift(a, t) :
	length = len(a)
	S = [0] * (length + 1)
	for i in range(1, length + 1) : #前缀和
		S[i] += S[i - 1] + a[i]
	k, p, sum = 0, 1, 0
	while p :
		if k + p <= length and sum + S[k + p] - S[k] <= t : # 倍增
			sum += S[k + p] - S[k]
			k += p
			k >>= 1 # 扩大范围
		else : p <<= 1 #二进制划分

快速幂

求 a 的 b 次方对 p 取模的值。

输入格式
三个整数 a,b,p ,在同一行用空格隔开。

输出格式
输出一个整数,表示 a b m o d p a^b mod p abmodp的值。

数据范围
0≤a,b≤109
1≤p≤109
输入样例:
3 2 7
输出样例:
2

def q_mi(a, b, p) :
	res = 1
	while b :
		if b & 1 :
			res = res * a % p
		b >>= 1 #二进制划分
		a = a * a % p # 倍增
	return res % p
a, b, p = map(int, input().split())
print(q_mi(a, b, p))

RMQ(区间最值)

RMQ 是 R a n g e M a x i m u m / M i n i m u m Q u e r y Range Maximum/Minimum Query RangeMaximum/MinimumQuery的缩写,表示区间最大(最小)值。使用倍增思想解决 RMQ 问题的方法是 ST 表。

  1. 预处理部分
    一个序列的子区间个数显然有 O ( N 2 ) O(N^2) O(N2)个,根据倍增思想,我们首先在这个规模为 O ( N 2 ) O(N^2) O(N2)的状态空间中选择一些2的整数次幂的位置作为代表值。
    设f[i, j]表示从i开始长度是 2 j 2^j 2j的区间的最大值。递推边界为f[i, 0] = a[i], 公式 f [ i , j ] = m a x ( f [ i , j − 1 ] , f [ i + 2 j − 1 , j − 1 ] ) f[i, j]= max(f[i, j - 1], f[i + 2^{j - 1}, j - 1]) f[i,j]=max(f[i,j1],f[i+2j1,j1]),即长度为 2 j 2^j 2j的子区间的最大值是左右两半长度最大值中较大的一个。
  2. 询问部分
    当询问任意区间 [ l , r ] [l, r] [l,r]的最值时,我们先计算一个k,使得从l开始的 2 k 2^k 2k个数和以r结尾的 2 k 2^k 2k个数这两段一定覆盖了整个区间[l, r]。这样就能求出最值 m a x ( f [ l , k ] , f [ r − 2 k + 1 , k ) max(f[l, k], f[r - 2^k + 1, k) max(f[l,k],f[r2k+1,k)。则 2 k < r − l + 1 ≤ 2 ∗ 2 k 2^k <r - l + 1\le 2 * 2^ k 2k<rl+122k。所以 k = l o g 2 ( r − l + 1 ) k = log_2(r - l + 1) k=log2(rl+1)

天才的记忆

从前有个人名叫 WNB,他有着天才般的记忆力,他珍藏了许多许多的宝藏。

在他离世之后留给后人一个难题(专门考验记忆力的啊!),如果谁能轻松回答出这个问题,便可以继承他的宝藏。

题目是这样的:给你一大串数字(编号为 1 到 N,大小可不一定哦!),在你看过一遍之后,它便消失在你面前,随后问题就出现了,给你 M 个询问,每次询问就给你两个数字 A,B,要求你瞬间就说出属于 A 到 B 这段区间内的最大数。

一天,一位美丽的姐姐从天上飞过,看到这个问题,感到很有意思(主要是据说那个宝藏里面藏着一种美容水,喝了可以让这美丽的姐姐更加迷人),于是她就竭尽全力想解决这个问题。

但是,她每次都以失败告终,因为这数字的个数是在太多了!

于是她请天才的你帮他解决。如果你帮她解决了这个问题,可是会得到很多甜头的哦!

输入格式
第一行一个整数 N 表示数字的个数。

接下来一行为 N 个数,表示数字序列。

第三行读入一个 M,表示你看完那串数后需要被提问的次数。

接下来 M 行,每行都有两个整数 A,B。

输出格式
输出共 M 行,每行输出一个数,表示对一个问题的回答。

数据范围
1≤N≤2×105,
1≤M≤104,
1≤A≤B≤N。

输入样例:
6
34 1 8 123 3 2
4
1 2
1 5
3 4
2 3
输出样例:
34
123
123
8

from math import log
N, M = 200010, 18
f = [[0] * M for _ in range(N)]
a = [0] * N
# 预处理部分
def init() :
	for i in range(1, n + 1) :
		f[i][0] = a[i]
	k = int(log(n, 2)) + 1
	for length in range(1, k) :
		for l in range(1, n + 1) :
			r = l + (1 << length) - 1
			if r > n : break
			f[l][length] = max(f[l][length - 1], f[l + (1 << (length - 1))][length - 1]) # 倍增
#查询部分		
def query(l, r) :
	k = int(log(r - l + 1, 2))
	return max(f[l][k], f[r - (1 << k) + 1][k])

n = int(input())
a[1 : n + 1] = list(map(int, input().split()))
init()
m = int(input())
for i in range(m) :
	l, r = map(int, input().split())
	print(query(l, r))

LCA(最近公共祖先)

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。 为了方便,我们记某点集 S = { v 1 , v 2 , … , v n } S=\{v_1,v_2,\ldots,v_n\} S={v1,v2,,vn} 的最近公共祖先为 LCA ( v 1 , v 2 , … , v n ) \text{LCA}(v_1,v_2,\ldots,v_n) LCA(v1,v2,,vn) LCA ( S ) \text{LCA}(S) LCA(S)

向上标记法

O ( n m ) O(nm) O(nm)

从x向上走到根节点,并标记所有经过的节点。
从y向上走到根节点,当第一次遇到已标记的节点时,就找到LCA(x, y)。
在这里插入图片描述

st = [False] * N
ans = 0

def sign(root, x) :
	if root == x : # 访问节点为x返回True
		st[root] = True
		return True
	i = h[root]
	while ~ i :
		j = e[i]
		if sign(j, x) : # 标记路径上含有x的情况
			st[j] = True
			return True
		i = ne[i]
	return False
falg = True
def dfs(root, y) :
	global ans
	if root == y :
		return True
	i = h[root]
	while ~ i :
		j = e[i]
		if dfs(j) :
			if st[j] and flag:
				ans = j
				flag = False #标记第一次遇见已标记点状态
			return True
		i = ne[i]
	return False
def lca(x, y) :
	sign(root, x)
	dfs(root, y)
	return ans

树上倍增法

在线求LCA O ( n l o g n ) O(nlogn) O(nlogn)

  1. 预处理部分
    设f[x, k]表示 x x x 2 k 2^k 2k辈祖先,即从x向根节点走 2 k 2^k 2k步到达的节点。特别的,若该节点不存在,则令f[x, k] = 0.f[x, 0]就是x的父节点。除此之外, ∀ k ∈ [ 1 , l o g n ] , f [ x , k ] = [ f [ x , k − 1 ] , k − 1 ] \forall k\in[1, logn], f[x, k] = [f[x, k - 1], k - 1] k[1,logn],f[x,k]=[f[x,k1],k1]
  2. 查询部分
    基于f数组计算LCA(x, y)分为以下几步:
    1. 设d[x]表示x的深度。不妨 d [ x ] ≥ d [ y ] d[x] \ge d[y] d[x]d[y](否则可以交换x, y)
    2. 用二进制拆分思想,把x向上调整到y同一深度。
    3. 若此时 x = = y x == y x==y,说明已经找到了LCA,LCA 就等于y
    4. 用二进制拆分思想,把x,y同时向上调整,并保持深度一致,且二者不会相会。
    5. 此时x,y必定只差一步就相会了,它们的父节点就是LCA了。

祖孙询问

给定一棵包含 n 个节点的有根无向树,节点编号互不相同,但不一定是 1∼n。

有 m 个询问,每个询问给出了一对节点的编号 x 和 y,询问 x 与 y 的祖孙关系。

输入格式
输入第一行包括一个整数 表示节点个数;

接下来 n 行每行一对整数 a 和 b,表示 a 和 b 之间有一条无向边。如果 b 是 −1,那么 a 就是树的根;

第 n+2 行是一个整数 m 表示询问个数;

接下来 m 行,每行两个不同的正整数 x 和 y,表示一个询问。

输出格式
对于每一个询问,若 x 是 y 的祖先则输出 1,若 y 是 x 的祖先则输出 2,否则输出 0。

数据范围
1≤n,m≤4×104,
1≤每个节点的编号≤4×104
输入样例:
10
234 -1
12 234
13 234
14 234
15 234
16 234
17 234
18 234
19 234
233 19
5
234 233
233 12
233 13
233 15
233 19
输出样例:
1
0
0
0
2

from math import log
from collections import deque

N, M = 40010, 18
h = [-1] * N
e = [0] * N * 2
ne = [-1] * N * 2
idx = 0
f = [[0] * M for _ in range(N)]
d = [0] * N

def add(a, b) :
	global idx
	e[idx] = b
	ne[idx] = h[a]
	h[a] = idx
	idx += 1

def bfs(root) :
	que = deque()
	que.appendleft(root)
	d[root] = 1
	while len(que) :
		t = que.pop()
		i = h[t]
		while ~ i :
			j = e[i]
			if not d[j] : #防止向上遍历
				que.appendleft(j)
				d[j] = d[t] + 1
				f[j][0] = t
				for k in range(1, length) : # 倍增
					f[j][k] = f[f[j][k - 1]][k - 1]
			i = ne[i] 
def lca(a, b) :
	if d[a] < d[b] : a, b = b, a
	for i in range(length, -1, -1) : # 二进制拆分
		if d[f[a][i]] >= d[b] : # 尝试跳跃i距离,最终必定跳到与b同一高度
			a = f[a][i]
	if a == b : return b
	for i in range(length, -1, -1) : # 二进制拆分
		if f[a][i] != f[b][i] : # 尝试跳跃,如果没跳跃到公共祖先节点,否则缩小区间再跳。
			a, b = f[a][i], f[b][i]
	return f[a][0]

n = int(input())
length = int(log(n, 2)) + 1
root = 0
for i in range(n) :
	a, b = map(int, input().split())
	if ~ b :
		add(a, b), add(b, a)
	else : root = a

bfs(root)

m = int(input())
for i in range(m) :
	a, b = map(int, input().split())
	p = lca(a, b)
	if p == a : print(1)
	elif p == b : print(2)
	else : print(0)

Tarjan算法

离线求LCA O ( n + m ) O(n + m) O(n+m)

Tarjan算法本质是使用并查集对“向上标记法”的优化。
在深度优先遍历的任意时刻,树中节点分为3类:

  1. 已经访问完毕并且回溯的节点。在这些节点标记一个整数2.
  2. 已经开始递归,但尚未回溯的节点。这些节点就是当前正在访问的节点x以及x的祖先。在这些节点上标记一个整数1.
  3. 尚未访问的节点。这些节点没有标记。

对于正在访问的节点 x x x,它到根节点的路径已经标记为1.若y是已经访问完毕并且回溯的节点,则LCA(x, y)就是从y向上走到根,第一个遇到标记为1的节点。
可以利用并查集进行优化,当一个节点获得整数2的标记时,把它所在的集合合并到它父节点所在的集合中(合并时它的父节点的标记一定为1,且单独构成一个集合)。
这相当于每个完成回溯的节点都有一个指针指向它的父节点,只需要查询y所在集合代表元素,就等价于y一直向上一直走到一个开始递归但尚未回溯的节点(标记1),即LCA(x, y)

距离

给出 n 个点的一棵树,多次询问两点之间的最短距离。

注意:

边是无向的。
所有节点的编号是 1,2,…,n。
输入格式
第一行为两个整数 n 和 m。n 表示点数,m 表示询问次数;

下来 n−1 行,每行三个整数 x,y,k,表示点 x 和点 y 之间存在一条边长度为 k;

再接下来 m 行,每行两个整数 x,y,表示询问点 x 到点 y 的最短距离。

树中结点编号从 1 到 n。

输出格式
共 m 行,对于每次询问,输出一行询问结果。

数据范围
2≤n≤104,
1≤m≤2×104,
0<k≤100,
1≤x,y≤n
输入样例1:
2 2
1 2 100
1 2
2 1
输出样例1:
100
100
输入样例2:
3 2
1 2 10
3 1 15
1 2
3 2
输出样例2:
10
25

N = 10010
M = N * 2

h = [-1] * N
e = [0] * M
w = [0] * M
ne = [-1] * M
idx = 0
dist = [0] * N
res = [0] * M
st = [0] * N
p = [0] * N
query = [[] for _ in range(N)]

def add(a, b, c) :
    global idx
    e[idx] = b
    w[idx] = c
    ne[idx] = h[a]
    h[a] = idx
    idx += 1
def find(x) :
	if x != p[x] : p[x] = find(p[x])
	return p[x]

def dfs(u, fa) : #求每个节点到根节点距离
	i = h[u]
	while ~ i :
		j = e[i]
		if j != fa : #防止回父节点
			dist[j] = dist[u] + w[i]
			dfs(j, u)
		i = ne[i]
def tarjan(u) :
	st[u] = 1
	i = h[u]
	while ~ i :
		j = e[i]
		if not st[j] : # 防止多次遍历
			tarjan(j)
			p[j] = u # 回溯完合并到此时为1的节点的集合
		i = ne[i]
	for item in query[u] :
		y, id = item[0], item[1]
		anc = find(y)
		if st[y] == 2 :
			res[id] = dist[u] + dist[y] - dist[anc] * 2
	st[u] = 2 #标记回溯

n, m = map(int, input().split())

for i in range(n - 1) :
    a, b, c = map(int, input().split())
    add(a, b, c), add(b, a, c)
    
for i in range(1, n + 1) : #初始化并查集
    p[i] = i

for i in range(m) :
    a, b = map(int, input().split())
    if a != b :
        query[a].append([b, i])
        query[b].append([a, i])

dfs(1, -1)    
tarjan(1)

for i in range(m) :
    print(res[i])

总结

其实很多二进制优化的题目,都可以归结为倍增和二进制拆分,多重背包就是个例子。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/169138.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++系列案例-大数据减法-绘制余弦曲线-兔子数量-快速排序

文章目录关于C的几个经典案例代码大数减法问题绘制余弦曲线兔子数量问题快速排序问题函数运行全部源码关于C的几个经典案例代码 大数减法问题 因为较大整数的相加很可能超出整型的32位限制&#xff0c;或者本身就是超出限制的大数之间的加减运算。 所以我们需要单独写一个能大…

C语言程序设计易混、易错知识点(下篇)

有环的单向链表跟无环的单向链表不可能相交 -正确 有环的单向链表和无环的单向链表不能相交&#xff0c;因为当相交的时候&#xff0c;无环的单向链表也会被迫存在一个环&#xff0c;只不过这个环的”起点“可能不是原来单向链表的头结点 如果两个单向链表相交&#xff0c;那这…

Vmware Pro 17 设置共享文件夹

目录 一、概述 二、在VMware设置共享文件夹详细步骤 一、概述 VMware 是运行在Windows系统上的&#xff0c;很多时候需要将 Windows 系统上的资料或代码复制到运行在VMware上的虚拟机&#xff0c;通常有两种方法可以完成复制&#xff0c;一个是在Vmware软件上设置共享文件夹&…

网络分层模型和应用协议

网络分层模型和应用协议 分层模型 分层模型的意义 当遇到一个复杂问题的时候&#xff0c;可以使用分层的思想把问题简单化。 比如&#xff0c; 你有一杯82年的可乐&#xff0c;想要分享给你的朋友张富贵&#xff0c;但你们已经很久没有联系&#xff0c;要完成这件事&#xf…

《深入浅出计算机组成原理》学习笔记 Day2

文章目录指令篇1. 从高级语言到机器指令1.1 CPU的作用1.2 代码如何变为机器码1.3 指令的分类2. 指令跳转2.1 CPU 是如何执行指令2.2 条件和循环的本质3. 函数调用3.1 栈的作用3.2 Stack Overflow指令篇 1. 从高级语言到机器指令 计算机或者说CPU本身并没有能力去理解这些高级…

配置远程服务器时候sftp的配置以及注意事项【在本地配置sftp】

我之所以不用PutTy是因为这个工具有问题&#xff0c;在我的电脑上怎么也安装不上&#xff0c;也找了很多办法根本没用。如这个 我试过修改权限以及各种安装办法连安装都不行。 所以才决定直接使用vscode自带的上传工具上传到服务器。 想要上传到服务器&#xff0c;需要使用vsc…

2023牛客寒假算法基础集训营1 -- G-鸡格线(map + 内置二分写法)

题目如下&#xff1a; 示例1 输入 3 5 0 2 114514 2 1 1 2 2 2 1 1 3 1 2输出 114516 114551 3445思路 or 题解&#xff1a; 通过 f(x)round(10∗x)f(x) round(10*\sqrt{x})f(x)round(10∗x​) 我们可得到&#xff1a; 经过至多 111111 次 0 -> 01~99 -> 99100 ~ i…

如何利用jar命令把前端代码打进jar包

目录背景介绍简单介绍几个常用的jar命令参数查询一个文件在jar包中的位置将文件解压到当前目录把一个目录打包进jar把一个文件打包进jar更新jar的shell脚本update.sh命令执行过程部分截图背景介绍 前后端分离是目前主流的开发模式&#xff0c;部署的时候也是利用类似nginx实现…

C++之多态

文章目录一、多态的理解二、多态的定义及实现1.多态的构成条件2.虚函数3.虚函数的重写/覆盖4. C11 的 override 和 final5.重载、重写/覆盖、隐藏/重定义三、抽象类四、多态的原理1.虚函数表2.多态的原理3.动态绑定与静态绑定五、单继承和多继承关系的虚函数表1.单继承中的虚函…

【Git】IDEA 集成 Git

7、IDEA 集成 Git 7.1、配置 Git 忽略文件 1、Eclipse 特定文件 2、IDEA 特定文件 3、Maven 工程的 target 目录 问题 1:为什么要忽略他们&#xff1f; 答&#xff1a;与项目的实际功能无关&#xff0c;不参与服务器上部署运行。把它们忽略掉能够屏蔽 IDE 工具之间的差异。 …

联合变换相关器摄远物镜光学设计

联合变换相关器摄远物镜光学设计 联合变换相关器工作原理 随着科学技术的飞速发展&#xff0c;光学相关探测器件由最初的匹配滤波器发展到今天的联合变换相关器&#xff0c;联合变换相关器与范得耳-卢格特相关器相比&#xff0c;具有灵活性好、识别精度高等特点&#xff0c;所…

Media基础知识一

1.视频文件是什么&#xff1f; FLV, MKV, MP4是不同的视频后缀名&#xff0c;不同的视频格式就像一个容器。容器里封装的是音视频流。 FFmpeg&#xff1a;一款开源软件&#xff0c;用来处理音视频&#xff0c;对音视频进行编解码。要在Android中使用FFmpeg&#xff0c;需要下载…

vue-element-admin 换肤功能,登录后不同权限不同皮肤,刷新不会失效

一、拉vue-element-admin的代码跑起来 安装依赖时会遇到一些问题&#xff0c;tui-editor装不上&#xff0c;需要按照以下步骤删除它 1、vue-element-admin\package.json 删除‘tui-editor’&#xff1a;‘1.3.3’依赖项。 2、vue-element-admin\src\components 删除MarkdownE…

C++ 三种智能指针及其设计实现unique_ptr、 share_ptr 指针

0、差不多春节啦。。。。。 好久没有写博客&#xff0c;写一写吧。。。。。。 祝大家嗨皮&#xff0c;提前恭喜发财 1、三种智能指针的使用方法 C 有3种指针&#xff1a;share_ptr, unique_ptr, weak_ptr 1.1&#xff09;unique_ptr 指针 std::unique_ptr 是一种独占的智能指…

Sklearn标准化和归一化方法汇总(1):标准化 / 标准差归一化 / Z-Score归一化

Sklearn中与特征缩放有关的五个函数和类&#xff0c;全部位于sklearn.preprocessing包内。作为一个系列文章&#xff0c;我们将逐一讲解Sklearn中提供的标准化和归一化方法&#xff0c;以下是本系列已发布的文章列表&#xff1a; Sklearn标准化和归一化方法汇总(1)&#xff1a…

ESP32 (WIFI)-AP、STA模式(13)

提示&#xff1a;本博客作为学习笔记&#xff0c;有错误的地方希望指正 文章目录一、ESP32 WIFI模式概述二、ESP32 WIFI-AP初始化流程三、WIFI-AP示例四、ESP32 WIFI-STA初始化流程五、WIFI-STA示例一、ESP32 WIFI模式概述 参考资料&#xff1a;ESP IDF编程手册V4.4   WIFI主…

【GD32F427开发板试用】Systick系统定时器的使用

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;HonestQiao 基于Arm Cortex-M系列内核的MCU&#xff0c;都包含了SysTick定时器。 所谓SysTick即为系统定时器&#xff0c;又称嘀嗒定时器&am…

Docker Swarm

Swarm 是什么&#xff1f; Docker Swarm 是Docker官方的跨节点的容器编排工具。用户只需要在单一的管理节点上操作&#xff0c;即可管理集群下的所有节点和容器。 主要解决什么问题 1. 解决docker server的集群化管理和部署。 2. Swarm通过对Docker宿主机上添加的标签信息来…

分享60个PHP源码,总有一款适合您

PHP源码 分享60个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 60个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1SvjbzolwuMrQyhVb_byG5Q?pwdx831 提取码&#xff…

生物素点击标记试剂:DBCO-SS-PEG3-biotin,1430408-09-5,生物素PEG3二硫键DBCO

1、理论分析&#xff1a;中文名&#xff1a;生物素-三聚乙二醇-二硫-二苯并环辛炔&#xff0c;生物素-PEG3-二硫-DBCO &#xff0c;生物素PEG3-二硫二苯并环辛炔英文名&#xff1a;DBCO-S-S-PEG3-biotin&#xff0c;Biotin-PEG3-SS-DBCOCAS号&#xff1a;1430408-09-5化学式&am…