「数学パズル「4つの4」入門」のつづきです。前回は問題の雰囲気をざっとつかみました。今回は、パズルを解くための自律的思考アルゴリズムの基礎を紹介します。100行未満の小さなスクリプトでブーツストラッピングが始まり自走していく姿が、かなり印象的です。
すでに見たように例えば「4」を出発点にするとに
4 → √4
4 → 4!
4 → Σ4
といったことで「作れる数の可能性」をふやせます。もちろん「4」が与えられたとき、そのまま何もしない、という選択もあります。
4 → 4
同様に (4+4)
というひとつの表現をもとに、
(4+4) → √(4+4)
(4+4) → (4+4)!
(4+4) → Σ(4+4)
(4+4) → (4+4) (何もしない)
の4とおりができます。得られた(√(4+4))
を出発点にすれば、
(√(4+4)) → √(√(4+4))
(√(4+4)) → (√(4+4))!
(√(4+4)) → Σ(√(4+4))
(√(4+4)) → (√(4+4))
がでます。このように、ひとつの項に対して、単項演算子を適用することで「知っている」式表現が増えます。そのうちのいくつかは無効な表現です。上の例ですと、(√(4+4))! や Σ(√(4+4)) は、無効とみなされます。
自分がいま知っているすべての項を把握し、そのひとつひとつにすべてのパターンの単項演算子を適用して自律的に知識を増やす、というスクリプトの骨格を紹介します。まず単項演算子を定義します。ここでは概念を理解するのが目的なので、簡単のため平方根と階乗だけ考え、階乗も3と4に対してのみ値を返すようにしましょう。NOOPは何もしないで入力をそのまま返す演算子です。
var oUnaryOperators = new Object(); oUnaryOperators["NOOP"] = function( n ) { return n; } oUnaryOperators["Math.sqrt"] = function( n ) { if( n < 0 ) return void 0; else return Math.sqrt( n ); } oUnaryOperators["Factorial"] = function( n ) { if(n==3) return 6; else if(n==4) return 24; else return void 0; }
oUnaryOperators は関数列で、演算子の名前をキーとして関数を値とするハッシュです。これをキーにたばねておくのは、次のようなことをやらかすためです。
function think_about( oExpression_for ) { var new_brain = new Object(); for( value in oExpression_for ) { enrich( oExpression_for[value] , value , new_brain ); } return new_brain; } function enrich( proto_expression, proto_value , oExpression_for ) { for(var op_name in oUnaryOperators) { var new_value = oUnaryOperators[ op_name ]( proto_value ); if( new_value === void 0 ) continue; if( op_name === "NOOP" ) { var new_expression = proto_expression; } else { var new_expression = op_name + "( " + proto_expression + " )"; } if( oExpression_for[ new_value ] === void 0 ) { oExpression_for[ new_value ] = new_expression; } } } var knowledge = new Object(); knowledge[ "4" ] = "4"; var new_knowledge = think_about( knowledge ); for( var value in new_knowledge ) document.write( "<p>わたしは" + value + "を意味する記号列" + new_knowledge[value] + "を知ってます<\/p>" );
"4" が4を意味することだけ教えてあげると、あとは自分で考えて自分のデータベースを更新します。データベースに入るのは式表現とその式の意味(値)の対であり、値をキーとするハッシュです。出力例:
わたしは4を意味する記号列4を知ってます わたしは2を意味する記号列Math.sqrt( 4 )を知ってます わたしは24を意味する記号列Factorial( 4 )を知ってます
オブジェクト new_knowledge には例えば 24
というプロパティがあって、new_knowledge のプロパティ「24」の値は、Factorial( 4 ) という文字列です。
new_knowledge["24"] = "Factorial( 4 )"
というのは「24」の作り方についての知識(4の階乗)と解釈することも、記号列「Factorial( 4 )」は「24」を意味する、と解釈することもできます。本質的にはプロパティが数値で値が文字列なのですが、JavaScriptの仕様上、プロパティは文字列なので、両方、文字列になってます。キーの文字列は直接、数値を表し、値の文字列はその数値と等しい式表現です。
まったく同じ発想で二項演算子を処理すれば、あとはスクリプトがパズルを自動解決してくれるようになります。上の続き。
var oBinaryOparators = new Object(); oBinaryOparators["+"] = function(x,y) { return eval(x)+eval(y); } oBinaryOparators["-"] = function(x,y) { return x-y; } oBinaryOparators["*"] = function(x,y) { return x*y; } oBinaryOparators["/"] = function(x,y) { if( y==0 ) return void 0; else return x/y; } function mix( oExpression1_for, oExpression2_for, oNewExpressionsFor ) { for( var value1 in oExpression1_for ) { for( var value2 in oExpression2_for ) { for( var op_name in oBinaryOparators ) { var new_value = oBinaryOparators[ op_name ]( value1, value2 ); if( new_value === void 0 ) continue; var expr1 = oExpression1_for[ value1 ]; var expr2 = oExpression2_for[ value2 ]; var new_expression = "(" + expr1 + ")" + op_name + "(" + expr2 + ")" ; if( oNewExpressionsFor[ new_value ] === void 0 ) oNewExpressionsFor[ new_value ] = new_expression; } } } } var expression_for = new Object(); mix( new_knowledge, new_knowledge, expression_for ); for( var value in expression_for ) document.write("<p>わたしは" + expression_for[value] + "が" + value + "を意味することを知ってます。<\/p>");
出力例:
わたしは(4)+(4)が8を意味することを知ってます。 わたしは(4)-(4)が0を意味することを知ってます。 わたしは(4)*(4)が16を意味することを知ってます。 わたしは(4)/(4)が1を意味することを知ってます。 わたしは(4)+(Math.sqrt( 4 ))が6を意味することを知ってます。 わたしは(4)-(Math.sqrt( 4 ))が2を意味することを知ってます。 わたしは(4)+(Factorial( 4 ))が28を意味することを知ってます。 わたしは(4)-(Factorial( 4 ))が-20を意味することを知ってます。 わたしは(4)*(Factorial( 4 ))が96を意味することを知ってます。 わたしは(4)/(Factorial( 4 ))が0.16666666666666666を意味することを知ってます。 わたしは(Math.sqrt( 4 ))-(4)が-2を意味することを知ってます。 わたしは(Math.sqrt( 4 ))/(4)が0.5を意味することを知ってます。 わたしは(Math.sqrt( 4 ))+(Math.sqrt( 4 ))が4を意味することを知ってます。 わたしは(Math.sqrt( 4 ))+(Factorial( 4 ))が26を意味することを知ってます。 わたしは(Math.sqrt( 4 ))-(Factorial( 4 ))が-22を意味することを知ってます。 わたしは(Math.sqrt( 4 ))*(Factorial( 4 ))が48を意味することを知ってます。 わたしは(Math.sqrt( 4 ))/(Factorial( 4 ))が0.08333333333333333を意味することを知ってます。 わたしは(Factorial( 4 ))-(4)が20を意味することを知ってます。 わたしは(Factorial( 4 ))-(Math.sqrt( 4 ))が22を意味することを知ってます。 わたしは(Factorial( 4 ))/(Math.sqrt( 4 ))が12を意味することを知ってます。 わたしは(Factorial( 4 ))*(Factorial( 4 ))が576を意味することを知ってます。
これらのリストは説明のための概念的なものであり、現実の実装には適しません。実装上の詳しい説明は次回以降にまわして、とりあえずパズルをざっと解くための指示を書いてしまいます。上の2つの関数を組み合わせて4つの4で作れるものをデータベースにつっこみ、あとからほしいものを検索するだけです。
var eva = new Array(); for(var i=1; i<=4; i++) eva[i] = new Object(); var oBuffer = new Object(); oBuffer["4"] = "4"; eva[1] = think_about( oBuffer ); var oBuffer = new Object(); mix( eva[1], eva[1], oBuffer ); eva[2] = think_about( oBuffer ); var oBuffer = new Object(); mix( eva[1], eva[2], oBuffer ); mix( eva[2], eva[1], oBuffer ); eva[3] = think_about( oBuffer ); var oBuffer = new Object(); mix( eva[1], eva[3], oBuffer ); mix( eva[2], eva[2], oBuffer ); mix( eva[3], eva[1], oBuffer ); eva[4] = think_about( oBuffer ); for(var i=0; i<=100; i++) { var expression = eva[4][i.toString()]; if( expression !== void 0 ) document.write( "<p>" + i + " == " + expression + "<\/p>\n"); else document.write( "<p>" + i + ": 未解決<\/p>\n" ); }
出力例:
0 == (4)+((4)-((4)+(4))) 1 == (4)/((4)+((4)-(4))) 2 == (4)*((4)/((4)+(4))) 3 == (4)-((4)/(Math.sqrt( (4)*(4) ))) 4 == Math.sqrt( (4)+((4)+((4)+(4))) ) 5 == (4)+((4)/(Math.sqrt( (4)*(4) ))) 6 == Math.sqrt( (4)+((4)*((4)+(4))) ) 7 == (4)+((4)-((4)/(4)))
以下100まででます。ここで、eva[1] には1つの4で作れるすべての表現、eva[2] には2つの4で作れるすべての表現、……、eva[4]には4つの4で作れるすべての表現が格納されることに注意してください。ただし「すべて」の意味は、ここで定義した式表現空間の範囲のすべて、です。例えば、44や444を使ったり、指数を使ったり、総和を使ったり、特殊記号を二重に使う方法は、ここでは取り入れてません。上記の動作原理さえ分かれば、そういった細部も簡単に実装できます。
上の出力のかわりに、次のデバッグ用出力を実行すると理解に役立つかもしれません。
function dump( x ) { for( var value in eva[x] ) document.write("<p>学習プロセス" + x + "において、" + value + "を意味する記号列" + eva[x][value] + "を見つけました。<\/p>") } dump(1); dump(2); dump(3); dump(4);
出力例の最初の部分です。
学習プロセス1において、4を意味する記号列4を見つけました。 学習プロセス1において、2を意味する記号列Math.sqrt( 4 )を見つけました。 学習プロセス1において、24を意味する記号列Factorial( 4 )を見つけました。 学習プロセス2において、8を意味する記号列(4)+(4)を見つけました。 学習プロセス2において、2.8284271247461903を意味する記号列Math.sqrt( (4)+(4) )を見つけました。 学習プロセス2において、0を意味する記号列(4)-(4)を見つけました。 学習プロセス2において、16を意味する記号列(4)*(4)を見つけました。
以上で「4つの4」パズルをとく自律的思考アルゴリズムの基礎を終わります。次回以降は、計算時間の最適化(高速化)、そして、重要な話題として、ある数を作る方法が2とおり以上ある場合に、より簡潔なほうを選ぶためのアルゴリズム(式表現の最適化)について書く予定です。