クラスタリングによる迷路作成アルゴリズム


はじめに

 クラスタリングアルゴリズムにより、解くと絵が浮かび上がる 迷路を作成する方法を紹介する。

注意:ここで言う「クラスタリング」は同値関係をつなぐもので、 データ解析で良くでてくるクラスタリング、つまり情報空間上に散らばっているデータ点を 適当に似ているもの(近いもの)同士で分類する手法ではありません。


クラスタリングとは

 ウェブのリンク情報や、mixiの友人関係など、ネットワークの性質を 知りたいことがよくある。このとき、ネットワークの性質として

などの情報が欲しくなる。このような解析をするときに 必要となるのがクラスタリングである。

 クラスタリングとは、同値関係のリストが与えられたときにグループ分けを することである。たとえば、

 友達の友達は友達である
と定義すると、友人関係は同値関係を作る。 その上で、 という情報が分かっていると、 この情報から、

という関係がわかる。このように、 何かと何かがつながっているという情報を処理して、 全体をグループわけする作業がクラスタリングである。 物理面での応用としては、浸透問題などの臨界現象や、 粉体のForce Networkなどの解析にも用いられる。


アルゴリズム

 クラスタリングアルゴリズムはいくつか知られているが、 そのうち一次元配列を使った簡単な例を紹介する。

 この作業はリンクリストの作成である。 下図では、6つの要素からなるリンクリストが、 クラスタリングにより2つにグループ分けされた様子を示す。 最初、すべての要素のリンクは自分を指しているが、 クラスタリングにより、必ず番号の低い側(図では左側)にリンクされる。 このとき、自分からたどっていったリンクの終点の要素の番号が 自分のクラスタ番号となる。

 以上の作業をコードで書くと以下のようになる。 pair1[i]とpair2[i]に同値関係が入っているとして、 関数clusteringを呼んでやればクラスタリングができる。 あとは int get_cluster_numberに要素番号を入れれば その要素のクラスタ番号が返ってくるので、あとは 最大クラスタを探すなり、クラスタの数を数えるなりすることができる。

//---------------------------------------------------------------------------
int cluster[N];
int pair1[N_PAIR],pair1[N_PAIR];
//---------------------------------------------------------------------------
int
get_cluster_number(int index){
  int i = index;
  while(i != cluster[i]){
    i = cluster[i];
  }
  return i;
}
//---------------------------------------------------------------------------
void
clustering(void){
  for(int i=0;i < N;i++){
    cluster[i] = i;
  }
  for(int i=0;i < N_PAIR;i++){
    int i1 = pair1[i];
    int i2 = pair2[i];
    i1 = get_cluster_number(i1);
    i2 = get_cluster_number(i2);
    if(i1 < i2){
        cluster[i2] = i1;
    }else{
      cluster[i1] = i2;
    }
  }
}
//---------------------------------------------------------------------------


迷路作成アルゴリズム

 では、クラスタリングを迷路作成に応用してみよう。 迷路を作成するには、基本的には枝をつけるだけでよい。 しかし、適当に枝を伸ばすと、 「入り口からいけない場所(死に領域)ができる」 「ループができてしまい、解答が一意でなくなる」 などの問題ができてしまう。 そこで、クラスタリングを使って解答のパスが一意で、 なおかつ死に領域ができないことを保証する。

 迷路の作成アルゴリズムは以下の通り。

以上で死に領域とループがない迷路の完成である。


迷路作成例

 以上のアルゴリズムを用いて、「解くと絵が浮かび上がる迷路」を 作ることができる。やり方は簡単で、先に解答のパスを作り、その部分だけ クラスタリングしてから壁を壊し始めれば、ループを作らない保証から 与えた解答が最短距離となる迷路が出来上がる。 以下の図はそのようにして作成した迷路の例である。 左図の迷路を解くと、右図のような絵が浮かび上がる。

 上記の迷路は、クラスタリングアルゴリズムによる迷路作成エンジンを実装したソフト 「迷次郎」により作成した。


おわりに

 クラスタリングアルゴリズムを応用し、 「解くと絵が浮かび上がる迷路」を 作成してみた。クラスタリングはあらゆるネットワークの解析に使えるし、 コードも20行もないので知っていて損はないアルゴリズムである。

追記

 誤解している人がいるようなのでいくつか追記。 クラスタリングアルゴリズムというのは僕が考えたものではなく、かなり古いアルゴリズム。 実装方法はいくつかあるが、上記ではそのうちもっとも簡単なものの一つを紹介してある。 また、この記事の主眼は「迷路作成」ではなく、あくまでもクラスタリングアルゴリズムの紹介。


Rubyによるサンプルコード

 上記の説明だけじゃアルゴリズムが分かりにくいようなので、 某所用に書いたRubyによる手抜きサンプルを置いておく。 実行すると、以下のようなA4一枚の大きさの迷路をeps形式で吐き出す。

 以下はソース。単に実行すれば、同じディレクトリにmaze.epsを作成する。

maze.rb
  1. # maze.rb
  2. # Author: Hiroshi Watanabe
  3. class Maze
  4. def initialize(s)
  5. @lx = s
  6. @ly = @lx*297/210
  7. @bond_h = Array.new((@lx+1)*@ly) { false}
  8. @bond_v = Array.new(@lx*(@ly+1)) { false}
  9. @point = Array.new(@lx*@ly*2) {|i| i}
  10. @GridSize = (600.0-30)/(@lx+4)
  11. @LeftMargin = @GridSize*3
  12. @TopMargin = @GridSize*2
  13. makeMaze
  14. end
  15. def getClusterIndex(x, y)
  16. index = @lx*y+x
  17. while(index != @point[index])
  18. index = @point[index]
  19. end
  20. return index
  21. end
  22. def connect(ix1, iy1, ix2, iy2)
  23. i1 = getClusterIndex(ix1,iy1)
  24. i2 = getClusterIndex(ix2,iy2)
  25. if i1<i2
  26. @point[i2] = i1
  27. else
  28. @point[i1] = i2
  29. end
  30. end
  31. def makeMazeSub
  32. rate = 0.8
  33. for ix in 0..@lx-2
  34. for iy in 0..@ly-1
  35. next if rand <rate
  36. next if getClusterIndex(ix,iy) == getClusterIndex(ix+1,iy)
  37. @bond_h[(@lx+1)*iy+ix+1] = true
  38. connect(ix,iy,ix+1,iy)
  39. end
  40. end
  41. for ix in 0..@lx-1
  42. for iy in 0..@ly-2
  43. next if rand <rate
  44. next if getClusterIndex(ix,iy) == getClusterIndex(ix,iy+1)
  45. @bond_v[(iy+1)*@lx+ix] = true
  46. connect(ix,iy,ix,iy+1)
  47. end
  48. end
  49. end
  50. def makeMazeFinal
  51. for ix in 1..@lx-2
  52. for iy in 1..@ly-1
  53. next if getClusterIndex(ix,iy) == getClusterIndex(ix+1,iy)
  54. @bond_h[iy*(@lx+1)+ix+1] = true
  55. connect(ix,iy,ix+1,iy)
  56. end
  57. end
  58. end
  59. def makeMaze
  60. for i in 0..10
  61. makeMazeSub
  62. end
  63. makeMazeFinal
  64. @bond_h[0] = true
  65. @bond_h[(@lx+1)*@ly-1] = true
  66. end
  67. def exportEPS (filename)
  68. f = open(filename,"w")
  69. f.print "%!PS-Adobe-2.0\n"
  70. f.print "%%BoundingBox: 0 0 600 850\n"
  71. f.print "%%EndComments\n"
  72. f.print "/mydict 120 dict def\n"
  73. f.print "mydict begin\n"
  74. f.print "gsave\n"
  75. g = @GridSize
  76. h = @GridSize*0.5
  77. f.print "/arrow {gsave translate "
  78. f.print "0 0 moveto "
  79. f.print g.to_s + " 0 lineto stroke\n"
  80. f.print g.to_s + " 0 moveto "
  81. f.print h.to_s + " -" + h.to_s + " lineto "
  82. f.print h.to_s + " " + h.to_s + " lineto "
  83. f.print "closepath fill stroke grestore} def\n"
  84. f.print @LeftMargin.to_s + " " + @TopMargin.to_s + " translate \n"
  85. f.print "-" + g.to_s + " " + (g*0.5).to_s + " arrow \n"
  86. f.print "" + (g*@lx).to_s + " " + (g*(@ly-0.5)).to_s + " arrow \n"
  87. for ix in 0..@lx
  88. for iy in 0..@ly-1
  89. x = ix * @GridSize
  90. y = iy * @GridSize
  91. next if @bond_h[iy*(@lx+1)+ix]
  92. f.print x.to_s + " " + y.to_s + " moveto "
  93. f.print x.to_s + " " + (y+@GridSize).to_s + " lineto stroke\n"
  94. end
  95. end
  96. for ix in 0..@lx-1
  97. for iy in 0..@ly
  98. x = ix * @GridSize
  99. y = iy * @GridSize
  100. next if @bond_v[iy*@lx+ix]
  101. f.print x.to_s + " " + y.to_s + " moveto "
  102. f.print ""+(x+@GridSize).to_s + " " + y.to_s + " lineto stroke\n"
  103. end
  104. end
  105. f.print "grestore\n"
  106. f.print "end\n"
  107. end
  108. end
  109. s = 50
  110. if(ARGV.size > 0)
  111. s = ARGV[0].to_i
  112. end
  113. m = Maze.new(s).exportEPS("maze.eps")

Author: Hiroshi Watanabe

トップページへ戻る