プログラミングパズルに関心のある人は雑談しましょう!
11パズル最長手数面の探索に関する一連のメモリ節約法を実験してみた結果です。 実験1: 1手ずつ、ビットマップ3枚 メモリ=89,812,800byte 455sec 実験2: 1手ずつ、ビットマップ2枚 メモリ=59,875,200byte 518sec 実験3: 2手ずつ、ビットマップ2枚 メモリ=29,937,600byte 348sec 実験4: 2手ずつ、ビットマップ1枚+2M メモリ=16,968,800byte 392sec CPU = Athlon XP 2100+ (1.73GHz) Compiler = Visual C++ 5.0 (実行速度で最適化) deepgreenさんの考案した、(2)+(3)の方法が一番魅力的のように感じました。 ただ、ディスクアクセスがプログラムの意図したタイミングで行われているかは 再調査の余地があります。メモリに余裕が有り過ぎるとOSが勝手に最適化して しまう可能性が考えられるのです。とは言っても、(2)+(3)の方法がメモリの 節約という観点と実行速度で使いものなるという観点からベストのように思います。
塗り壁さん、こんばんは。お久しぶりです。 ホームページの移転に関する、リンクの更新は了解しました。 N−クイーン問題も15パズルも終わりは無いと思っています。 ただ、私の場合は数学的なアプローチがとても弱いので着いて行く のがつらいのですが、なるだけ着いて行きたいと思っています。 中学生の頃までは数学も得意だったのですが、ポリポリ。 いろんなアイデアはお互いに使い回しするのが正しい道だと思っています。 山があるから登るみたいに、興味の尽きない問題はみんなでどこまでも 登って行きましょう。
高橋謙一郎様、御無沙汰しております。 「点と線の部屋」の高橋です。 このたび、プロバイダーの都合でホームページを移転することになりました。 移転先は次の通りです。よろしければリンクの更新をお願い致します。 「点と線の部屋」本館 http://www32.ocn.ne.jp/~graph_puzzle/index.html 直接メールでも良かったのですが、目立たせようとこちらに記載させてもらいました。 悪しからず、ご了承ください。 ついでといってはなんですが、2件ほど書かせてもらいます。 ・N−クイーン問題 プロジェクトの話は私はあきらめておりません。手詰まりとのことですが、 そういう場合の数学での常套手段は、問題を一般化することです。 一般化の方針は幾何学的な観点、代数学的な観点などいろいろあると思いますが、 組み合わせ論的には次のようになると思います。 ●N次の順列に対し、同じ斜め一直線上にある点の対の個数をクイーン指数 と呼ぶ事にする。クイーン指数がmである順列の個数をQInd(N,m)と書くとき、 それらの値、及び値の間に成り立つ関係を求めよ。 言わずもがなですが、Qind(N,0)がN−クイーン問題の解の個数に当たります。 私も、そのうちこの問題を考えようと思っていますが、興味のある方は是非挑戦して下さい。 ・15パズル 先日の貴兄の書き込みにて、私もプログラムを作成してみました。(そのうち、 ホームページにも載せようと思います) この問題も数学的には順列の話ですね。空きマスを移動させることは、 状態(を表わす順列)に互換(2個の要素を交換し、他の要素は動かさない置換) を作用させることに当たります。 私のプログラムはその状態を木構造で管理してそれを幅優先で辿るというものです。 木構造の実装が今の状態では結構メモリーを食いますので、残念ながら今は9パズ ルまでしか対応できていません。 ただ、もう少し理論的に追い込む必要がありそうです。例えば、3パズルの最長手数の 最終状態は<(1,4)(2,3)>とサイクル表示できます。これは斜めの対ですね。 同様に、5パズルは<(1,4)(2,5)(3,6)> 縦並びの対。 7パズルは<(1,8,5,4)(2,7)(3,6)> 中央斜めの対、及び、それ以外(両端) 以降は略しますが、この辺が取っ掛かりになりそうな気がします。
deepgreenさん、こんばんは。 それにしても次々といろんなことを考えるものだと驚いてしまいます。 でも、私の力不足のため、今回の(5)の方法は理解するのが難しいです。 「すべてのビットが0のマップは作らないことにする」という意味がよく分かりません。 私がテスト中だった方法と似ている気もします。(成果はあまりよく無かったのですが) 私がテスト中だった方法: BitMap全体を最初に作るのではなくBitMap全体をいくつかの領域に分割することにして、 あるBitMap領域を初めて参照しようとしたときに、その領域だけのBitMapを動的に確保し、 あるBitMap領域がすべて(n-2)手の局面で埋まって役目を終えたなら動的にメモリを開放する。 (5)の方法も、動的にメモリを確保したり開放したりするということなのでしょうか。 それと、「ただし、*:上位の桁 ?:下位の桁とする」の部分ですが、「*」は位置の ことなのか、初期完成形でその位置にあるコマ(場所は変動する)ということなのかも 教えていただけると助かります。すみません、よろしくお願いします。
こんばんは、deepgreenです。 強いて圧縮をやるとすればという案ですが、どうでしょうか。 (5)マップの圧縮 ハッシュ値を上位m桁、下位n桁に分けて考える。 下位n桁は従来どおりのビットマップとする(n!/2のMapがたくさんある) メモリを節約するため、すべてのビットが0のマップは作らないことにする。 上位m桁の値でこのビットマップを以下のように管理する(MapIndexと呼ぶ) −1 : ビットマップは存在しない(値がすべて0のマップのため) k : 当該の下位ビットマップ用にK番目のマップ域を使用する 局面の配置 abcd efgh ---> ハッシュ値 ---> U = H / (n!/2) ijkl (H) | +---+------------+ ↓ ↓ +---+----+----+---+----+---+--------- MapIndex | 0 | -1 | -1 | 1 | -1 | 2 | ... +---+----+----+---+----+---+--------- | +---------+ ↓ +-----+-----+-----+------------- | | | | BitMap | [0] | [1] | [2] | ... | | | | +-----+-----+-----+------------- BitMap内の位置は、L = H % (n!/2) で求める 高橋謙一郎さんの11パズルSolverを少し改造して、m=6,n=6の 場合について必要なビットマップの数を調べた結果、以下のようになりました。 MapIndexの位置 必要なBitMap数 BitMapの全数 圧縮率 [ケース1] ***? **?? 329,520 665,280 49.5% *??? [ケース2] **** **?? 300,097 665,280 45.1% ???? [ケース3] **** *??? 294,522 665,280 44.3% *??? ただし、*:上位の桁 ?:下位の桁とする MapIndexに必要なメモリは、665,280x4=約2.7MB BitMapに必要なメモリは、約30MB*0.45=13.5MB あわせて、約15.2MBとなります。 [ケース1]が良いだろうと思って最初に調べたのですが、結果は普通にやる方が よいと出てしまいました。
高橋さん、こんばんは。deepgreenです。 ご指摘のとおり、(4)は,重すぎてつかえませんね。 策におぼれてしまいました。面白いとおもったのですが。 ハッシュ関数を変える検討もしてみましたが、うまい答えがみつかりません。 確かに、nを中心に考えると、(n-1),(n+1)はその近傍になるので何かできそうな 感じはするのですが,...
deepgreenさん、こんばんは。 (2)は1回の探索で済ます方法がありましたね。私が甘かったです。(2)の方法は とても素晴らしい方法だと再認識しました。それにしても、deepgreenさんて、こういう 解析力は抜群ですね。(3)の方法はプログラムがややこしそうなので、(1)+(2)の 方法は、かなり有効な手段になりそうです。 (4)は、空きマスの位置に着目した偶奇性が合致して(n−1)手の局面ではない局面 すべてに対して、1手もどした局面にn手の局面が存在すれば(n+1)手の局面である という逆発想でひとつひとつ鑑定していくのであれば、暗黙的に存在する下の方法(0) よりは確かに良いのですが、「(1)+(2)の方法」と同じメモリ量が必要になる分だけ、 効率がよくないと思います。でも、こういう方法を思い付けるのは、すごいです。 (0)12!/2 のすべての局面からそれぞれ完成形までの最短手数を反復深化で調べる。 保存用メモリはまったく要らない。(ただし、効果的な下限値枝刈り等が必要) 尚、追加問題については最小完全ハッシュ関数の作り方から見直す必要があると思います。 最小とはならないまでもn手付近のハッシュ値が群生するような特別な「完全ハッシュ関数」 の作り方が必要になると思うので私には難問すぎるようです。deepgreenさんならと振って みたのですが迷惑だったかも知れませんね。
こんにちは、deepgreenです。 追加問題がでるとは思いもよらないことでしたが、...ちょっと補足します。 (2)の補足:最長手数とその局面を求める方法 2手づつの探索では、最長手数がn、(n+1)のいずれかを確定する処理が必要となります。 探索の結果、(n+2)手の局面が存在しない場合は、以下の手続きで最長手数とその局面を 求めることができる。 n手の局面から1手進めた局面をaとします。aは、(n+1),(n-1)のいづれかの筈で すから、aを基準にして1手進めた局面はn,(n-2) のいづれかになります。aを基準 にして生成される局面がすべてn手の局面であれば、aは(n+1)手の局面と判明します。 下図のbのように(n-2)手の局面が1つでも生成される場合は、(n-1)手の局面です。 (n+1)手の局面 ---> a / \ n 手の局面 ---> X Y Z \ / (n−1)手の局面 ---> b / \ (n−2)手の局面 ---> P Q R 上記の操作で(n+1)手の局面が全く存在しない場合は、最長手数は、n手になります。 (4)3枚を1枚にする方法 (n-1),n,(n+1)の局面を1枚のマップにたたみこむ方法です。 前回の(2)で述べたように、局面の奇偶性に着目すると、nと(n+1) 又は、nと(n-1) を1つのマップで管理することが可能です。あるビットがオンになっていた時、その局面 の奇偶性を調べることで、n,(n+1)のいづれであるかを判別できるからです。しかし、 (n-1)と(n+1)の奇偶性は同じため、(n-1),n,(n+1)を同時に管理することはできません。 そこで、x<Kの範囲では、n,(n+1)を管理し、x>=Kの範囲では、n,(n-1)を管 理するという棲み分けを考えます。Kの値を0からMaxまでずらしていくことにより、 n,(n-1)の局面からn,(n+1)の局面に変換していきます。 (局面の奇偶性の定義) * ○ * ○ 空きマスの位置が○のとき 偶数手の局面 ○ * ○ * 空きマスの位置が*のとき 奇数手の局面 * ○ * ○ (マップの各ビットの定義) n,(n+1)のマップ n,(n-1)のマップ [0]--------------[K]-----------------[Max] | | | +---> 0 : (n-2)手以下の局面,又は(n+1)手以上の局面 | 1 : 局面の奇偶性とnの奇偶性が一致する場合は | n手の局面、そうでなければ、(n-1)手の局面 | +-------------> 0 : (n-1)手以下の局面,又は(n+2)手以上の局面 1 : 局面の奇偶性とnの奇偶性が一致する場合は n手の局面、そうでなければ、(n+1)手の局面 (Kを1つ進める手順) Kの位置のビット=1の時 当該ビットに対応する局面の奇偶性とnの奇偶性が一致する場合はKを1つ進 めるだけでよい。そうでない場合は、当該ビットを0にしてKを1つ進める。 Kの位置のビット=0の時 当該ビットに対応する局面の奇偶性とnの奇偶性が一致する場合はKを1つ進 めるだけでよい。(この局面は(n+2m)、(n-2m)の局面であり、(n+1)の局面と はなりえない) そうでない場合は、当該ビットに対応する局面をaとすると、aの局面から1手 もどした局面をすべて生成し、n手の局面が存在するかどうかを調べる。n手 の局面が1つでも存在する場合は、当該ビットを1にしてKを1つ進める。n 手の局面が全く存在しない場合はそのままKを1つ進める。 上記の操作によりKを進めていきMaxまで到達した場合は、1手進める。 (n+1-->n,K=0にする)
deepgreenさん、お久しぶりです。 そして、大変素晴らしい解答をありがとうございます。 既にお分かりでしょうが、私の用意していた答えは(1)の2ビットの情報で十分 というものです。deepgreenさんの(2)(3)には感服しました。さすがです。 (2)の方法については、1回の探索で行う方法もあるのかも知れませんが、 0手目(1局面)からスタートする探索と、1手目(2局面)からスタートする探索を 別々に2回に分けて行えば、探索の論理としては明瞭で確実と思われます。 最小完全ハッシュ関数にしても、空きマス位置6通りを基数にして最初に抜き出せば 良いので問題はないでしょう。(3)の方法も素晴らしいです。キューはシーケンシャル なので小さなバッファ分だけメモリ上にあれば良いという訳ですね。これらのアイデアが 有効に使える問題に出会ったときは、即実行ですね。 続いて第2問です。実は、この問題の答えは私自身も用意がありません。 n手の局面を読んで(n+1)手の局面を生成するとき、同一面のチェックとしては、 (n+1)手、n手、(n−1)手のエリアだけを調べればよいということが知られて います。この理論を利用してメモリを節約する方法は? というのが問題です。
ご無沙汰してます。deepgreenです。 幅優先探索の場合、ある局面の状態を以下のいづれかに分けて管理します。 T:既に生成された局面 C:丁度、n手で生成される局面 N:(n+1)手で生成される局面 φ:まだ、生成されていない局面 (1)マップを3枚ー>2枚にする方法 この4つの状態を管理するには、2ビットの情報で十分であり、A,B2つの マップを用意して、以下のように対応させればよい。 A B 0 0 −−> φ 1 0 −−> T 1 1 −−> C 0 1 −−> N A,Bがともに1となる位置に対応する局面をスキャンして、その局面を基に 1手進めた局面を生成し、それに対応するA,Bの位置の状態が共に0であれば、 新たな局面であるから、Bのみを1(Nの状態)にする。 全てのn手の局面をスキャンしたら、Cの状態をTに、Nの状態をCに変更する。 n+1−−>nとして、上記の操作を繰り返す。 (2)管理する局面を間引く方法 探索のループを2手づつ進めることにすると、空きの位置は以下の図のように 12ケ所から6ケ所に減るので、マップそのものが半分になる。 * ○ * ○ ○:管理対象とする空きの位置(偶数手の局面) ○ * ○ * *:管理対象外とする空きの位置(奇数手の局面) * ○ * ○ 探索の論理がちょっと面倒になるが、メモリ使用量が半分になるのは魅力。 また、(1)の方法とも共存できるので、合わせ技で全体を1/3にできる。 (3)T,φのみをメモリで管理する方法 メモリ上にはT,φを管理するマップのみを残し、新たな局面かどうかのチェ ックに利用する。C,Nに対応する局面はハッシュ値をファイルに書いておく。 I/O時間が気になるが、1MB程度のバッファを使えば、sequentialI/O なので、大きな問題にはならない。 メモリ使用量は、30MB+2MB(バッファ)となる。(2)と合わせると 15MB+2MBと約1/6になる。