最近再刷关于图的算法,这两天刚好刷到了LCA,学习的时候网上大部分代码都是c++写的,我就花时间整理了JAVA语言的模版
原题链接:
P3379 【模板】最近公共祖先(LCA) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
求lca问题一般可以用倍增和Tarjan,这里我用的倍增算法
原理讲解
正常情况下你要求两个节点的lca,都是向上暴力递归,就像你找9和2的lca
9->3->6->5
2->5
一直找到第一个祖先相同的时候,但面对大量数据必然会爆掉,一分没有;
倍增算法在我这几天的学习过程中我认为它是二进制和二分算法的结合体,二进制在很多算法都有用到(状压,快速幂..),在这里了类似于一下找到到lca的一半路程
9->6->5
2->5
这是什么原理呢?
我们先初始化每个节点的深度,像9的深度就是4(根节点深度默认为0),而它和2的lca深度为1
深度差就是3,二进制表示就是11,我们一般都是从大到小往上跳,因为你要快速准确求一个数的二进制表达形式肯定要从左往右求把,它第一次跳就跳二进制最左边的1,也就是10(二进制)也就是跳两步跳到6,最后再跳一步到5.
当数据量特别庞大的时候就可以大大减少跳的步数了(假如你的lca相差1019层)
eg:1019(1111111011)=512+256+128+..+8+2+1,(九次)
比暴力解法的1019次少了1010次,可以看出时间节省了很多
代码如何实现:
预处理
首先你要预处理每个节点的2^n(n>=0)的祖先节点是哪个
首先fa[now][j]表示的就是当前节点的第2^j个祖先
核心代码: fa[u][i]=fa[fa[u][i-1]][i-1];
这里的原理就是:2^i = 2^(i-1) + 2^(i-1),给大家举个例子吧
你要求u节点的第八代祖先,是不是等于它的第四代祖先的第四代祖先
为什么这要求呢?当然也是节省时间了,这个预处理是从根节点向下递归,当你求到u节点
的时候,它的所有祖先都已经被预处理完成了
public static void dfs(int u,int father){//倍增算法预处理
dep[u]=dep[father]+1;//当前节点深度等于父节点深度+1
fa[u][0]=father;//2^0=1
//用来求出上限,也就是求这个深度二进制表示中最左边的1是多大
int temp=(int)(Math.log(dep[u])/Math.log(2));
for (int i = 1; i <=temp ; i++) {
fa[u][i]=fa[fa[u][i-1]][i-1];//核心代码
}
//继续递归它的孩子
for (int v:g[u]) {
if (v!=father) dfs(v,u);
}
}
求LCA
首先你就是要将这两个节点跳到相同高度上,也就是说假如两个节点深度分别是14和15
你需要先将两个节点同时到14节点上,然后再一起向上跳,直到第一次它俩祖先节点相等的时候,这个节点就是lca了
public static int lca(int u,int v){
if (dep[u]<dep[v]){//保证u的深度更大
int temp=u;
u=v;
v=temp;
}
//使u和v的深度相同
while(dep[u]>dep[v]){
int temp=(int)(Math.log(dep[u]-dep[v])/Math.log(2));
u=fa[u][temp];
}
if (u==v) return u;//如果u和v的深度相同,说明v就是u的祖先,直接返回
//从高位向低位进行遍历,找到LCA;
int temp=(int)(Math.log(dep[u])/Math.log(2));
for (int i = temp; i >=0; i--) {
if (fa[u][i]!=fa[v][i]){
u=fa[u][i];
v=fa[v][i];
}
}
return fa[u][0];//返回u的父节点
}
完整代码:
下面给大家提供的是模版代码,非常的简洁,只保留了关键代码部分,没有加入快读和快输,在洛谷中能拿七十分
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
public class LCA {
static List<Integer>[] g;//邻接表
static int[] dep;
static int[][] fa;
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n= sc.nextInt();
int m= sc.nextInt();
int root= sc.nextInt();
g= new ArrayList[n+1];
dep=new int[n+1];
fa=new int[n+1][20];
dep[0]=-1;
Arrays.setAll(g,i->new ArrayList<>());
for (int i = 0; i < n - 1; i++) {//邻接表填充
int x= sc.nextInt();
int y= sc.nextInt();
g[x].add(y);
g[y].add(x);
}
dfs(root,0);
for (int i = 0; i < m; i++) {
int u= sc.nextInt();
int v=sc.nextInt();
System.out.println(lca(u,v));
}
}
public static void dfs(int u,int father){//倍增算法预处理
dep[u]=dep[father]+1;//当前节点深度等于父节点深度+1
fa[u][0]=father;//2^0=1
//用来求出上限,也就是求这个深度二进制表示中最左边的1是多大
int temp=(int)(Math.log(dep[u])/Math.log(2));
for (int i = 1; i <=temp ; i++) {
fa[u][i]=fa[fa[u][i-1]][i-1];//核心代码
}
//继续递归它的孩子
for (int v:g[u]) {
if (v!=father) dfs(v,u);
}
}
public static int lca(int u,int v){
if (dep[u]<dep[v]){//保证u的深度更大
int temp=u;
u=v;
v=temp;
}
//使u和v的深度相同
while(dep[u]>dep[v]){
int temp=(int)(Math.log(dep[u]-dep[v])/Math.log(2));
u=fa[u][temp];
}
if (u==v) return u;//如果u和v的深度相同,说明v就是u的祖先,直接返回
//从高位向低位进行遍历,找到LCA;
int temp=(int)(Math.log(dep[u])/Math.log(2));
for (int i = temp; i >=0; i--) {
if (fa[u][i]!=fa[v][i]){
u=fa[u][i];
v=fa[v][i];
}
}
return fa[u][0];//返回u的父节点
}
}