8パズルは、9つの位置があり、8つの番号付きタイルと1つのギャップで埋められた正方形のボードです。いつでも、ギャップに隣接するタイルをギャップに移動して、新しいギャップ位置を作成できます。言い換えると、ギャップは隣接する(水平方向および垂直方向の)タイルと交換できます。ゲームの目的は、タイルの任意の構成から始めて、ボードの周囲を走り回るか、左上に1を付けて左から右に並べられるように、番号の付いたタイルを昇順で配置するように移動することです。 -手の位置。
この問題を解決するには、どのようなアプローチが効率的か疑問に思いました。
なぜそれが最適であるかについての詳細で前の答えを書き直そうとします。
wikipedia から直接取得したA *アルゴリズムは
_ function A*(start,goal)
closedset := the empty set // The set of nodes already evaluated.
openset := set containing the initial node // The set of tentative nodes to be evaluated.
came_from := the empty map // The map of navigated nodes.
g_score[start] := 0 // Distance from start along optimal path.
h_score[start] := heuristic_estimate_of_distance(start, goal)
f_score[start] := h_score[start] // Estimated total distance from start to goal through y.
while openset is not empty
x := the node in openset having the lowest f_score[] value
if x = goal
return reconstruct_path(came_from, came_from[goal])
remove x from openset
add x to closedset
foreach y in neighbor_nodes(x)
if y in closedset
continue
tentative_g_score := g_score[x] + dist_between(x,y)
if y not in openset
add y to openset
tentative_is_better := true
elseif tentative_g_score < g_score[y]
tentative_is_better := true
else
tentative_is_better := false
if tentative_is_better = true
came_from[y] := x
g_score[y] := tentative_g_score
h_score[y] := heuristic_estimate_of_distance(y, goal)
f_score[y] := g_score[y] + h_score[y]
return failure
function reconstruct_path(came_from, current_node)
if came_from[current_node] is set
p = reconstruct_path(came_from, came_from[current_node])
return (p + current_node)
else
return current_node
_
それで、ここにすべての詳細を記入させてください。
_heuristic_estimate_of_distance
_は関数Σd(x私)ここで、d(.)は、各正方形xのマンハッタン距離です。私 その目標状態から。
だからセットアップ
_ 1 2 3
4 7 6
8 5
_
_heuristic_estimate_of_distance
_は1 + 2 + 1 = 4になります。これは、8,5のそれぞれがd(。)= 1で目標位置から1離れており、7がd(7)で目標状態から2離れているためです。 = 2。
A *が検索するノードのセットは、開始位置とそれに続くすべての可能な正当な位置として定義されます。つまり、開始位置x
は上記のとおりです。
_ x =
1 2 3
4 7 6
8 5
_
次に、関数neighbor_nodes(x)
は、2つの可能な正当な動きを生成します。
_ 1 2 3
4 7
8 5 6
or
1 2 3
4 7 6
8 5
_
関数dist_between(x,y)
は、状態x
からy
に遷移するために発生した正方形の移動の数として定義されます。これは、アルゴリズムの目的上、常にA *の1に等しくなります。
closedset
とopenset
はどちらもA *アルゴリズムに固有であり、標準のデータ構造(私が信じる優先キュー)を使用して実装できます。_came_from
_は、ソリューションの再構築に使用されるデータ構造です。関数_reconstruct_path
_を使用して見つけた人の詳細は、ウィキペディアで見つけることができます。ソリューションを覚えたくない場合は、これを実装する必要はありません。
最後に、最適性の問題について説明します。 A *ウィキペディアの記事からの抜粋を検討してください。
「ヒューリスティック関数hが許容可能である場合、つまり、目標に到達するための実際の最小コストを過大評価しない場合、閉集合を使用しない場合、A *自体が許容可能(または最適)です。閉集合が使用される場合、 A *を最適化するには、hも単調(または一貫性)である必要があります。これは、隣接するノードxとyの任意のペア(d(x、y)はそれらの間のエッジの長さを表す)に対して、次の条件が必要であることを意味します。 h(x) <= d(x、y)+ h(y) "
したがって、ヒューリスティックが許容可能で単調であることを示すだけで十分です。前者(許容性)の場合、ヒューリスティック(すべての距離の合計)の構成では、各正方形は合法的な移動だけに制約されず、目標位置に向かって自由に移動できると推定されます。これは明らかに楽観的な推定であるため、ヒューリスティックです。許容されます(または、目標位置に到達すると、ヒューリスティックな推定と同じ数の移動が常に少なくともかかるため、過大評価されることはありません)。
言葉で述べられている単調性の要件は次のとおりです。「ノードのヒューリスティックコスト(目標状態までの推定距離)は、隣接ノードへの移行コストにそのノードのヒューリスティックコストを加えたもの以下である必要があります。」
これは主に、関係のないノードに移行すると、実際に移行を行うコストよりもゴールノードまでの距離が短くなり、ヒューリスティックが不十分になる可能性がある負のサイクルの可能性を防ぐためです。
単調性を示すために、私たちの場合は非常に単純です。隣接するノードx、yは、dの定義によりd(x、y)= 1になります。したがって、表示する必要があります
h(x)<= h(y) + 1
これは
h(x)-h(y) <= 1
これは
Σd(x私)-Σd(y私)<= 1
これは
Σd(x私)-d(y私)<= 1
neighbor_nodes(x)
の定義により、2つの隣接ノードx、yは、最大で1つの正方形の位置が異なる可能性があることがわかります。つまり、合計では、
d(x私)-d(y私)= 0
iの1つの値を除くすべて。一般性を失うことなく、これはi = kに当てはまるとしましょう。さらに、i = kの場合、ノードは最大で1つの場所に移動したため、目標状態までの距離は、前の状態よりも最大で1つ長くなければなりません。
Σd(x私)-d(y私)= d(xk)-d(yk)<= 1
単調性を示します。これは、表示する必要があるものを示しているため、このアルゴリズムが最適であることを証明します(big-O表記または漸近的な方法で)。
私はbig-O表記に関して最適性を示しましたが、ヒューリスティックを微調整することに関してはまだまだ多くの余地があることに注意してください。それにさらにねじれを追加して、目標状態までの実際の距離をより正確に見積もることができますただし作成する必要があります確実ヒューリスティックは常に過小評価であると、最適性が失われます!
これを(ずっと)後でもう一度読んで、私がそれを書いた方法がこのアルゴリズムの最適性の意味を混乱させるようなものであることに気づきました。
私がここで得ようとしていた最適性には、2つの明確な意味があります。
1)アルゴリズムは最適解を生成します。これは、客観的な基準が与えられた場合に可能な最良の解です。
2)アルゴリズムは、同じヒューリスティックを使用して、すべての可能なアルゴリズムの中で最小数の状態ノードを拡張します。
1)を取得するためにヒューリスティックの許容性と単調性が必要な理由を理解する最も簡単な方法は、A *を、これまでに移動したノード距離とヒューリスティックによってエッジの重みが与えられるグラフ上のダイクストラの最短パスアルゴリズムのアプリケーションとして表示することです。距離。これらの2つのプロパティがないと、グラフに負のエッジが存在するため、負のサイクルが発生する可能性があり、ダイクストラの最短経路アルゴリズムは正しい答えを返しません。 (これの簡単な例を作成して、自分を納得させてください。)
2)実際には理解するのがかなり混乱しています。これの意味を完全に理解するために、このステートメントには多くの数量詞があります。たとえば、他のアルゴリズムについて話すときは、ノードを拡張して事前情報なしで検索するsimilar
アルゴリズムをA *と呼びます(ヒューリスティック以外。)明らかに、オラクルや魔神など、道のあらゆる段階で答えを教えてくれる、ささいな反例を作成することができます。この声明を深く理解するために、 ウィキペディア の歴史セクションの最後の段落を読み、その注意深く述べられた文のすべての引用と脚注を調べることを強くお勧めします。
これにより、読者になる可能性のある人々の間で残っている混乱が解消されることを願っています。
数字の位置に基づくヒューリスティックを使用できます。つまり、各文字の目標状態からのすべての距離の合計が大きいほど、ヒューリスティック値が高くなります。次に、時間と空間の複雑さの観点から最適な検索であることが証明できるA *検索を実装できます(ヒューリスティックが単調で許容できる場合) http://en.wikipedia.org/wiki/A * _search_algorithm
OPは写真を投稿できないので、これは彼が話していることです:
このパズルを解く限り、 このページ によって8パズルの問題に関連するように 反復深化深さ優先探索 アルゴリズムを見てください。
ドーナツだよ!このパズルの検索スペースが比較的限られていることを考慮して、IDDFSがそのトリックを実行します。それは効率的であるため、OPの質問に答えます。それは最適な解決策を見つけるでしょうが、必ずしも最適な複雑さではありません。
IDDFSの実装は、この問題のより複雑な部分です。ボード、ゲームルールなどを管理するための簡単なアプローチを提案したいと思います。これは、特に、解決可能なパズルの初期状態を取得する方法に対応しています。質問のメモに示唆されているように、9つのタイツのすべてのランダムな割り当て(空のスロットを特別なタイルと見なす)が解決可能なパズルを生み出すわけではありません。それは数学的パリティの問題です...それで、ゲームをモデル化するための提案があります:
ゲームの有効な「動き」を表すすべての3x3順列行列のリストを作成します。このようなリストは、すべて0と2つの1を含む3x3のサブセットです。各マトリックスは、IDDFS検索ツリーで移動を追跡するのに非常に便利なIDを取得します。マトリックスの代わりに、タイル位置番号の2つのタプルを交換することもできます。これにより、実装が高速化される可能性があります。
このような行列を使用して、「勝利」状態から開始し、ランダムに選択された任意の数の順列を実行して、初期パズル状態を作成できます。初期状態が解けることを保証することに加えて、このアプローチはまた、与えられたパズルを解くことができる指標動きの数を提供します。
それでは、IDDFSアルゴリズムを実装して、[ジョーク] A + [/ joke]の割り当てを返しましょう...
これは、古典的な最短経路アルゴリズムの例です。最短経路についての詳細を読むことができます ここ および ここ 。
要するに、いくつかのグラフの頂点としてのパズルのすべての可能な状態を考えてください。移動するたびに状態が変わります。つまり、有効な移動はそれぞれグラフのエッジを表します。移動にはコストがないため、各移動のコストは1であると考えることができます。次のc ++のような擬似コードは、この問題に対して機能します。
{
int[][] field = new int[3][3];
// fill the input here
map<string, int> path;
queue<string> q;
put(field, 0); // we can get to the starting position in 0 turns
while (!q.empty()) {
string v = q.poll();
int[][] take = decode(v);
int time = path.get(v);
if (isFinalPosition(take)) {
return time;
}
for each valid move from take to int[][] newPosition {
put(newPosition, time + 1);
}
}
// no path
return -1;
}
void isFinalPosition(int[][] q) {
return encode(q) == "123456780"; // 0 represents empty space
}
void put(int[][] position, int time) {
string s = encode(newPosition);
if (!path.contains(s)) {
path.put(s, time);
}
}
string encode(int[][] field) {
string s = "";
for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) s += field[i][j];
return s;
}
int[][] decode(string s) {
int[][] ans = new int[3][3];
for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) field[i][j] = s[i * 3 + j];
return ans;
}
私の parallel15パズルの解決策の反復深化検索 (4x4の兄)については、このリンクを参照してください8パズルの。