アルゴリズム・データ構造 i 第9回 幅優先探索で8パ...

20
アルゴリズム・データ構造 I 9幅優先探索で8パズルを解く 名城大学理工学部情報工学科 山本修身

Upload: others

Post on 07-Jan-2020

1 views

Category:

Documents


0 download

TRANSCRIPT

アルゴリズム・データ構造 I 第9回幅優先探索で8パズルを解く

名城大学理工学部情報工学科山本修身

もう一つの探索方法:幅優先探索 (1)

データ構造:キュー(queue; FIFO: First In First Out)

2

取り出すときは左から入れるときは右から

a = []a.push(23)a.push(55)puts(a.shift())a.push(89)puts(a.shift())puts(a.shift())

235589

JavaScriptでは,配列にキューの機能がついている.データを入れる場合にはpush(), データを取り出す場合にshift()を用いる

復習

もう一つの探索方法:幅優先探索 (2)もう一つの木の辿り方:深さ優先探索では深いところまでまず,降りて行き,降りられなくなったら戻るという方法をとった.もう一つの方法は,根 (root) に近いところから順に見て行く方法である.木の深さが無限大でも探索することができる.

3

L L LA

L LA

L LA

L LA

A

L LA

A

L LA

L LA

A

A

幅優先探索:BFS (breadth first search)

復習

もう一つの探索方法:幅優先探索 (3)• 深さ優先探索が再帰的なプログラミングで実現できるのに対して,幅優先探索は繰り返しを用いて実現できる.

4

queue = [根のノード]

while (queue.length > 0){ node = queue.shift() node 個別の処理 nodeの子供をqueueに付け加える}

この方法ではqueueの長さが爆発する可能性がある

復習

8パズルとは

• 3x3のボードの上に1~8までの数の書かれたコマが置かれていて,一カ所だけ空いている(左下の図を参照).

• 空白のマスの周りにあるコマを空白のマスに移動させることにより,このパズルの状態を変化させることができる.

• このパズルの目的は,適当にコマを何回か移動させて,右下のようなゴール状態に変化させることである.

5

4 7 3

8 1

5 2 6

1 2

3 4 5

6 7 8

ゴール状態適当な初期状態

8パズルの状態と遷移を表現する

• 8パズルの状態を表現するには,それぞれの位置にどのコマが置かれているを表現すれば良い.そのために配列を用いる.

• 最終状態は,とりあえず [0, 1, 2, 3, 4, 5, 6, 7, 8] とする.

• また,遷移は空白の位置へ動かす駒の動く方向によって表現する.空白を中心として,空白に接するコマをどちらに動かすかで,r, l, d, u を用いて表現する.

6

2

3 5

4u

lr

d

8パズルを実行できる環境 (1)

• ここでは,8パズルの動きを表示できるプログラミング環境を用意している.

7

8パズルを実行できる環境 (2)

• この環境では2つの関数が用意されている.一つは

8

set_board_state(state)

であり,コマの配置を大きさ9の配列stateで指定する.もう一つの関数は,

play_moves("lluurrdd")

であり,空欄へ動かすコマの移動方向を示す文字列を引数として渡す.これらの関数による命令は,プログラムが実行された後に実行される.

8パズルをランダムに動かしてみる (1)• まず,パズルを動かす環境をつくる.

9

var statevar UP = 0var DOWN = 1var RIGHT = 2var LEFT = 3var dir = "udrl"

function find_zero(){ for (var i =0; i < 9; i++){ if (state[i] == 0) return i } return null}

function move(i){ var z = find_zero() var ix = z % 3 var iy = Math.floor(z / 3) if (i == DOWN && iy > 0){ state[z] = state[z - 3] state[z - 3] = 0 } else if (i == UP && iy < 2){ state[z] = state[z + 3] state[z + 3] = 0 } else if (i == RIGHT && ix > 0){ state[z] = state[z - 1] state[z - 1] = 0 } else if (i == 3 && ix < 2){ state[z] = state[z + 1] state[z + 1] = 0 }}

8パズルをランダムに動かしてみる (2)• 実際に動かす関数を作る.

10

function make_random_state(N){ state = [0, 1, 2, 3, 4, 5, 6, 7, 8] var last_move = null var res = "" for (var n = 0; n < N; n++){ var moves = next_move_list(state, last_move) var m = Math.floor( Math.random() * moves.length) res = res + dir[moves[m]] move(moves[m]) last_move = moves[m] } return res}

配列 moves に現時点で動くことが可能な方向を入れて,その中からランダムに1つ適当な方向を選ぶ

↓初期状態

最後に動かした履歴を文字列として返す

function next_move_list(pat, last_move){ var moves = [] var z = find_zero() var ix = z % 3 var iy = Math.floor(z / 3) if (ix > 0 && last_move != LEFT)moves.push(RIGHT) if (ix < 2 && last_move != RIGHT) moves.push(LEFT) if (iy > 0 && last_move != UP)moves.push(DOWN) if (iy < 2 && last_move != DOWN)moves.push(UP) return moves }

8パズルをランダムに動かしてみる (3)実際に動かした最終状態から逆戻しに初期状態まで動かしてみる

11

function work(){ var res = make_random_state(200) var transform = function (c){ if (c == 'u') c = 'd' else if (c == 'd') c = 'u' else if (c == 'r') c = 'l' else if (c = 'l') c = 'r' return c } set_board_state(state) resx = "" for (var i = 0; i < res.length; i++) resx = transform(res[i]) + resx play_moves(resx)}

work()

逆戻しの動きを作る

←200回ほどランダムにコマを動かす

←ボードに最後の状態をセットする

←文字列を入れ換えてひっくり返す←動きのアニメーションを表示

8パズルをランダムに動かしてみる (4)

• 以下のように変化して初期状態に戻すことができる

12

200ステップ

ururdlulddruurdldluurrddluldruldrruulddruullddrruuldlurrddllurrulddluurrddllurdruldluurrddllurrullddrurdlurdlurdlluruldrrdlluurdlurrdlulddruldrruuldrdlurullddruurdlurdllurdrdluulddruulddrruuldrullddrr

幅優先探索で8パズルを解く (1)幅優先探索で8パズルを解くには,まず,それぞれのノードをどのように表現するかを考える必要がある.

• 明らかにそれぞれの状態はそれぞれのコマの配置を表現しなければならない.

• さらにどのような動作で,その状態に至ったかがわからないといけない.

• また,自分の親の状態が何なのかを知る必要がある

13

6 1 2

3 8 5

7 4

6 1 2

3 8 5

7 4

6 1 2

8 5

3 7 4

LEFT DOWN

[6,1,2,3,8,5,0,7,4]

[6,1,2,3,8,5,7,0,4] [6,1,2,0,8,5,3,7,4]

幅優先探索で8パズルを解く (2)

ここではノードを以下のように表現する:

[LEFT, [6,1,2,3,8,5,7,0,4], 親のノード]

14

6 1 2

3 8 5

7 4

LEFT

[6,1,2,3,8,5,7,0,4]

幅優先探索を用いるために,このように表現されたノードをキューに入れて処理する.純粋に幅優先探索を用いて,それ以上の手がかりを使うことは考えない.【実はコマの配置から解に近そうなノードとそうでないノードを区別することが可能である.そのような情報を用いて高速に解を導く方法が考えられる】

幅優先探索で8パズルを解く (3)• いくつかの部品を改良する.大域変数stateをなくす

15

function find_zero(state){ for (var i =0; i < 9; i++){ if (state[i] == 0) return i } return null}

function move(state, i){ state = state.slice(0) var z = find_zero(state) var ix = z % 3 var iy = Math.floor(z / 3) if (i == DOWN && iy > 0){ state[z] = state[z - 3] state[z - 3] = 0 } else if (i == UP && iy < 2){ state[z] = state[z + 3] state[z + 3] = 0 } else if (i == RIGHT && ix > 0){ state[z] = state[z - 1] state[z - 1] = 0 } else if (i == 3 && ix < 2){ state[z] = state[z + 1] state[z + 1] = 0 } return state}

function next_move_list(state, last_move){ var moves = [] var z = find_zero(state) var ix = z % 3 var iy = Math.floor(z / 3) if (ix > 0 && last_move != LEFT) moves.push(RIGHT) if (ix < 2 && last_move != RIGHT) moves.push(LEFT) if (iy > 0 && last_move != UP) moves.push(DOWN) if (iy < 2 && last_move != DOWN) moves.push(UP) return moves }state上のコマを動かす

空白の位置を探す

次に動かせる方向のリストを作る→

幅優先探索で8パズルを解く (4)• さらにいくつかの部品を追加する

16

function make_random_state(N, state){ var last_move = null for (var n = 0; n < N; n++){ var moves = next_move_list(state, last_move) var m = Math.floor(Math.random() * moves.length) state = move(state, moves[m]) last_move = moves[m] } return state}

function make_node(dir, pat, parent){ return [dir, pat, parent]}

function eq_pat(pat1, pat2){ for (var i = 0; i < 9; i++){ if (pat1[i] != pat2[i]) return 1 } return 0}

N回ランダムに動かして新たな配置を作る

ノードを作る

パターンが等しいかどうかを調べる

幅優先探索で8パズルを解く (5)

• さらに部品を追加する

17

function encode(pat){ var s = 0 for (var i = 0; i < 9; i++) s = s * 9 + pat[i] return s}

function in_list(a, lst){ for(var i = 0; i < lst.length; i++){ if (a == lst[i]) return true } return false}

コマの配置を数で表現する

数のリスト lst の中に数 a が存在するかどうかを調べる

幅優先探索で8パズルを解く (6)• 幅優先探索でパズルを解く

18

function work(){ var state1 = [0, 1, 2, 3, 4, 5, 6, 7, 8] var state = make_random_state(40, state1) puts(state) var mm = [] var queue = [make_node(null, state, null)] while (queue.length > 0){ var node = queue.shift() if (eq_pat(node[1], state1) == 0) break var mlist = next_move_list(node[1], node[0]) for (var i = 0; i < mlist.length; i++){ var st = move(node[1], mlist[i]) if (in_list(encode(st), mm)) continue var node1 = make_node(mlist[i], st, node) mm.push(encode(st)) queue.push(node1) } }

action = "" while (true){ if (node[0] == null) break action = dir[node[0]] + action node = node[2] } puts(action)}

ランダムなパターンを作る

初期のキューを作る

一度見つかった配置が再度現れたらキューに入れない

1回出現したパターンを蓄える

• 移動回数が最も少ない解を見つける

幅優先探索で8パズルを解く (7)• 幅優先探索を純粋に実行すると計算量が大きくなりすぎる.そのために,一度出て来たパターンは2回目は使わないようにする.

19

より根に近いものがあれば,遠いものは調べる必要がない.

if (in_list(encode(st), mm)) continue var node1 = make_node(mlist[i], st, node) mm.push(encode(st))

同じパターン

幅優先探索ではこちらの方が必ず先に出現する

0

1

2

3

幅優先探索で8パズルを解く (8)• 結果の取り出し

20

A

r根 (root)

終了状態

u

l

d

修了状態が得られたら,そこから根にむけてサーチし,根からの動きを出力する.

“ruld”

B

C

D

E

A=[null, p1, null]

B= [‘r’, p2, A]

C= [‘u’, p2, B]

D= [‘l’, p3, C]

E= [‘d’, p4, D]

それぞれのコマの配置を出力する必要はない.初期状態(根におけるコマの配置)がわかっていれば,そこから作り出すことができる.

action = "" while (true){ if (node[0] == null) break action = dir[node[0]] + action node = node[2] } puts(action)