算法学习笔记 - 倍增

前言

倍增,字面意思就是“成倍增长”。这是指我们在进行递推时,如果状态空间很大,通常的线性递推无法满足时间与空间复杂度的要求,那么我们可以通过成倍增长的方式,只递推状态空间中在 2 的整数次幂位置上的值作为代表。当需要其他位置上的值时,我们通过“任意整数可以表示成若干个2的次幂项的和”这一性质,使用之前求出的代表值拼成所需的值。所以使用倍增算法也要求我们递推的问题的状态空间关于2的次幂具有可划分行

倍增”与“二进制划分”两个思想相互结合,降低了求解很多问题的时间与空间复杂度。快速幂其实就是“倍增”与“二进制划分”思想的一种体现。其他应用还有,序列上的倍增问题,求解RMQ(区间最值)问题的ST算法,求解最近公共祖先(LCA)等。

快速幂

正如上面所说,每一个正整数可以唯一表示为若干指数不重复的2的次幂的和。所以我们很容易通过 k 次递推求出每个乘积项,当二进制下该位置为 1 时,把该乘积项累积到答案中。b&1 运算可以取出 b 在二进制表示下的最低位,而 b>>1 运算可以舍去最低位,在递推的过程中将二者结合,就可以遍历 b 在二进制表示下的所有数位 ci。

整个算法的时间复杂度为 O(log2 b)

int q_pow(int a,int b,int p){
    int ans=1%p;
    for(;b;b>>=1){
        if(b&1) ans=(long long)ans*a%p;
        a=(long long)a*a%p;
    }
    return ans;
}

ST算法

在RMQ问题(区间最值问题)中,著名的ST算法就是倍增的产物,给定一个长度为 N 的数列 A,ST算法能在O(N logN) 时间的预处理后,以 O(1) 的时间复杂度在线回答“数列 A 中下标在 l~r 之间的数的最大值是多少”这样的区间最值问题。

一个序列的子区间个数显然有 O(N^2) 个,根据倍增思想,我们首先在这个规模为 O(N^2) 的状态空间里选择一些 2 的整数次幂的位置作为代表值。

设 F[i,j] 表示数列 A中下标在子区间 [i,i+2^j-1] 里的数的最大值,也就是从 i 开始的 2^j 个数的最大值。递推边界显然是 F[i,0] = A[i],即数列 A 在子区间 [i,i] 里的最大值。

在递推时,我们把子区间的长度成倍增长,有公式 F[i,j] = max(F[i,j-1],F[i+2^(j-1),j-1]),即长度为 2^j 的子区间的最大值是左右两半长度为 2^(j-1) 的子区间的最大值中较大的一个

void ST——prework(){
    for(int i=1;i<=n;i++) f[i][0]=a[i];
    int t=log(n)/log(2)+1;
    for(int j=1;j<t;j++)
        for(int i=1;i<=n-(1<<j)+1;i++)
            f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}

当询问任意区间 [l,r] 的最值时,我们先计算出一个 k,满足 2^k < r - l +1 ≤ 2^(k+1),也就是使 2 的 k 次幂小于区间长度的前提下最大的 k。那么“从 l 开始的 2^k 个数”和“以 r 结尾的 2^k 个数”这两段一定覆盖了整个区间 [l,r],这两段的最大值分别是 F[l,r] 和 F[r-2^k+1,k],两者中较大的那个就是整个区间 [l,r] 的最值。因为求的是最大值,所以这两段只要覆盖区间 [l,r] 即可,即使有重叠也没关系

int ST_query(int l,int r){
    int k=log(r-l+1)/log(2);
    return max(f[l][k],f[r-(1<<k)+1][k]);
}

最近公共祖先

给定一颗有根树,若节点 z 既是节点 x 的祖先,也是节点 y 的祖先,则称 z 是 x,y 的公共祖先。在 x,y 的所有共公共祖先中,深度最大的一个称为 x,y 的最近公共祖先,记为 LCA(x,y)。

LCA(x,y) 是 x 到根的路径与 y 到根的路径的交汇点,它也是 x 与 y 之间的路径上深度最小的节点

这里着重介绍树上倍增法求LCA。

树上倍增法是一个很重要的算法。除了求 LCA 之外,它在很多问题中都有广泛应用。设 F[x,k] 表示 x 的 2^k 辈祖先,即从 x 向根节点走 2^k 步到达的节点。特别地,若该节点不存在,则令 F[x,k] = 0. F[x,0] 就是 x 的父节点。除此之外,任意k∈[1,log n],F[x,k]=F[F[x,k-1],k-1]。

这类似于一个动态规划的过程,“阶段”就是节点的深度。因此,我们可以对树进行广度优先遍历,按照层次顺序,在节点入队之前,计算它在 F 数组中相应的值。

以上部分是预处理,时间复杂度为O(nlogn),之后可以多次对不同的 x,y 计算 LCA,每次询问的时间复杂度为 O(logn)。

基于 F 数组计算 LCA(x,y) 分为以下几步:

  1. 设 d[x] 表示 x 的深度。不妨设 d[x]≥d[y](否则交换 x,y)
  2. 用二进制拆分思想,把 x 向上调整到与 y 同一深度。具体来说,就是依次尝试从 x 向上走      k = 2^logn,...,2^1,2^1步,检查到达的节点是否比 y 深。在每次检查中,若是,则令 x=F[x,k]
  3. 若此时 x=y,说明已经找到了 LCA,LCA 就等于 y。
  4. 用二进制拆分思想,把 x,y 同时向上调整,并保持深度一致且二者不相会。具体来说,就是依次尝试把 x,y 同时向上走 k=2^logn,...,2^1,2^0步,在每次尝试中,若 F[x,k] ≠ F[y,k](即仍未相会),则令 x = F[x,k],y = F[y,k]。
  5. 此时 x,y 必定只差一步就相会了,它们的父节点 F[x,0] 就是 LCA。

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

#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn=101000;
int n;
int m;
int s;
int tot;
int head[maxn];
int lg[maxn];
int depth[maxn];
int fa[maxn][32];

struct edge{
	int to;
	int from;
	int nxt;
}e[2*maxn];

void add(int x,int y){
	tot++;
	e[tot].to=y;
	e[tot].from=x;
	e[tot].nxt=head[x];
	head[x]=tot;
}

void dfs(int now,int fath){
	fa[now][0]=fath;
	depth[now]=depth[fath]+1;
	for(int i=1;i<=lg[depth[now]];i++) fa[now][i]=fa[fa[now][i-1]][i-1];
	for(int i=head[now];i;i=e[i].nxt){
		int y=e[i].to;
		if(y==fath) continue;
		dfs(y,now);
	}
}

int lca(int x,int y){
	if(depth[x]<depth[y]) swap(x,y);
	while(depth[x]>depth[y]) x=fa[x][lg[depth[x]-depth[y]]-1];
	if(x==y) return x;
	for(int k=lg[depth[x]]-1;k>=0;k--){
		if(fa[x][k]!=fa[y][k]){
			x=fa[x][k];
			y=fa[y][k];
		}
	}
	return fa[x][0];
}

int x,y;

int main(){
	cin>>n>>m>>s;
	for(int i=1;i<n;i++){
		cin>>x>>y;
		add(x,y);
		add(y,x);
	}
	for(int i=1;i<=n;i++) lg[i]=lg[i-1]+(1<<lg[i-1]==i);
	dfs(s,0);
	for(int i=1;i<=m;i++){
		cin>>x>>y;
		cout<<lca(x,y)<<endl;
	}
}
  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值