2008-11-30
木の循環検知、こんな感じだとどうでしょうか?
|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さん
計算量の考察を全然していませんでした。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で図示してみよう。
- 41 http://www.rubyist.net/~kazu/samidare/
- 35 http://www.rubyinside.com/llvmruby-a-compiler-toolkit-available-to-rubyists-1362.html
- 28 http://practical-scheme.net/wiliki/wiliki.cgi?Shiro
- 16 http://llvmruby.org/wordpress-llvmruby/
- 14 http://shinh.skr.jp/m/
- 10 http://madscientist.jp/~ikegami/diary/20081130.html
- 7 http://practical-scheme.net/wiliki/rssmix.cgi
- 7 http://reader.livedoor.com/reader/
- 6 http://www.google.com/reader/view/
- 4 http://a.hatena.ne.jp/h_sakurai/
今ざくっと2^20ノードくらいまでやらせてみたんですが、O(N^2)よりは小さくO(NlogN)よりは大きい感じですね。
面白い問題をありがとうございます。
大規模なグラフ構造を並列処理するという話は面白そうで、利用価値も今後高まりそうですね。
いけがみさんの解説中(http://madscientist.jp/~ikegami/diary/20081130.html)に、もっといいアルゴリズムが紹介されていて、少し並行処理についても言及されています。その辺の詳細をもっと知りたいなと思いました。
が #f になってしまいますが、意図された動作でしょうか…
shiroさんの場合と同じようにeq?のノードに出会ってしまうのが原因だと思います。辿ったノードを記録しておけば解決できそうですが、それだとこのプログラムのメリットが無いので、別の対応方法を考えてみます。
(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
確かに今のうさぎに欠点はありますが、わたしはこれに可能性を感じています :)
うさぎは今のままでは使い物にならないと思いますが、なんか化けそうだと私も信じています。
でも、化けなくても既に元を取っていると思います。イプシロンが速くなったこともありますし、何よりグラフにループがあるか判定するという興味を持つ人は少ないだろうと思うアルゴリズムにみんなが欠点を探してくれたこと、そのものが最大の報酬だと思っています。
http://shinh.skr.jp/t/081202_2358.jpg
言葉で説明するのは大変そうなので…しかし絵も下手ですいません。で、 D からのパスを無駄に何度も何度も歩かせてやれ、という意図でした。
うさぎには厳しい試練であることが良くわかりました :)