前回のつづき。

今回は解説もしてみた。

環境は sbcl で、quicklispがインストール済、かつ lisp -builder- sdl が動く状態になっていること(これがちょっと面倒なんだけど…)。

そもそもhashlifeってなに

Gosperにより考案されたアルゴリズムで、フィールドを一つ一つの升目ではなく4分木として扱うのが特徴。

基本データ構造であるnodeは 2^n x 2^n の正方形のフィールドを表す。以下nをレベルと呼ぶ。各ノードは5つのフィールド: nw、ne、sw、se、RESULT を持つ。nw〜se は自身の1/4の大きさの子ノードで、例えば今考えているノードが 2^4 x 2^4 の大きさ(level 4)であれば子ノードは全て 2^3 x 2^3 の大きさ(level 3)である。RESULTも子ノードと同じ大きさのノードなのだが、後述。

+---------------+ +-------+-------+ | | | | | | | | | | | | | | | | | | nw | ne | +-------+ | | | | | | | | | | | | | | | n n | | | | | | node = | 2 x 2 | = +-------+-------+ | RESULT| | | | | | | | | | | | | | | | | | | | | | | | | sw | se | +-------+ | | | | | | | | | | | | | | | +---------------+ +-------+-------+

規則的なパターンを扱うのであれば必要なノード数は非常に少なくなる。

例えば1024x1024の空のフィールドなら、1x1の空のノード(level 0)、それを4つ集めた2x2のノード(level 1)、それを4つ集めた4x4のノード(level 2)、...、それを4つ集めた2^10x2^10のノード(level 10)と、たった11個のノードを保持するだけで済んでしまう。この倍の大きさのフィールドを考えるにはもう1個ノードを使うだけでよい。こういうことをするためにはもう使われているノードとまだ使われてなくて新しく作る必要のあるノードを効率的に区別する必要があるが、これはハッシュテーブルを使うことで解決出来る(4つの子ノードをキーにする)。

"次"の世代を計算するにはどうするかだが、各ノードの情報だけでは自分自身が次どういうノードになるかわからない(端があるので)。そこで、2^n x 2^n のうち真ん中の 2^(n-1) x 2^(n-1) だけを計算する。

具体的には、まず自分の子ノードの子ノード(孫ノード)を組み合わせて、自分よりレベルが1つ下のノードを9つ作る。nw、se、sw、se はすでにあるので、こいつらの子を組み合わせる事で東西南北と真ん中(図の e、w、s、n、c) を作る。この9つが互いに重なり合っているのに注意。

(重なり合った9ノード) +-------+-------+ +---+---+---+---+ | | | | | | | | | | | | | | | | | | | | | | | | | | | +---+---+---+---+ nw n se | | | 分割 | | | | | | | | ・ | | | | | | | | 組む | | | | | +-------+-------+ → +---+---+---+---+ : w c e | | | | | | | | | | | | | | | | | | | | | | | | | | | +---+---+---+---+ sw s se | | | | | | | | | | | | | | | | | | | | | | | | +-------+-------+ +---+---+---+---+

で、この9つについて、再帰的にそれぞれの"次の真ん中"が計算出来たとしよう。すると、うまい具合にちょうど重なりのない9つのノードになる。こいつらを組み合わせると重なり合った4つのノードになる。

(重なり合った9ノード) +---+---+---+---+ | | | | | (重ならない9ノード) (重なりあった4ノード) | | | | | +---+---+---+ +---+---+---+ | | | | | | | | | | | | | +---+---+---+---+ | | | | | | | | | | | | | | | | | | | | | | | | | | +---+---+---+ +---+---+---+ nw ne | | | | | step | | | | 組む | | | | +---+---+---+---+ → | | | | → | | | | : | | | | | | | | | | | | | | | | | | +---+---+---+ +---+---+---+ sw se | | | | | | | | | | | | | +---+---+---+---+ | | | | | | | | | | | | | | | | | | | | | | | | | | +---+---+---+ +---+---+---+ | | | | | +---+---+---+---+

で、この重なってる4ノードについて今と同じようにそれぞれの"次の真ん中"を計算して組み合わせれば、欲しかったノードが手に入る。

(重なり合った4ノード) +---+---+---+ | | | | (重ならない4ノード) | | | | +---+---+ +-------+ | | | | | | | | | +---+---+---+ | | | | | | | | | step | | | 組む | | | | | | → +---+---+ → | | : 目標ノード | | | | | | | | | +---+---+---+ | | | | | | | | | | | | | | | | | | +---+---+ +-------+ | | | | +---+---+---+

このやりかたの利点としてはノードのレベルによらず同じ手続きが使える事だ(一番下の数レベルを除けば)。そして、今の手続きに自分より1個下のレベルの"次"を求める作業が2回入っているのに注目してほしい。ノードのレベルが1個上がると、"次"というのは世代数にして倍になることがわかる。

再帰手続きの最後はどうなっているかというと、最終的にレベル2のノードで1世代先に進む:

(レベル2) (レベル1) .... OO.. step O. O.O. → .. O...

結局、レベル n のノードについて今の手続きを適用すると一度に 2^(n-2) 世代先まで計算する事になる。

この計算結果を RESULT に格納しておいて、次からは今の手続きを行わず単にこれを参照する。同じ形のフィールドが100回繰り返し出てくるような規則的なパターンを扱うとき、これによって実際には1回しか計算をしなくてよくなるわけだ。

こういうわけで、hashlifeは時間方向や空間方向に規則的なパターンについては非常に効率的に計算できる。