Location : Home > Languages > Perl > Package Title : Algorithm::Tree::NCA |
![]() |
Algorithm::Tree::NCA - 最近共通祖先(Nearest Common Ancestor)を求める
use Algorithm::Tree::NCA; my $tree = ...; my $nca = new Algorithm::Tree::NCA(-tree => $tree); my $x = $tree->get_node(...); my $y = $tree->get_node(...); my $z = $nca->nca($x,$y);
本パッケージはツリーにおけるノードの最近共通祖先(NCA; Nearest Common Ancestor)の定数時間取得を提供する。実装はハレル(Harel)によるアルゴリズムに基づいており、線形時間前処理の後、定数時間内に2つのノードの最近共通祖先を取得する。
アルゴリズムを実装するためにはツリーの各ノードにいくつかのデータを格納しておくことが必要である。
上述の全データは、ノード番号を除いて Algorithm::Tree::NCA オブジェクトの内部に配列で格納されている。
ノードに対して set メソッド 及び get メソッドmethod を必要とする。方法で実際のツリーノードに格納されていなければならない(または遅くなるが定数時間内に取得する方法もある)。最もよくあるケースではノードを表すのにハッシュを用いるのでこれらのメソッドのデフォルトの実装としている。
デフォルトの set メソッドは以下のようなものである。
sub _set_method { my($node,$value) = @_; $node->{'_nca_number'} = $value; }
デフォルトの get メソッドは以下のようなものである。
sub _get_method { my($node) = @_; return $node->{'_nca_number'}; }
ノードに対して他の表現方法を選択した場合は、 Algorithm::Tree::NCA オブジェクトを生成する際に -set 及び -get オプションをつけることで他のメソッドを指定することができる。
このセクションでは、前処理及び最近共通祖先を取得するにあたり利用しているアルゴリズムについて記述する。なぜアルゴリズムが有効なのかについては一切説明せず、アルゴリズムがどのように機能するかについてのみ説明する。アルゴリズムの説明にあたっては、ノード自身に必要な情報が格納されていることを想定する。アルゴリズムは Pascal 風に記述している。詳しい説明については参照の [1] または [2] を見よ。
height は非零の整数で、整数の右端のゼロの数を示す。LSSB(least significant set bit)は非零で最小有効ビットのみを示す数値である。つまりある数に対する LSSB と height は以下のようになる。
Number LSSB Height -------- -------- ------ 01001101 00000001 0 01001100 00000100 2
LSSB(i) < LSSB(j) であれば、かつそのときに限り height(i) < height(j) となることは重要である。このことは height(i) < height(j) の確認を LSSB(i) < LSSB(j) で置き換えられることを意味する。LSSB(i) のほうが計算が容易なのでこのようにすれば計算は高速化するだろう。
ツリーの前処理には3つの数値:ノード番号・ラン・マジックナンバーが必要である。各ランにはリーダの計算も必要である。これらの計算はツリーの2つの再帰的上昇・下降により計算される。
Procedure Preprocess(root:Node) Var x,y : Integer; (* ダミー変数 *) Begin (x,y) := Enumerate(root,nil,1); ComputeMagic(root,root,0); End;
第1フェーズではツリーを列挙し、各ノードのランを計算し、サブツリーにおけるノードに与えられた最大値を計算し、各ノードの親ノードを求める。もし親ノードが他の方法で求められていたならこの処理は冗長である。ノードのランは最大の height を持つサブツリーにおけるノードの数である。
Function Enumerate(node,parent:Node; num:Integer) : (Integer,Integer) Var run : Integer; Begin node.parent := parent; node.number := num; node.run := num; run := num; num := num + 1; Foreach child in node.children Do (num,run) := Enumerate(child,node,num); If height(run) > height(node.run) Then node.run := run; Done node.max := num; Return (num,node.run) End;
第2でフェーズでは各ランにおけるリーダを計算し(各ノードに対しランを知ることができるのでこれを行うことができる)、マジックナンバーを求める。リーダはノード番号を通じてアクセスすることができるように格納されているため、配列に格納する。
VAR Leader : Array [1..NODE_COUNT] of NodePtr;
各ランのリーダは各ノードに対し格納されているか(こちらを想定している)、 node.run == node.number を満たすノードにのみ格納されているかである。そのときはノードのリーダは Leader(node.run) を通して計算され、リーダが疎な配列として実装されていれば少ない容量しか必要としない。
ノードのマジックナンバーはビットワイズか、ルートからそのノードまでのパスにおける全てのノードのランである。
Procedure ComputeMagic(node, current_leader:Node; magic:Integer) Begin node.magic = magic | LSSB(node.run); If node.run != leader.run Then Leader(node.number) = node Else Leader(node.number) = current_leader Foreach child in node.children Do ComputeMagic(child, Leader(node.number), node.magic) Done End;
2つのノードの最近共通祖先を探索するには2分木にノードをマッし、最近共通祖先 b を求める(これは簡単)。2つのノードのマジックナンバーが共通に 1 であり、height が b より大きくなるようなビット j を探索するというビットワイズ計算を行う。
Function NCA(x,y:Node) : Node; Begin b := BinNCA(x,y); (* b = 10111000 *) m := (NOT b) AND (b - 1); (* m = 00000111 *) c := x.magic AND y.magic; u := c AND (NOT m); j := LSSB(u); x1 := Closest(x,j); y1 := Closest(y,j); If x1.number < y1.number Then Return x1 Else Return y1 End;
2分木において最近共通祖先を取得するにはノード番号の簡単ではあるが特殊な付け方を想定する。ルートのノード番号を 10000000 として、ツリーを下るにつれて(例えば)左にいけば 0 、右に行けば 1 となりようにパス番号を各ノードに付与していくのである。ルートから各ノードへ名同一のパスにはなりえないので以下の処理すればよい。
返り値は最近共通祖先であるノードのパス番号である。(今回の実装ではノード番号からランへのマッピングはランがパス番号である2分木へのマッピングである。)
Function BinNCA(x,y:Node) : Integer Var k,m,r : Integer; Begin (* Check that neither is the ancestor of the other *) If x.number <= y.number and y.number < x.max Then Return x.run If y.number <= x.number and x.number < y.max Then Return y.run (* Suppose x.run = 10110--- and y.run = 10111--- *) (* Then x.run XOR y.run = 00001---, and further: *) k := MSSB(x.run XOR y.run); (* k = 00001000 *) m := k XOR (k - 1); (* m = 00001111 *) r := (NOT m) AND x.run; (* r = 10110000 *) Return r OR k; (* result: 10111000 *) End;
最近共通祖先 z と同じラン上にあるノード n に最も近いノードを探索するには、上記の NCA 関数で提供された j が必要である。
Function Closest(n:Node; j:Integer) : Node; Begin l := LSSB(n.magic); If l = j Then Return x k := MSSB((j - 1) AND x.magic); u := ((NOT ((k - 1) OR k)) AND x.run) OR k w := Leader(u); Return w.parent; (* z = w.parent *) End;
Mats Kindahl, <matkin@acm.org>
Perl
![]() |
Updated : 2007/07/20 |