M.Hiroi's Home Page

xyzzy Lisp Programming

Common Lisp 入門

[ PrevPage | xyzzy Lisp | NextPage ]

簡易エキスパートシステムの作成 (4)

●実行例 -- 家系図

それでは、プログラムが完成したので実際に動かしてみましょう。具体的な例題として、次のような家系図を考えてみます。

この家系図は、次のような事実で表すことができます。

List 1 : 家系図の定義

((男性 太郎))            ; 太郎は男性である。
((男性 一郎))            ; 一郎は男性である。
((男性 次郎))            ; 次郎は男性である。
((男性 三郎))            ; 三郎は男性である。
((女性 花子))            ; 花子は女性である。
((女性 友子))            ; 友子は女性である。
((女性 幸子))            ; 幸子は女性である。
((女性 洋子))            ; 洋子は女性である。
((父親 太郎 一郎))       ; 太郎は一郎の父親である。
((父親 太郎 次郎))       ; 太郎は次郎の父親である。
((父親 太郎 友子))       ; 太郎は友子の父親である。
((母親 花子 一郎))       ; 花子は一郎の母親である。
((母親 花子 次郎))       ; 花子は次郎の母親である。
((母親 花子 友子))       ; 花子は友子の母親である。
((父親 一郎 三郎))       ; 一郎は三郎の父親である。
((父親 一郎 洋子))       ; 一郎は洋子の父親である。
((母親 幸子 三郎))       ; 幸子は三郎の母親である。
((母親 幸子 洋子))       ; 幸子は洋子の母親である。
; 規則の定義
((両親 ?x ?y) (父親 ?x ?y))
((両親 ?x ?y) (母親 ?x ?y))
((息子 ?x ?y) (両親 ?y ?x) (男性 ?x))
((娘 ?x ?y)   (両親 ?y ?x) (女性 ?x))
((祖父 ?x ?y) (両親 ?z ?y) (父親 ?x ?z))

性別の定義は簡単ですね。男性、女性という述語を使っています。父親と母親ですが、(父親 ?x ?y) は「?x は ?y の父親」という関係を表していることにします。

それでは、定義した事実を使って規則を作ってみましょう。まず「両親」という規則を定義します。両親は、父親か母親という事実を満たせばいいですね。このような場合、次のように定義します。

List 2 : 両親の定義 (?x は ?y の両親である)

((両親 ?x ?y) (父親 ?x ?y))
((両親 ?x ?y) (母親 ?x ?y))

「両親は父親である」と「両親は母親である」という規則を定義しただけですが、最初の節が失敗したら次の節が選択されるので、「両親は父親または母親である」という条件を満たしています。

ほかのプログラミング言語では、「~または~」という条件は OR を使って表現するのが一般的です。ところが、今回のようなエキスパートシステムでは、Prolog の場合も同様ですが、複数の規則を定義するだけで OR を実現することができるのです。それでは実行してみましょう。一郎の両親は誰か質問してみます。

(Q '(両親 ?x 一郎))
?x = 花子

?x = 太郎

nil

正解は太郎と花子です。うまく動作していますね。それでは、この規則を使って「息子」という規則を定義しましょう。?x が ?y の息子であるならば、?y は ?x の両親でかつ ?x は男性のはずです。したがって、規則は次のようになります。

List 3 : 息子の定義 (?x は ?y の息子である)

((息子 ?x ?y) (両親 ?y ?x) (男性 ?x))

「かつ」は規則でゴールを順番に並べれば実現できましたね。まず (両親 ?y ?x) を満たす関係を求めます。そのあと、(男性 ?x) で ?x が男性であることを確かめます。(男性 ?x) で失敗しても、(両親 ?y ?x) に戻って次の候補を見つけてくれるので大丈夫です。それでは実行してみましょう。

(Q '(息子 ?x 花子))
?x = 次郎

?x = 一郎

nil

花子の息子は一郎と次郎である、と答えが出ました。この節で (男性 ?x) を (女性 ?x) に変更すると「娘」の関係を表すことができます。

List 4 : 娘の定義 (?x は ?y の娘である)

((娘 ?x ?y) (両親 ?y ?x) (女性 ?x))

最後に「祖父」の関係を求める規則を定義しましょう。これは今までと違ってちょっと面倒です。?x の祖父 ?y を求めるには、まず ?x の両親を求め、さらにその父親を求めます。母方と父方に祖父がいますから、父親の父親を求めるのでは母方の祖父がわかりません。そこで、?x の両親を求めるときに ?x と ?y 以外の新しい変数を用意して、?x の両親をその変数とマッチングさせることにします。

List 5 : 祖父の定義 (?x は ?y の祖父である)

((祖父 ?x ?y) (両親 ?z ?y) (父親 ?x ?z))

(両親 ?z ?y) で変数 ?z に ?y の両親が代入されます。そして、次のゴール (父親 ?x ?z) で ?z の父親が ?x に代入されます。変数を使うことで実行結果を保持し、次のゴールへ値を渡すことができるのです。それでは実行してみましょう。

(Q '(祖父 ?x ?y))
?y = 洋子
?x = 太郎

?y = 三郎
?x = 太郎

nil

祖父と同じように「祖母」の関係も定義できます。

●実行例 -- 簡単なリスト操作

今回作成したエキスパートシステムは、簡単なリスト操作も行うことができます。ユニフィケーション unify は、ドット対を使うと面白い動作をします。次の例を見てください。

(unify '(春 夏 秋 冬) '(?x . ?y) nil) => (?y ?x)

ユニフィケーションは成功しましたが、?x と ?y にはどんな値がセットされているでしょうか。

?x => 春
?y => (夏 秋 冬)

リストとドット対をマッチングすると、ドットの後ろの変数は「残りのリストすべて」とマッチングします。この場合、?x が先頭の要素とマッチングし、春を取り除いた残りのリストと ?y がマッチングします。

これは、unify の仕組みから考えると当然の動作です。まず、春と ?x がマッチングしますね。次は、CDR 部が取り出されますが、この場合、(夏 秋 冬) と ?y を照合することになります。したがって、?y の値は (夏 秋 冬) となるのです。もう少し例を見てみましょう。

(unify '(春 夏 秋 冬) '(?x ?y . ?z) nil) => (?z ?y ?x)
?x => 春
?y => 夏
?z => (秋 冬)

この例では、?x が春、?y が夏にマッチングし、残りのリストと ?z がマッチングします。Lisp では、car でリストの先頭の要素を、cdr で先頭を取り除いた残りのリストを求めることができますが、今回のシステムでは、パターンマッチングを行うことでリストを分解することができるのです。実は、Prolog でも同じようにリストを操作することができます。

それでは具体的にリスト操作を行う規則を作ってみましょう。先頭の要素を求めることと、先頭の要素を取り除いた残りのリストを求めることは、リストのパターンマッチングを使えば簡単に実現できます。

List 6 : リスト操作

; 先頭の要素を取り出す
((first (?x . ?y) ?x))

; 先頭の要素を取り除いたリストを求める
((rest (?x . ?y) ?y))

C言語や Lisp などのプログラミング言語では、実行結果を関数の返り値として出力させます。ところが、今回のエキスパートシステムでは、第 1 引数にリストを与え、第 2 引数の変数で解を取り出すように定義します。簡単な実行例を示しましょう。

(Q '(first (a b c d) ?z))
?z = a

nil

(Q '(rest (a b c d) ?z))
?z = (b c d)

nil

first も rest もリストと (?x . ?y) をマッチングします。?x には a が、?y には (b c d) がセットされます。first の場合は変数 ?z と変数 ?x がマッチングするので、?z の値は a となります。rest の場合は変数 ?y とマッチングするので、?z の値は (b c d) となるのです。

次に、リストの先頭にデータを追加する規則を作ります。

List 7 : リストの先頭にデータを追加

((add_to_list ?x ?l (?x . ?l)))

では、実行例を見てください。

(Q '(add_to_list a (b c d) ?z))
?z = (a b c d)

nil

第 1 引数に追加するデータ、第 2 引数にリスト、第 3 引数で結果を受け取ります。Lisp では cons でリストの合成を行いますが、このシステムではパターンマッチングで行うことができるのです。

次は、2 つのリストを 1 つのリストに結合する規則を作りましょう。Lisp の関数 append と同じ働きします。Lisp は再帰を使って簡単に作ることができました。今回作成したエキスパートシステムでも、再帰を使って簡単に作ることができます。

List 8 : リストの結合

((append nil ?x ?x))
((append (?u . ?x) ?y (?u . ?z)) (append ?x ?y ?z))

append は、第 1 引数と第 2 引数のリストを結合した結果が、第 3 引数にセットされます。最初の規則は、空リストと ?x を結合すると ?x である、ということを表しています。これが再帰呼び出しの停止条件になります。次の規則では、頭部で第 1 引数のリストを ?u と ?x に分解して、?x と ?y を append したリスト ?z の先頭に ?u を追加します。

実際の動作は、再帰呼び出しによって第 1 引数のリストが分解され、停止条件で第 2 引数が第 3 引数とマッチングします。この間 ?z はずっと自由変数のままであることに注意してください。再帰呼び出しから戻るときに ?z は束縛されるのです。

停止条件から戻ってきた直後に、?z は第 2 引数のリストに束縛されています。あとは、(?u . ?z) によって第 1 引数の要素が追加され、その値が呼び出し元の ?z の値となります。けっきょく、再帰呼び出しから戻っていくときに、第 1 引数の要素が ?z に追加されていって、リストが連結されるのです。

なにか騙されているように思われるかもしれませんが、これで正常に動作します。それでは実行してみましょう。

(Q '(append (a b c) (d e f) ?z))
?z = (a b c d e f)

nil

(Q '(append ((a b) (c d)) ((e f) (g h)) ?z))
?z = ((a b) (c d) (e f) (g h))

nil

プログラムの動作を考えると頭が混乱するかもしれません。このようなエキスパートシステムでは、「矛盾しないように規則を定義する」ことを心がけた方がよいのでしょう。

ところで、append はリストを結合するだけでなく、リストを分解する動作を行うことができます。

(Q '(append ?z (c d) (a b c d)))
?z = (a b)

nil

(Q '(append (a b) ?z (a b c d)))
?z = (c d)

nil

もっと凄いのが、第 1 引数と第 2 引数を変数にすることです。

(Q '(append ?x ?y (a b c d)))
?y = nil
?x = (a b c d)

?y = (d)
?x = (a b c)

?y = (c d)
?x = (a b)

?y = (b c d)
?x = (a)

?y = (a b c d)
?x = nil

nil

このように、2 つのリストに分解できるすべての組み合わせを求めることができます。ひとつの規則で複数の使い方ができるのが、ほかの言語にはマネのできない面白い特徴です。

今度は、リストの中から要素をひとつ選ぶ規則を作ってみます。

List 9 : 要素の選択

((select ?x (?x . ?l) ?l))
((select ?x (?y . ?l) (?y . ?z)) (select ?x ?l ?z))

select は第 2 引数のリストから要素を選んで第 1 引数の変数にセットし、第 3 引数に残りのリストをセットします。最初の規則は簡単ですね。第 2 引数のリストを分解して先頭の要素を選びます。そして 2 番目の規則では、先頭要素を取り除いた残りのリストの中から要素を選びます。頭部の第 2 引数 (?y . ?l) でリストを分解し、select を再帰呼び出しします。要素 ?y は取り除いたリスト ?z に追加することをお忘れなく。

それでは実行してみましょう。

(Q '(select ?x (a b c) ?y))
?y = (a b)
?x = c

?y = (a c)
?x = b

?y = (b c)
?x = a

nil

この select を使って順列を求めることができます。

List 10 : 順列を求める

((perm nil nil))
((perm ?x (?z . ?l)) (select ?z ?x ?y) (perm ?y ?l))

perm は第 1 引数のリストに格納されている要素の順列を第 2 引数の変数に求めます。2 番目の規則を見てください。?x から select で要素をひとつ選び ?z にセットします。次に、残ったリスト ?y の順列を perm で求め、その結果 ?l に ?z を追加すれば ?x の順列は完成します。再帰の停止条件は、リストの要素が無くなった場合です。

再試行するたびに select は新しい要素を返すので、これですべての順列を求めることができるのです。では、実行してみましょう。

(Q '(perm (a b c) ?y))
?y = (c b a)

?y = (c a b)

?y = (b c a)

?y = (b a c)

?y = (a c b)

?y = (a b c)

nil

正常に動作していますね。パターンマッチングとバックトラックだけで、ここまでリスト操作ができるとは驚きです。ただし、変数の使い方がC言語や Lisp と大きく異なるので、慣れるまでちょっと苦労しそうですね。

これまで作成したリスト操作は Prolog でも同じように行えます。参考までに、Prolog で書かれたプログラムを示します。

List 11 : Prolog のプログラム

% 先頭の要素を取り出す
first( [X|Y], X ).

% 先頭の要素を取り除いたリストを求める
rest( [X|Y], Y ).

% リストの先頭にデータを追加
add_to_list( X, L, [X | L] ).

% リストの結合
append( [], X, X ).
append( [U | X], Y, [U | Z] ) :- append( X, Y, Z ).

% 要素の選択
select( X, [X | L], L).
select( X, [Y | L], [Y | Z]) :- select( X, L, Z ).

% 順列を求める
perm( [], [] ).
perm( X, [Z | L] ) :- select( Z, X, Y ), perm( Y, L ).

Prolog では、リストを [ X, Y, Z ] のように [ ] で表します。要素がない [ ] は空リストを表していて、ドット対のドット ( . ) に対応するのが | です。エキスパートシステムの規則とほとんど同じですね。このシステムに数値演算、条件分岐、入出力などの機能を組み込むことで、よりいっそう Prolog インタプリタに近づけることができます。

●おわりに

パターンマッチングとバックトラックを用いて、規則の中から答えを導き出す簡易エキスパートシステムを作成しました。ドキュメントの分量は多くなりましたが、プログラム自体は約 7 k byte です。記号処理が得意な Lisp だからこそ、ここまでコンパクトに記述できたと思います。ほかの言語ではパターンマッチングを作るだけでも大変でしょう。

今回作成したプログラムは簡易システムとはいいながら、その動作は Prolog とほぼ同じです。もっとも、これより複雑な規則を作るのであれば、このシステムでは力不足です。これに対し Prolog はプログラミング言語です。Lisp と同様に、データ型を判定する述語、算術演算、プログラムを制御するための述語や入出力が組み込まれているので、本格的なプログラムを作成することができます。

一時期、第五世代プロジェクトの影響で Prolog の人気が沸騰しましたが、最近は鎮静化してしまったようです。ですが、Prolog は面白い特徴を持ったプログラミング言語です。事実と規則を定義していく、つまり、物事の関係を記述していくプログラミングスタイルは、ほかの言語にはない独自なものです。M.Hiroi が初めて Prolog でプログラミングしたときは、C言語や Lisp との違いが大きすぎて、とても困惑してしまいました。かえって、ほかの言語を知らない方が、Prolog を簡単にマスターできるかもしれません。

このエキスパートシステムに興味を持たれた方は、ぜひ Prolog にも挑戦してみてください。よろしければ拙作のページ Prolog Programming もお読みくださいませ。


ちょっと寄り道

■フリップ・イット・スターを解く

今回はパズル「フリップ・イット・スター」を Common Lisp で解いてみましょう。「フリップ・イット (Flip It)」は芦ヶ原伸之氏が考案されたパズルで、すべての駒を裏返しにするのが目的です。詳しいルールは Puzzle DE Programmingフリップ・イット・スター をお読みください。それでは問題です。

図では空き場所を □ で表しています。ルールは「フリップ・イット」と同じで、駒は直線に沿ってほかの駒を跳び越すことができます。途中で曲がって跳び越すことは許されません。すべての駒を白にする最短手順を求めてください。

-- 参考文献 --------
[1] 芦ヶ原伸之『ブルーバックス B-1377 超々難問数理パズル 解けるものなら解いてごらん』 講談社

■反復深化+下限値枝刈り法

今回は「反復深化」でプログラムを作ります。ただし、単純な反復深化だと M.Hiroi のオンボロマシン (Pentium 166 MHz) では時間がかかるので、下限値枝刈り法を使うことにします。

フリップ・イット・スターの場合、簡単な方法で下限値を求めることができます。6 つの頂点に注目してください。頂点にある駒はほかの駒から跳び越されることはありません。頂点の駒が黒の場合、まず頂点から別の場所に移動して、それからほかの駒に跳び越されないと白にすることはできません。したがって、頂点にある黒駒を裏返しにするには、最低でも 2 手必要になることがわかります。これを下限値として利用することにしましょう。

盤面はリストで表すことにします。リストと盤面の対応を図に示します。

List 12 : 直線の定義

(setq *line*
      #2A((0 2 5 7)      ; 0  
          (0 3 6 10)     ; 1  
          (7 8 9 10)     ; 2
          (1 2 3 4)      ; 3
          (1 5 8 11)     ; 4
          (4 6 9 11)))   ; 5

6 本の直線は配列 *line* で表すことができます。ここで、*line* に格納される番号は昇順に並んでいることに注意してください。

駒の移動は跳び先表を定義すると簡単です。次のリストを見てください。

List 13 : 駒の跳び先表

; 駒の跳び先表(直線の番号と駒の位置)
(setq *move-pattern-table*
      #((0 5 0 7 1 6 1 10)  ; 0
        (3 3 3 4 4 8 4 11)  ; 1
        (0 7 3 4)           ; 2
        (1 10 3 1)          ; 3
        (3 1 3 2 5 9 5 11)  ; 4
        (0 0 4 11)          ; 5
        (1 0 5 11)          ; 6
        (0 0 0 2 2 9 2 10)  ; 7
        (2 10 4 1)          ; 8
        (2 7 5 4)           ; 9
        (1 0 1 3 2 7 2 8)   ; 10
        (4 1 4 5 5 4 5 6))) ; 11

配列 *move-pattern-table* は空き場所を基準にして、直線の番号と移動する駒の位置を順番に定義しています。配列の要素はリストで、その要素は直線の番号と動かす駒の位置です。たとえば、空き場所が 2 であれば、直線 0 の 7 にある駒、直線 3 の 4 にある駒を動かすことがでます。

駒を動かす関数 move-piece は次のようになります。

List 14 : 駒を動かす

(defun move-piece (board line p1 p2)
  ; p1 と p2 の要素を交換
  (psetf (nth p1 board) (nth p2 board)
         (nth p2 board) (nth p1 board))
  ; 順番のチェック
  (if (< p2 p1) (psetf p1 p2 p2 p1))
  ; 駒の裏返し
  (dotimes (x 4 board)
    (let ((p3 (aref *line* line x)))
      (if (< p1 p3 p2)
          (setf (nth p3 board)
                (if (eq 'B (nth p3 board)) 'W 'B))))))

引数 board が盤面、line が直線の番号、p1 と p2 が動かす駒の位置と空き場所の位置です。空き場所と駒の位置は p1 と p2 のどちらでもかまいません。最初に p1 と p2 にある要素を交換します。今回はリストを破壊的に修正していることに注意してください。

リストや配列の要素を交換する場合、次のように作業用の変数(テンポラリ変数)を使う方法が一般的でしょう。

(let ((temp (nth p1 board)))
  (setf (nth p1 board) (nth p2 board)
        (nth p2 board) temp))

Common Lisp の場合、代入を並行に行うマクロ psetq や psetf を使うと、テンポラリ変数を使わなくても値を交換することができます。ここで psetq と psetf を簡単に説明します。

psetq symbol1 value1 symbol2 value2 ...

psetq はシンボル symbol1 に value1 を評価した結果を代入し、symbol2 に value2 を評価した結果を代入する、というように値を順番に代入します。このとき、psetq は setq と違って代入した値の影響をうけません。簡単な例を示します。

(setq x 100 y 200) => 200
x => 100
y => 200

(psetq x y y x) => nil
x => 200
y => 100

psetq で x に y の値 200 を代入し、そのあとで x の値を y に代入しています。このとき x の値は 200 ではなく、代入される前の値 100 のままなのです。したがって、y に代入される値は 100 になります。

psetf は setf と同様に指定された場所へ値を代入しますが、setf と違って代入した値の影響をうけません。psetf は nil を返します。簡単な例を示します。

(setq board '(1 2 3 4))             => (1 2 3 4)
(psetf (nth 0 board) (nth 3 board)
       (nth 3 board) (nth 0 board)) => nil

board => (4 2 3 1)

このように、psetq や psetf を使うと簡単に値を交換することができます。

プログラムの説明に戻ります。次に駒を裏返しにしますが、ここで配列 line に格納されている番号が昇順に並んでいることを利用します。p1 < p2 になるように値を入れ替えて、p1 と p2 の間にある駒を裏返しにします。B と W は黒石と白石を表すシンボルです。

あとは「反復深化+下限値枝刈り法」で最短手順を求めるだけです。次のリストを見てください。

List 15 : 反復深化+下限値枝刈り法

(defun solve-id (n limit space board history)
  (if (= n limit)
      (when (zerop (count 'B board))
        (print-answer n board history)
        (throw 'find-answer t))
    ; 駒を動かす
    (let ((pattern (aref *move-pattern-table* space)) line pos)
      (while pattern
        (setq line (pop pattern) pos (pop pattern))
        (when (or (not (eql (first (car history)) line))
                  (not (eql (second (car history)) pos)))
          ; 移動可能
          (move-piece board line space pos)
          ; 下限値による枝刈り
          (if (<= (+ n 1 (get-lower-value board)) limit)
              (solve-id (1+ n)
                        limit
                        pos
                        board
                        (cons (list line space pos) history)))
          ; 元に戻す
          (move-piece board line space pos))))))

関数 solve-id の引数 n が手数、limit が反復深化の上限値、space が空き場所の位置、board が盤面、history が移動手順です。history の要素はリスト (直線の番号 空き場所の位置 動かした駒の位置) です。

手数 n が上限値 limit に達したならば、関数 count で黒石 (B) の個数を数えます。黒石が 0 個ならば全部の石が白になったので、print-answer で手順を表示して throw で大域脱出します。そうでなければ、駒を動かして新しい局面を生成します。

配列 *move-pattern-table* から直線の番号と動かす駒の位置を取り出して、変数 line と pos にセットします。フリップ・イットは、同じ駒を続けて動かすと元の状態に戻ってしまいます。そこで、1 手前の直線の番号 (first (car history)) が同じで、動かす駒の位置 pos が 1 手前の空き場所の位置 (second (car history)) と同じ場合は、その駒を動かさないようにします。このチェックがないと実行時間がとても遅くなります。ご注意くださいませ。

次に、move-piece で駒を動かして、下限値のチェックを行います。関数 get-lower-value は盤面 board の下限値を求めます。下限値の計算は頂点にある黒駒を数えて 2 倍するだけです。「手数+下限値」が上限値 limit 以下であれば solve-id を再帰呼び出しします。そのあと、move-piece で盤面を元に戻すことをお忘れなく。

あとのプログラムは簡単なので説明は省略いたします。詳細は プログラムリスト をお読みくださいませ。

■実行結果

さっそく実行してみたところ、最短手順は次のようになりました。図では黒石を B, 白石を W, 空き場所を S で表しています。

     S
  B B B B
   B   B
  B B B B
     B

 -(1)----- -(2)----- -(3)----- -(4)----- -(5)----- -(6)----- 
     B         B         B         B         B         S     
  B W B B   B S B B   B B W S   B B W B   B B W B   B B B B  
   W   B     B   B     B   B     B   W     B   S     B   B   
  S B B B   W B B B   W B B B   W B W B   W B B B   W B B B  
     B         B         B         S         W         W     

 -(7)----- -(8)----- -(9)----- -(10)---- -(11)---- -(12)---- 
     W         W         W         W         W         W     
  B W B B   B S B B   B B W S   B B W W   S B W W   W W S W  
   W   B     B   B     B   B     B   W     W   W     W   W   
  S B B B   W B B B   W B B B   W B W B   W W W B   W W W B  
     W         W         W         S         B         B     

 -(13)---- -(14)---- -(15)---- -(16)---- -(17)---- -(18)---- 
     W         S         W         W         W         W     
  W W B W   W W W W   W B W W   W B W W   S B W W   W W S W  
   W   B     W   W     S   W     B   W     W   W     W   W   
  W W W S   W W W W   W W W W   W B W W   W W W W   W W W W  
     B         B         B         S         W         W     

          図 4 : 「フリップ・イット・スター」の解答

最短手数は 18 手、実行時間は M.Hiroi のオンボロマシン (Pentium 166 MHz) で約 13 秒でした。ちょっと遅いように思われたかもしれませんが、これでも下限値枝刈り法の効果は十分に出ています。単純な反復深化だけでは、もっともっと時間がかかるでしょう。

ちなみに、C言語で単純な反復深化のプログラムを作成した場合、M.Hiroi のオンボロマシンでは約 7 秒かかりました。興味のある方は、Puzzle DE Programmingフリップ・イット・スター をお読みくださいませ。


■プログラムリスト

;
; flip_star.l : パズル「フリップ・イット・スター」
;               反復深化+下限値枝刈り法による解法
;
;               Copyright (C) 2003 Makoto Hiroi
;

; 直線の定義
(setq *line*
      #2A((0 2 5 7) (0 3 6 10) (7 8 9 10)
          (1 2 3 4) (1 5 8 11) (4 6 9 11)))

; 駒の跳び先表(直線の番号と駒の位置)
(setq *move-pattern-table*
      #((0 5 0 7 1 6 1 10)  ; 0
        (3 3 3 4 4 8 4 11)  ; 1
        (0 7 3 4)           ; 2
        (1 10 3 1)          ; 3
        (3 1 3 2 5 9 5 11)  ; 4
        (0 0 4 11)          ; 5
        (1 0 5 11)          ; 6
        (0 0 0 2 2 9 2 10)  ; 7
        (2 10 4 1)          ; 8
        (2 7 5 4)           ; 9
        (1 0 1 3 2 7 2 8)   ; 10
        (4 1 4 5 5 4 5 6))) ; 11


; 駒を動かす
(defun move-piece (board line p1 p2)
  ; p1 と p2 の要素を交換
  (psetf (nth p1 board) (nth p2 board)
         (nth p2 board) (nth p1 board))
  ; 順番のチェック
  (if (< p2 p1) (psetf p1 p2 p2 p1))
  ; 駒の裏返し
  (dotimes (x 4 board)
    (let ((p3 (aref *line* line x)))
      (if (< p1 p3 p2)
          (setf (nth p3 board)
                (if (eq 'B (nth p3 board)) 'W 'B))))))


; 下限値を求める
(defun get-lower-value (board)
  (let ((value 0))
    (dolist (x '(0 1 4 7 10 11) (* value 2))
      (if (eq 'B (nth x board)) (incf value)))))


; 盤面を表示する
(defun print-board (board)
  (apply #'format
         t
         "    ~S~% ~S ~S ~S ~S~%  ~S   ~S~% ~S ~S ~S ~S~%    ~S~%~%"
         board))


; 手順を表示する
(defun print-answer (n board history)
  (when history
    (apply #'move-piece board (car history))
    (print-answer (1- n) board (cdr history))
    (apply #'move-piece board (car history)))
  (format t "----- ~D 手 -----~%" n)
  (print-board board))


;
; 反復深化+下限値枝刈り法
; history = (line space piece)
;
(defun solve-id (n limit space board history)
  (if (= n limit)
      (when (zerop (count 'B board))
        (print-answer n board history)
        (throw 'find-answer t))
    ; 駒を動かす
    (let ((pattern (aref *move-pattern-table* space)) line pos)
      (while pattern
        (setq line (pop pattern) pos (pop pattern))
        (when (or (not (eql (first (car history)) line))
                  (not (eql (second (car history)) pos)))
          ; 移動可能
          (move-piece board line space pos)
          ; 下限値による枝刈り
          (if (<= (+ n 1 (get-lower-value board)) limit)
              (solve-id (1+ n)
                        limit
                        pos
                        board
                        (cons (list line space pos) history)))
          ; 元に戻す
          (move-piece board line space pos))))))


; フリップ・イット・スターの解法
(defun solve-flip-star (pos)
  (let ((board (make-list 12 :initial-element 'B)))
    (setf (nth pos board) 'S)
    (catch 'find-answer
      (do ((limit (get-lower-value board) (1+ limit)))
          ((> limit 24))
        (format t "----- ~D 手 を探索 -------~%" limit)
        (solve-id 0 limit pos board nil)))))

Copyright (C) 2000-2003 Makoto Hiroi
All rights reserved.

[ PrevPage | xyzzy Lisp | NextPage ]