Hatena::ブログ(Diary)

miura1729の日記 このページをアンテナに追加 RSSフィード

2008-11-30

木の循環検知、こんな感じだとどうでしょうか?

17:32 |  木の循環検知、こんな感じだとどうでしょうか?を含むブックマーク  木の循環検知、こんな感じだとどうでしょうか?のブックマークコメント

shiroさんのページより http://practical-scheme.net/wiliki/wiliki.cgi?Shiro

最後の "Best solution" にあげられてる「うさぎと亀」はあらゆるLisp/Scheme処理系で使われていると思う。list?は循環リストについても停止しないとならないため。 (CLのlistpは最初のペアしか見ないのでちょっと違うが、 lengthなどは循環リストを検出しないとだめ)。

難しいのはcar方向にも循環する場合、つまり木の場合で、これはvisitした全ノードを何らかの方法で区別する (マークをつける、ハッシュテーブルに記録する、みつけたものから移動してゆく、など) しかないと思うのだけれど、もっと効率の良い方法はあるのだろうか。

木の場合も結局、「何とか優先」で順番に辿るわけなんで、うさぎを作っちゃうという考え方です。

(define (no-cycle?-aux tr tr2 fst)
  (if (and (not fst) (eq? tr tr2))
      #f
      (if (pair? tr2)
	  (and
	   (if (pair? (car tr2))
	       (and
		(no-cycle?-aux (car tr) (car (car tr2)) #f)
		(no-cycle?-aux (car tr) (cdr (car tr2)) #f))
	       #t)
	   (if (pair? (cdr tr2))
	       (and
		(no-cycle?-aux (cdr tr) (car (cdr tr2)) #f)
		(no-cycle?-aux (cdr tr) (cdr (cdr tr2)) #f))
	       #t))
	  #t)))

(define (no-cycle? tr)
  (no-cycle?-aux tr tr #t))

(print (no-cycle? '(a b x)))
(print (no-cycle? '((a b) x c)))

(define n '((a b) x c))
(set-cdr! (cdr (car n)) (car n))
(print (no-cycle? n))

(define m '(a (a b) x c))
(set-car! m m)
(print (no-cycle? n))

結果です

gosh ttra.scm
#t
#t
#f
#f

追記

いろいろ意見を頂いてありがたいです。ありがとうございます。

shiroさん

ところが、このアルゴリズムは全ノードについて探索が2分岐するから時間計算量はO(2^N)になる。

計算量の考察を全然していませんでした。O(2^N)じゃあダメですね。枝刈りして計算量が減らせるような気がするのでもう少し考えてみます。ビールを飲んだ後なのでそのまま寝ちゃうかもしれませんが・・・。

fujita-yさん

ろくなチェックしていないのがばればれですね。とりあえず改正版を作ってみました。

(define (no-cycle?-aux tr tr2 fst)
  (if (and (not fst) (eq? tr tr2) (not (equal? tr '())))
      #f
      (if (and (pair? tr2) (pair? tr))
	  (and
	   (if (pair? (car tr2))
	       (and
		(no-cycle?-aux (car tr) (car (car tr2)) #f)
		(no-cycle?-aux (car tr) (cdr (car tr2)) #f))
	       #t)
	   (if (pair? (cdr tr2))
	       (and
		(no-cycle?-aux (cdr tr) (car (cdr tr2)) #f)
		(no-cycle?-aux (cdr tr) (cdr (cdr tr2)) #f))
	       #t))
	  #t)))

追記2

O(2^N)の件、うさぎさんは一杯子供を産むけどどれも違うノードを辿るのでO(2^N)にならないような気がしました。私の力では数学的に証明できないので、大きなデータを作って確かめてみました。確かめていたらバグを見つけたので、改定します。

これで計算量が増えましたが、それでもO(2^N)じゃ無いような気がします。

(print (no-cycle? '(#1=(a (b (c d) e) (f (g h) (i (j (k l) m)))
			  (n (o p) (q r)) s (t (u v) (w x (y z #1#)))))))

葉のサイズが26で、関数呼び出し回数が326だったので、多く見積もってO(N^2)位だと思います。

(define cnt 0)
(define (no-cycle?-aux tr tr2 fst)
  (set! cnt (+ cnt 1))
  (if (and (not fst) (eq? tr tr2) (not (null? tr)))
      #f
      (if (and (pair? tr2) (pair? tr))
	  (and
	   (if (pair? (car tr2))
	       (and
		(no-cycle?-aux (car tr) (car (car tr2)) #f)
		(no-cycle?-aux (cdr tr) (car (car tr2)) #f)
		(no-cycle?-aux (car tr) (cdr (car tr2)) #f)
		(no-cycle?-aux (cdr tr) (cdr (car tr2)) #f))
	       #t)
	   (if (pair? (cdr tr2))
	       (and
		(no-cycle?-aux (car tr) (car (cdr tr2)) #f)
		(no-cycle?-aux (cdr tr) (car (cdr tr2)) #f)
		(no-cycle?-aux (car tr) (cdr (cdr tr2)) #f)
		(no-cycle?-aux (cdr tr) (cdr (cdr tr2)) #f))
	       #t))
	  #t)))

(define (no-cycle? tr)
  (no-cycle?-aux tr tr #t))

(print (no-cycle? '(a b x)))
(print (no-cycle? '((a b) x c)))

(define n '((a b) x c))
(set-cdr! (cdr (car n)) (car n))
(print (no-cycle? n))

(define m '(a (a b) x c))
(set-car! m m)
(print (no-cycle? n))

(print (no-cycle? '(1 (2 (3 4) ()) ())))
(print (no-cycle? '((a . #1=(c . d)) #1#)))
(print (no-cycle? '(#1=(a (b (c d) e) (f (g h) (i (j (k l) m)))
			  (n (o p) (q r)) s (t (u v) (w x (y z #1#)))))))
(print (no-cycle? '(#1=(a (b (c d) e) (f (g h) (i (j (k l) m)))
			  (n (o p) (q r)) s (t (u v) (w x (y z)))))))
(print cnt)

出力結果です


gosh ttra.scm
#t
#t
#f
#f
#t
#t
#f
#t
665

追記3

shinhさんの「木ぽいののループ検出」(http://shinh.skr.jp/m/?date=20081202#p01) より

N^2 のサイズの木?に対して指数ぽい挙動になるような。

私も試してみました。

3
34
45
97
120
218
259
455
530
920
1061
1837
2108
3654
4183
7267
8310
14468
16537
28841
32960
57554
65771
114943
131354
229680
262477
459109
524676
917918

ほんどだー、指数ぽい。でも、この関数で生成される木ってどんなだろう?生成関数を追ってみたのですが、頭がこんがらがってきました。後で、graphvizで図示してみよう。

fujita-yfujita-y 2008/11/30 19:02 'carうさぎ'と'cdrうさぎ'面白いですね :) でも、このままだと (no-cycle? '(1 (2 (3 4) ()) ())) => #f となってしまうようです・・・

shiroshiro 2008/11/30 21:00 あっそうか。私もノード数を増やしながら呼び出し回数を数えて判断してたんですが、ノード数を試行ごとに倍々に増やしていたのに出力を試行回数で表示してたものでexponentialと勘違いしました。
今ざくっと2^20ノードくらいまでやらせてみたんですが、O(N^2)よりは小さくO(NlogN)よりは大きい感じですね。

miura1729miura1729 2008/11/30 21:11 いい感じで落ち着きそうなので喜んでいます。
面白い問題をありがとうございます。

fujita-yfujita-y 2008/11/30 22:05 これは子うさぎを並列動作させても無駄なメモリがほとんど出ないところが素敵ですね!このタイプはみたことないですよ@@

miura1729miura1729 2008/12/01 06:50 並列動作で試してくださってありがとうございます。私も試してみたいのですが、マルチコアじゃ無いので試せないです。
大規模なグラフ構造を並列処理するという話は面白そうで、利用価値も今後高まりそうですね。
いけがみさんの解説中(http://madscientist.jp/~ikegami/diary/20081130.html)に、もっといいアルゴリズムが紹介されていて、少し並行処理についても言及されています。その辺の詳細をもっと知りたいなと思いました。

k.inabak.inaba 2008/12/01 14:18 (print (no-cycle? '((#1=(a)) . #1#)))
が #f になってしまいますが、意図された動作でしょうか…

miura1729miura1729 2008/12/01 17:27 バグです。試してくださって、さらにバグレポートまでありがとうございます。
shiroさんの場合と同じようにeq?のノードに出会ってしまうのが原因だと思います。辿ったノードを記録しておけば解決できそうですが、それだとこのプログラムのメリットが無いので、別の対応方法を考えてみます。

fujita-yfujita-y 2008/12/02 15:54 shinhさんのデータは循環なしですが共有があり、また共有を考慮せずにペアの数を数え上げると(gen-tree 1) => 5, (gen-tree 2) => 47, (gen-tree 3) => 499 となるようです。そこで計算しやすいように循環も共有も持たないシンプルなO(N^2)くらいのデータを考えて試してみることにしました。

(define n-list (lambda (e n) (if (= n 0) e (n-list (list e) (- n 1)))))とすれば、
(map (lambda (n) (n-list (cons n (- n)) 3)) (iota 1000))
;; pair 5000 call 49861 user 0.0099
(map (lambda (n) (n-list (cons n (- n)) 8)) (iota 1000))
;; pair 10000 call 198841 user 0.0392
(map (lambda (n) (n-list (cons n (- n)) 13)) (iota 1000))
;; pair 15000 call 446081 user 0.0811
(map (lambda (n) (n-list (cons n (- n)) 18)) (iota 1000))
;; pair 20000 call 790681 user 0.1352

つまりCAR方向に深いとうさぎが困るわけで、たとえば同じ5000のペアでも深さを増やしてやるとこうなります。
(map (lambda (n) (n-list (cons n (- n)) 48)) (iota 100))
;; pair 5000 call 354201 user 0.0616

一方でCDR方向に長いデータの場合、例えばalistやSchemeのコードなどを食べさせるとかなり高速なのではないかと思います。実はイプシロンに内蔵していたノード記録タイプとschemeのコードを食べさせて競争しましたところ・・・いくつかのデータでイプシロンがぼろぼろに負けて悔しかったのでイプシロンに改良を加えたほどです :p
確かに今のうさぎに欠点はありますが、わたしはこれに可能性を感じています :)

miura1729miura1729 2008/12/02 18:09 うれしいコメントありがとうございます。
うさぎは今のままでは使い物にならないと思いますが、なんか化けそうだと私も信じています。
でも、化けなくても既に元を取っていると思います。イプシロンが速くなったこともありますし、何よりグラフにループがあるか判定するという興味を持つ人は少ないだろうと思うアルゴリズムにみんなが欠点を探してくれたこと、そのものが最大の報酬だと思っています。

shinichiro_hshinichiro_h 2008/12/03 00:01 年に一度くらいしか scheme 触らないので自信はないのですけど、下記のようなグラフを作ってるつもりです。

http://shinh.skr.jp/t/081202_2358.jpg

言葉で説明するのは大変そうなので…しかし絵も下手ですいません。で、 D からのパスを無駄に何度も何度も歩かせてやれ、という意図でした。

miura1729miura1729 2008/12/03 07:17 あまりにシンプルで美しい構造でびっくりしました。ありがとうございます。
うさぎには厳しい試練であることが良くわかりました :)

トラックバック - http://d.hatena.ne.jp/miura1729/20081130/1228033970