LCA问题的在线算法(很经典的一个算法)

Tarjan算法解决LCA查询要求事先知道全部查询提问,如果LCA要求即时询问即时回答,就需要用到下面介绍的在线算法。在线算法需要对任意树进行预处理,设输入树的结点个数为n,该算法的预处理时间和空间复杂度都是O(n),查询复杂度为O(1)。

本算法的基本思想是将对一棵树T做预处理,生成每个结点的额外信息,然后利用结点的额外信息将T映射到一颗完全二叉树B,最后利用完全二叉树的特性,通过二进制位运算可以在常数时间内查询出任何两个结点x和y的LCA。

*****************预处理*****************
预处理T需要生成每个结点的inlabel值,ascendant值,parent和level值,以及一张大小为O(n)的HEAD表。

计算inlabel值的方法是:对T做先序(preorder)深度优先遍历,从1开始对每一个结点编号,由于是先序遍历,每一棵子树的根结点(设为v)的编号在该子树中是最小的,如果同时记录该子树的结点个数size(v),可以得出以v为根的子树的编号范围是闭区间:[v, v+size(v)-1]。(这里同时用v表示它的编号,注意不要混淆)。将这个区间中每一个编号转换成二进制表示(二进制位数l+1=floor(logn)+1)之后,inlabel(v)就是这些二进制数中右边0最多的那个编号。显然叶结点的inlabel(v)就是v的编号。

inlabel值具有两个重要的性质:1)inlabel(v)相同的结点在T中构成一条简单路径,叫做inlabel(v)路径。2)将完全二叉树B按照中序(inorder)遍历对它的每一个结点编号,然后将T中每一个结点的inlabel值映射到B中编号相同的结点,得到一个多对一映射(n:1),但不是满射(surjective)。显然inlabel path上的点映射为B中的同一个结点,不在同一个inlabel path上的点映射为B中不同的结点。采用这样的映射方法之后,在T中,如果结点q是结点p祖先,那么在B中inlabel(q)也是inlabel(p)的祖先。为方便起见,这里认为每一个结点也是自身的祖先结点(下面的分析中也用到这一规定)。

计算ascendant值的方法是:ascendant(v)记录的是v的所有从根开始的祖先结点中inlabel值的最右边的"1"的索引下标。例如, 假设v(它的inlabel值等于0011)的祖先的inlabel值依次为:1000,0100,0011(注意v也是自身的祖先),那么ascendant(v)=1101。ascendant值的二进制位数也是l+1=floor(logn)+1。因为ascendant值需要inlabel值,因此计算ascendant需要在另一个遍历(以下实现中使用层序遍历)中完成。

计算level值(从根的level=0开始,每增加一层level加1)和parent值很简单,在遍历时附带添加就可以了。HEAD表记录的是每一个inlabel路径的head,所谓head就是inlabel路径上与root最近的那个结点,它的parent的inlabel值和它的label值不同。由于inlabel值上界值为n,因此在以下实现中设置HEAD表的大小为n+1(下标从0开始),HEAD的每一个下标等于对应的inlabel值,由于并不是每一个下标都有相应的inlabel值,因此HEAD是一个稀疏表,但是这里还是采用数组来处理。

*****************查询:LCA(x,y)*****************
查询分两种情况:
1)如果inlabel(x)等于inlabel(y),即x和y在一条inlabel路径上,那么LCA(x,y)等于x和y中level值最小的那个结点。
2) 如果inlabel(x)不等于inlabel(y),即x和y不在同一条inlabel路径上。在树T中,令z=LCA(x,y),在树B中,令b=LCA(inlabel(x),inlabel(y))。设b的二进制表示中最右边的"1"索引值为i, inlabel(z)的最右边的“1”索引值为j。

按以下步骤计算z:
a) 计算i: 设inlabel(x)和inlabel(y)中从左到右第一个不同的bit所在索引值为(1),inlabel(x)的最右边的"1"的索引值为(2),inlabel(y)的最右边的"1"的索引值为(3),那么i=max((1),(2),(3));

b) 根据i,计算j和inlabel(z): 由于z是x和y的最近公共祖先,因此在B中inlabel(z)也是inlabel(x)和inlabel(y)的祖先,而且是b的最近祖先(b也可能等于inlabel(z))。j的计算如下:ascendant(x)和ascendant(y)的二进制数从右边第i位开始一直往左扫描,找到的第一个"1"所在索引值就是j。inlabel(z)根据inlabel(x)或inlabel(y)以及j值很容易得出(参考以下实现中的方法findInlabel):左边l-j位与inlabel(x)或inlabel(y)相应位相同,然后右边添加1,最后右边补上j个"0"。

c) 根据j,计算z: 现在已经找到inlabel(z),目标是找到inlabel(z)路径上x的最近祖先x_hat以及y的最近祖先y_hat,然后根据x_hat和y_hat的level值大小按照情况1)中的方法确定z的值。下面只介绍计算x_hat的方法,计算y_hat的方法类似。如果inlabel(x)等于inlabel(z),那么x_hat就是x。否则,计算x_hat可以先计算x_hat的儿子w, w同时也要求是x的祖先(w可以等于x)。
w不能直接计算出来,需要先计算出w的二进制表示中最右边的"1"的索引值k,然后根据以下实现中的方法findInlabel找到inlabel(w),最后根据inlabel(w)得出w。w计算出来之后,可以计算出x_hat=w.parent。下面只说明两个关键步骤:k和w如何计算。

k的计算方法:k等于ascendant(x)的二进制表示中最右边j位数中最左边的"1"的索引值。这是因为w离inlabel(z)最近,而且k小于j。

根据inlabel(w)得出w: 由于x_hat是inlabel(z)路径上x的最近祖先,也就是说从x到x_hat的搜索路径上,当遇到inlabel(z)路径时,这个结点就是x_hat, 因此x_hat的儿子w不会在inlabel(z)路径上,而且又由于w的parent就是x_hat,它们的inlabel值不同,所以w就是它所在的inlabel path的head。所以可以得到w=HEAD(inlabel(w))。

实现:

import java.util.LinkedList;
 
 
/**
 * 
 * Online LCA algorithm
 * complexity: 
 * 
 * see also: B. Schieber & U. Vishkin (1988) "On finding lowest common ancestors - Simplification and parallelization"
 * 
 * Copyright (c) 2011 ljs (http://blog.csdn.net/ljsspace/)
 * Licensed under GPL (http://www.opensource.org/licenses/gpl-license.php) 
 * 
 * 
 * @author ljs
 * 2011-08-23
 *
 */
public class OnlineLCA {
	static class Node{
		int key;
		Node[] children;
		
		Node parent;
		int inlabel;
		int ascendant;
		int level;
		public Node(int key){
			this.key = key;
		}
		public String toString() {
			return String.valueOf(key) + " parent:" + 
				(this.parent==null?"":this.parent.key) + 
				" inlabel:" + inlabel + " level:" + level + 
				" ascendant:" + ascendant;
		}
	}
	//the leaders of each inlabel path
	private Node[] HEAD;
	//the bits of inlabel and ascendent
	private int l;
	private Node root;
	
	public OnlineLCA(int n,Node root){
		HEAD = new Node[n+1];
		this.root = root;
		l = (int)(Math.log(n) / Math.log(2));
	}
	//preprocess tree T
	public void preprocess(){
		this.root.level = 0;
		doInOrderTraverse(this.root);
		
		doLevelOrderTraverse();
	}
	
	//return the LCA node of x and y
	public Node query(Node x,Node y){
		Node lca = null;
		if(x.inlabel==y.inlabel){
			if(x.leveli)
				i=tmp;
			int diff = x.inlabel ^ y.inlabel;
			tmp = (int)(Math.log(diff) / Math.log(2));
			if(tmp>i)
				i=tmp;
			
			//find j and z.inlabel (z is lca of x and y)
			int ascand = x.ascendant & y.ascendant;
			int j = findLSBOne(ascand,i); 
			int inlabelz = findInlabel(x,j); 
			
			//find x-hat and y-hat
			Node x_hat = findHat(x,inlabelz,j);
			Node y_hat = findHat(y,inlabelz,j);
			if(x_hat.level>= rightStart;		
		while((num & 0x01) != 0x01){
			num >>=1;
			i++;							
		}
		return i;
	}
	//calculate ascendants and head based on inlabel numbers
	//so this method must be called after inlabels are determined.
	private void doLevelOrderTraverse(){
		LinkedList queue = new LinkedList();
		queue.add(this.root); 
		HEAD[this.root.inlabel] = this.root;
		this.root.ascendant = (1<



测试:
LCA of 30 and 40 is: 20
LCA of 30 and 100 is: 10
LCA of 70 and 100 is: 50
LCA of 80 and 90 is: 80
LCA of 70 and 80 is: 50
LCA of 100 and 90 is: 80
LCA of 50 and 100 is: 50
************************
LCA of 40 and 70 is: 30
LCA of 170 and 180 is: 150
LCA of 90 and 220 is: 10
LCA of 130 and 200 is: 130
LCA of 160 and 220 is: 150
LCA of 200 and 210 is: 190
LCA of 120 and 190 is: 10
LCA of 50 and 80 is: 30
LCA of 40 and 110 is: 10
LCA of 60 and 100 is: 30
LCA of 30 and 200 is: 10
LCA of 60 and 190 is: 10
LCA of 10 and 50 is: 10
LCA of 20 and 50 is: 20
LCA of 50 and 90 is: 30
LCA of 30 and 150 is: 10



你可能感兴趣的:(数据结构和算法)