最通俗易懂的LCA求解方法
转载自:最通俗易懂的LCA求解方法
最近公共祖先(Lowest Common Ancestors,LCA)指有根树中距离两个节点最近的公共祖先。祖先指从当前节点到树根路径上的所有节点。
u和v的公共祖先指一个节点既是u的祖先,又是v的祖先。u和v的最近公共祖先指距离u和v最近的公共祖先。若v是u的祖先,则u和v的最近公共祖先是v。
可以使用LCA求解树上任意两点之间的距离。求u和v之间的距离时,若u和v的最近公共祖先为lca,则u和v之间的距离为u到树根的距离加上v到树根的距离减去2倍的lca到树根的距离:dist[u]+dist[v]-2×dist[lca]。
求解LCA的方法有很多,包括暴力搜索法、树上倍增法、在线RMQ算法、离线Tarjan算法和树链剖分。
在线算法:以序列化方式一个一个地处理输入,也就是说,在开始时并不需要知道所有输入,在解决一个问题后立即输出结果。
离线算法:在开始时已知问题的所有输入数据,可以一次性回答所有问题。
& 原理1 暴力搜索法
暴力搜索法有两种:向上标记法和同步前进法。
1.向上标记法
从u向上一直到根节点,标记所有经过的节点;若v已被标记,则v节点为LCA(u, v);否则v也向上走,第1次遇到已标记的节点时,该节点为LCA(u, v)。
2.同步前进法
将u、v中较深的节点向上走到和深度较浅的节点同一深度,然后两个点一起向上走,直到走到同一个节点,该节点就是u、v的最近公共祖先,记作LCA(u,v)。若较深的节点u到达v的同一深度时,那个节点正好是v,则v节点为LCA(u, v)。
3.算法分析
以暴力搜索法求解LCA,两种方法的时间复杂度在最坏情况下均为O(n)。
& 原理2 树上倍增法
树上倍增法不仅可以解决LCA问题,还可以解决很多其他问题,掌握树上倍增法是很有必要的。
F[i, j]表示i的2^j辈祖先,即i节点向根节点走2^j步到达的节点。
u节点向上走20步,则为u的父节点x,F[u,0]=x;向上走2^1步,到达y,F[u,1]=y;向上走2^2步,到达z,F[u,2]=z;向上走2^3步,节点不存在,令F[u,3]=0。
F[i, j]表示i的2^j辈祖先,即i节点向根节点走2^j步到达的节点。可以分两个步骤:i节点先向根节点走2^j-1步得到F[i, j-1];再从F[i, j-1]节点出发向根节点走2^j-1步,得到F[F[i, j-1], j-1],该节点为F[i, j]。
递推公式:F[i, j]=F[F[i, j-1], j-1],i=1,2,…n,j=0,1,2,…k,2^k≤n,k=log2n。
1.算法设计
(1)创建ST。
(2)利用ST求解LCA。
2.完美图解
和前面暴力搜索中的同步前进法一样,先让深度大的节点y向上走到与x同一深度,然后x、y一起向上走。和暴力搜索不同的是,向上走是按照倍增思想走的,不是一步一步向上走的,因此速度较快。
问题一:怎么让深度大的节点y向上走到与x同一深度呢?
假设y的深度比x的深度大,需要y向上走到与x同一深度,k=3,则求解过程如下。
(1)y向上走2^3步,到达的节点深度比x的深度小,什么也不做。
(2)减少增量,y向上走2^2步,此时到达的节点深度比x的深度大,y上移,y=F[y][2]。
(3)减少增量,y向上走2^1步,此时到达的节点深度与x的深度相等,y上移,y=F[y][1]。
(4)减少增量,y向上走2^0步,到达的节点深度比x的深度小,什么也不做。此时x、y在同一深度。
总结:按照增量递减的方式,到达的节点深度比x的深度小时,什么也不做;到达的节点深度大于或等于x的深度时,y上移,直到增量为0,此时x、y在同一深度。
问题二:x、y一起向上走,怎么找最近的公共祖先呢?
假设x、y已到达同一深度,现在一起向上走,k=3,则其求解过程如下。
(1)x、y同时向上走2^3步,到达的节点相同,什么也不做。
(2)减少增量,x、y同时向上走2^2步,此时到达的节点不同,x、y上移,x=F[x][2],y=F[y][2]。
(3)减少增量,x、y同时向上走2^1步,此时到达的节点不同,x、y上移,x=F[x][1],y=F[y][1]。
(4)减少增量,x、y同时向上走2^0步,此时到达的节点相同,什么也不做。
此时x、y的父节点为最近公共祖先节点,即LCA(x, y)=F[x][0]。
完整的求解过程如下图所示。
总结:按照增量递减的方式,到达的节点相同时,什么也不做;到达的节点不同时,同时上移,直到增量为0。此时x、y的父节点为公共祖先节点。
3.算法实现
void ST_create(){//构造ST
for(int j=1;j<=k;j++)
for(int i=1;i<=n;i++)//i先走2^(j-1)步到达F[i][j-1],再走2^(j-1)步
F[i][j]=F[F[i][j-1]][j-1];
}
int LCA_st_query(int x,int y) {//求x、y的最近公共祖先
if(d[x]>d[y])//保证x的深度小于或等于y
swap(x,y);
for(int i=k;i>=0;i--)//y向上走到与x同一深度
if(d[F[y][i]]>=d[x])
y=F[y][i];
if(x==y)
return x;
for(int i=k;i>=0;i--)//x、y一起向上走
if(F[x][i]!=F[y][i])
x=F[x][i],y=F[y][i];
return F[x][0];//返回x的父节点
}
class TreeAncestor { static final int LOG = 16; int[][] ancestors; public TreeAncestor(int n, int[] parent) { ancestors = new int[n][LOG]; for (int i = 0; i < n; i++) { Arrays.fill(ancestors[i], -1); } for (int i = 0; i < n; i++) { ancestors[i][0] = parent[i]; } for (int j = 1; j < LOG; j++) { for (int i = 0; i < n; i++) { if (ancestors[i][j - 1] != -1) { ancestors[i][j] = ancestors[ancestors[i][j - 1]][j - 1]; } } } } // 获取第k个祖先 public int getKthAncestor(int node, int k) { for (int j = 0; j < LOG; j++) { if (((k >> j) & 1) != 0) { node = ancestors[node][j]; if (node == -1) { return -1; } } } return node; } }
4.算法分析
采用树上倍增法求解LCA,创建ST需要O(nlogn)时间,每次查询都需要O(logn)时间。一次建表、多次使用,该算法是基于倍增思想的动态规划,适用于多次查询的情况。若只有几次查询,则预处理需要O(nlogn)时间,还不如暴力搜索快。