【算法日常】浅谈倍增(好吧我是用来凑字数的)

倍增

定义

倍增是一种与二分相似的算法,但是把二进制摆在了明面上。

大体思路是一步步确定答案的二进制表示的每一位。

简单倍增

例题:洛谷 P2249 【深基13.例1】查找

你说得对,但是这题其实是二分模板题。

首先转化为找到第一个小于 q q q 的位置 p p p。

因为长度不大于 1 0 6 10^6 106,所以答案一定可以用一个二十位二进制数来表示,因为 2 20 > 1 0 6 2^{20}>10^6 220>106

然后我们就尝试从高到低确定这个答案的每一位。

显然对于从低到高第 i i i 位,数值是 2 i 2^{i} 2i,其中 i ≥ 0 i\ge 0 i≥0。

那么我们判断,如果 2 i ≤ n ∧ a p i < q 2^i\le n\land a_{p_{i}}<q 2i≤n∧api<q,那么说明答案的第 i i i 位一定是一。

因为如果这一位不是一,那么就算后面所有的二进制位都是 1 1 1,总和也没有 2 i 2^{i} 2i 大,是更到不了 a a n s a_{ans} aans 的。

如果 a p i ≥ q a_{p_i}\ge q api≥q,说明跳太大了,这一位是 0 0 0。

简言之,二分是通过检查 mid 的可行性,而倍增是通过 check 每个二进制位的可行性。

以此类推,直到找到答案为止。

最后要记得特判是否越界。

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ljl;
#define FUP(i,x,y) for(int i=(x);i<=(y);++i)
#define FDW(i,x,y) for(int i=(x);i>=(y);--i)
const int N=1e6+5;
int n,q,a[N],x;

void Main()
{
	int p=0;cin>>x;
	FDW(i,20,0)
	{
		if((p|(1<<i))<=n&&a[p|(1<<i)]<x)
			p=p|(1<<i);
	}
	if(p<n&&a[p+1]==x)cout<<p+1<<' ';
	else cout<<"-1 ";
	return;
}
int main(){
	ios::sync_with_stdio(0);
	cin>>n>>q;
	FUP(i,1,n)cin>>a[i];
	while(q--)Main();
	cout<<'\n'; 
	return 0;
}

对于倍增的感性理解

首先我们知道,对于任意一个整数都可以分解成二进制的形式。

那么假设我们开了挂,知道了答案,那么这个答案 a n s ans ans 也是由一位位的二进制数表达的。

那么我们就可以尝试跳步子。

第一次跳足够长,如果超出了,就撤回这一步,尝试更小的步子。

如果没超过,就迈出去。

这样迈出去对应 1 1 1,撤回对应 0 0 0,总可以凑成 a n s ans ans。

树上倍增

因为倍增的结构相对稳定,那么就可以在多个数据结构上使用,就比如树。

举个例子,LCA!

Luogu P3379 【模板】最近公共祖先(LCA)

那么在树上,我们该怎么维护呢?

首先二分显然不太好搞。因为无法快速求出两点之间的中点。

我们考虑递推。

设 f i , j f_{i,j} fi,j 表示从 i i i 出发,跳了 2 j 2^j 2j 步所到达的点。

那么显然有 f i , j = f f i , j − 1 , j − 1 f_{i,j}=f_{f_{i,j-1},j-1} fi,j=ffi,j−1,j−1,即在 i i i 上先跳 2 j − 1 2^{j-1} 2j−1 步,再跳 2 j − 1 2^{j-1} 2j−1 步的结果。

因为 2 j = 2 j − 1 + 2 j − 1 2^j=2^{j-1}+2^{j-1} 2j=2j−1+2j−1。

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5,M=N;
int n,q,s,cnt_e,ehead[N],fa[N][25],dep[N];
struct E{
	int to,pre;
}e[M<<1];
void adde(int from,int to)
{
	e[++cnt_e].to=to;
	e[cnt_e].pre=ehead[from];
	ehead[from]=cnt_e;
	return;
}
void dfs(int u,int uf)
{
	fa[u][0]=uf;dep[u]=dep[uf]+1;
	for(int i=1;i<=20;++i)
		fa[u][i]=fa[fa[u][i-1]][i-1];
	for(int i=ehead[u];i;i=e[i].pre)
	{
		int v=e[i].to;
		if(v==uf)continue;
		dfs(v,u);
	}
	return;
}
int getlca(int x,int y)
{
	if(x==y)return x;
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;--i)
		if(dep[x]-(1<<i)>=dep[y])
			x=fa[x][i];
	if(x==y)return x;
	for(int i=20;i>=0;--i)
	{
		if(fa[x][i]!=0&&fa[x][i]!=fa[y][i])
			x=fa[x][i],y=fa[y][i];
	}
	return fa[x][0];
}
int main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>q>>s;
	for(int i=1,u,v;i<n;++i)
	{
		cin>>u>>v;
		adde(u,v);adde(v,u);
	}
	dfs(s,0);
	while(q--)
	{
		int u,v;cin>>u>>v;
		cout<<getlca(u,v)<<'\n';
	}
	return 0;
}

同理,树上倍增不仅可以维护祖先,还可以顺便维护一些链信息。

比如用 m i , j m_{i,j} mi,j 表示从 i i i 出发,跳 2 j 2^j 2j 步的过程中,所有节点的权值最大值。

那么状态转移方程也大差不差: m i , j = max ⁡ { m i , j − 1 , m f i , j − 1 , j − 1 } m_{i,j}=\max\left\{m_{i,j-1},m_{f_{i,{j-1}},j-1}\right\} mi,j=max{mi,j−1,mfi,j−1,j−1}

未知上界

如题,就是一种上界未知的二分。

其实说是未知,但毕竟是有答案的,所以上界也是有的。

现在讨论的是如何快速确定上界,即算法本身不依赖于上界,时间复杂度为 log ⁡ a n s \log ans logans。

还是用二进制玩。

我们从小到大枚举 i i i,check 一下 2 i 2^i 2i 可不可行。如果没跳过就跳。

如果跳过了,假设 2 t 2^t 2t 时超过答案。

那么答案就被确定在了 [ 2 t − 1 , 2 t ) [2^{t-1},2^t) [2t−1,2t),然后再从 t − 1 t-1 t−1 至 0 0 0 枚举 j j j,按照之前说的倍增方法一步一步确定答案即可。

总结:先逐渐扩大步长,够用了后再一步步缩短步长。也就是只有锁定第一步时是从小到大,其他都是从大到小。

优势:不依赖总大小,只依赖于答案。

相关推荐
zxy28472253012 小时前
C#的视觉库Halcon入门示例
c#·图像识别·halcon·机器视觉
瑶光守护者2 小时前
【学习笔记】5G RedCap:智能回落5G NR驻留的接入策略
笔记·学习·5g
你想知道什么?2 小时前
Python基础篇(上) 学习笔记
笔记·python·学习
monster000w2 小时前
大模型微调过程
人工智能·深度学习·算法·计算机视觉·信息与通信
小小晓.2 小时前
Pinely Round 4 (Div. 1 + Div. 2)
c++·算法
SHOJYS2 小时前
学习离线处理 [CSP-J 2022 山东] 部署
数据结构·c++·学习·算法
biter down2 小时前
c++:两种建堆方式的时间复杂度深度解析
算法
weixin_409383123 小时前
简单四方向a*学习记录4 能初步实现从角色到目的地寻路
学习·a星
zhishidi3 小时前
推荐算法优缺点及通俗解读
算法·机器学习·推荐算法
WineMonk3 小时前
WPF 力导引算法实现图布局
算法·wpf