最近公共祖先(LCA):离线&在线算法

问题:求两个结点的最近公共祖先(即在树中的公共祖先中高度最低的祖先),下面介绍两种适用于不同场景的算法。

 

Hiho15:离线Tarjan算法

基本思想

Tarjan算法适用于离线批量处理多个查询请求。基本思想是以深度优先搜索的顺序访问这颗树,给这棵树的结点染色,一开始所有结点都是白色的,而当第一次经过某个结点的时候,将它染成灰色,而当第二次经过这个结点的时候——也就是离开这棵子树的时候,将它染成黑色。

最近公共祖先(LCA):离线&在线算法_第1张图片

这样做的意义,举例说明,当我们深度优先搜索到A结点时,我们发现A结点和B结点是我们需要处理的一组询问。此时树结点的染色情况如下图:

最近公共祖先(LCA):离线&在线算法_第2张图片

发现B结点的颜色为灰色,那么最近公共祖先必然就是B结点(灰色代表第一次进入该结点的子树,A结点在B的子树里);

对于此时A和P的一组询问,发现P结点颜色为白色,则留待访问到P结点时处理;

还有A和C的一组询问,发现C为黑色,则LCA为C结点向上的第一个灰色结点。

  • 总结:我先计算每个结点涉及到的询问,然后在深度优先搜索的过程中对结点染色,如果发现当前访问的结点是涉及到某个询问,那么我就看这个询问中另一个结点的颜色,如果是白色,则留待之后处理,如果是灰色,那么最近公共祖先必然就是这个灰色结点,如果是黑色,那么最近公共祖先就是这个黑色结点向上的第一个灰色结点。

利用并查集查找黑色结点的最近灰色结点

还有一个问题就是如何快速找到黑色结点向上的第一个灰色结点:使用并查集维护。

想一下,当我们到达一个结点C时,该节点为灰色,进入该结点的子树回到该节点时,子树结点已经全部变黑,而这些黑色结点向上的第一个灰色结点就是当前结点C.

 最近公共祖先(LCA):离线&在线算法_第3张图片

而此节点离开时变黑,他自己向上的第一个灰色结点就是它的父节点D,那么这一过程,其实就是将C结点代表的集合合并到了D结点代表的集合中去了,如下图:

 最近公共祖先(LCA):离线&在线算法_第4张图片

  • 总结:每个结点最开始都是一个独立的集合,每当一个结点由灰转黑的时候,就将它所在的集合合并到其父亲结点所在的集合中去。这样无论什么时候,任意一个黑色结点所在集合的代表元素就是这个结点向上的第一个灰色结点!
  • 说明:用req数组表示每个结点的代表元素,对于灰色结点o的特点就是req[o]==o

所以查找的过程可以写成如下:

int Tree::find(int no) {
   if (no == req[no])
      return no;
   else{
      req[no] = find(req[no]);
      return req[no];
   }
}


实现该算法来解决hiho15题目时,为了快速找到树中某一结点做了映射,由string映射到树结点在数组中的索引值,这样就可以方便使用并查集。

 

Hiho17:在线算法

基本思想

就是每次询问都要直接给出结果,这里有一种在线算法,基本思想是从树的根结点深度优先搜索,每次经过某一个点——无论是从它的父亲节点进入这个点,还是每次从它的儿子节点返回这个点,都按顺序记录下来。这样就把一棵树转换成了一个数组,而找到树上两个节点的最近公共祖先,无非就是找到这两个节点最后一次出现在数组中的位置所囊括的一段区间中深度最小的那个点,转成数组后使用范围最小值算法(RMQ)即可搞定。

将树转为数组举例(图来自百度图片):

最近公共祖先(LCA):离线&在线算法_第5张图片

数组:5 2 8 6 2 7 2 1 3 1 4 1

注:这里我稍作调整,即非叶子结点的等每次访问完一个子节点后再记录,而叶子节点记录一次即可。

 

代码附录在我的github账号的leetcode仓库,如有需要请前往查看。


你可能感兴趣的:(Online,Judge,algorithm)