アルゴリズムとデータ構造
2012 年 7 月 23 日
酒居敬一 ([email protected])
http://www.info.kochi-tech.ac.jp/k1sakai/Lecture/ALG/2012/index.html
バックトラック法
(352ページ)
• 組織的かつ論理的なしらみつぶし解法
• 単純に全ての場合を試すのではなく、
問題の性質を考慮して無駄な計算を省く
• 例:n女王問題
– 盤面に女王を置ける場合の数は とおり
– しかし、ひとつの列に女王はひとつしか置けない
• とおりまで減らすことができる
– さらに、ひとつの行に女王はひとつしか置けない
n2
C
nn
n… …
…
…
…
…
…
…
3
1 2 3 4 5 n
1 2 3 4 n 1 2 3 4 n
1 2 3 n
…
…
…
…
1行目の女王の位置
2行目の女王の位置
3行目の女王の位置
×
×
×
× ×
×
×
×
×
深さ優先探索により、
すべての場合を調べるのではなく、
解の探索の途中で可能性の無い 枝を刈り払う → 枝刈り
図6.1.3 n女王問題の解の探索(354ページ)
女王が盤面の
(x1, y1)に居るとき、
array[x1]=y1 x1
y2 x2
y1 Java
(0, 0) x3
y3
(x2, y2)に 女王は置けない
↓
y1‐x1=y2‐x2が 成り立たないこと
minor[y1‐x1]=false
(x3, y3)に 女王は置けない
↓
x1+y1=x3+y3が 成り立たないこと
major[x1+y1]=false 各列には1個しか置けないので、horizontal[x1]=false
public class BackTrack {
private final boolean[] horizontal;
private final boolean[] major;
private final boolean[] minor;
private final int[] array;
private final StringBuffer hr = new StringBuffer();
private final StringBuffer queen = new StringBuffer();
public BackTrack(int n){
horizontal = new boolean[n];
major = new boolean[2*n - 1];
minor = new boolean[2*n - 1];
array = new int[n];
Arrays.fill(horizontal, true);
Arrays.fill(major, true);
Arrays.fill(minor, true);
for(int i = 0; i < n; i++) hr.append("+---");
hr.append('+');
for(int j = 0; j < n - 1; j++) queen.append("¦ ");
queen.append("¦ X ");
for(int j = 0; j < n - 1; j++) queen.append("¦ ");
queen.append('¦');
} }
5
左下がりの対角線上に置けるかどうか
1行に1個しか置けない ようにしたデータ構造 その列に置けるかどうか
右下がりの対角線上に置けるかどうか
盤面そのものを表す
特別なデータ構造はない!
private void backtrack(int level){
if(level >= horizontal.length){
for(int x: array){
System.out.println(hr);
System.out.append(queen, 4*x, 4*x + 4*array.length + 1);
System.out.println();
}
System.out.println(hr);
} else {
int row̲a = level;
int row̲i = level + horizontal.length - 1;
for(int i = 0; i < horizontal.length; i++){
if(horizontal[i] && major[row̲a + i] && minor[row̲i - i]){
horizontal[i] = false;
major[row̲a + i] = false;
minor[row̲i - i] = false;
array[row̲a] = i;
backtrack(level + 1);
horizontal[i] = true;
major[row̲a + i] = true;
minor[row̲i - i] = true;
} } } }
queenを置かなかったことにする
(後戻りするのでバックトラック法)
解の出力
横4文字・縦2文字で升目1つ
queenを置いてみる
新しいqueenの位置
すべての場合の盤面を
生成し検査するのでもない
(生成後検査法ではない)
枝刈り
+---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ X ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ X ¦ ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ X ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ X ¦ ¦ ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ ¦ ¦ X ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ X ¦ ¦ ¦ ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ ¦ ¦ ¦ X ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ ¦ X ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
+---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ ¦ ¦ X ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ X ¦ ¦ ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ X ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ X ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ ¦ X ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ X ¦ ¦ ¦ ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ X ¦ ¦ ¦ ¦ ¦ +---+---+---+---+---+---+---+---+
¦ ¦ ¦ ¦ ¦ ¦ ¦ X ¦ ¦ +---+---+---+---+---+---+---+---+
public static void main(String[] args) { for(String a: args){
int n;
try {
n = Integer.parseInt(a);
}catch(IllegalArgumentException e){
continue;
}
new BackTrack(n).backtrack(0);
} }
7
n=8の例
8-queen問題の解の一部
幅優先探索
(365ページ)
• 深さ優先探索は有用である
– 閉路のあるグラフでも深さ優先探索はできる
• グラフがメモリ上に存在しないときは 深さ優先探索が使えない
– 頂点を辿ったという印を付けられない
• 8パズルのように探索のためのグラフを
動的生成するときは、幅優先探索する
9
1 7 8
6 5 4
3
2 1
7 8
6 5 4
3
2 1
7 8
6 5 4
3
2 1
7 8
6 5 4
3 2
1
7 8 6 5 4
3 2
1
7 8 6 5 4
3 2
1 7
8 6 5 4
3 2
1 7
8 6 5 4
3 2
1 7
8 6 5 4
3 2
1 7
8 6
5 4
3 2
1 7
8 6
5 4
3 2
1 7
8 6 5 4
3 2
図6.2.2と図6.2.3 8パズルのグラフの一部
• 12手で一巡する閉路が存在する
• 各状態から作れる状態の数は2から4
• 全ての状態をメモリに置くには多い
• この場合の「多い」とはメモリ容量に対して
S1
S2
S3
初期状態
• 初期状態から生成できる新しい状態S1を求める
• 次にS1から新しい状態S2を求める
• ただし余分の状態は取り除く
• 初期状態へ戻るものも取り除く
• さらにS2からS3状態を… と順に生成を続ける
• 解となる状態が生成できたら終了
• このときSk状態を生成するためにSk-1とSk-2状態が必要
public class PuzzleBoard { private final int[] board;
private int hole = -1;
private static int size;
private final PuzzleBoard parent;
public PuzzleBoard(int[] new̲board){
if(size == 0){
size = (int)Math.sqrt((double)new̲board.length);
} // 例外処理は割愛 this.board = new̲board;
for(int i = 0; i < new̲board.length; i++){
if(new̲board[i] == 0){
hole = i;
}
} // 例外処理は割愛 this.parent = null;
}
private PuzzleBoard(PuzzleBoard current, int new̲hole){
this.board = current.board.clone();
this.board[current.hole] = this.board[new̲hole];
this.board[new̲hole] = 0;
this.hole = new̲hole;
this.parent = current;
} }
11
初期状態と最終状態の生成用
パズルの盤の定義(その1)
途中の状態の生成用
public boolean equals(Object obj) {
PuzzleBoard in = (PuzzleBoard)obj;
int[] array = in.board;
for(int i = 0; i < this.board.length; i++){
if(array[i] != this.board[i]){
return false;
}
} // 例外処理は割愛 return true;
}
public int hashCode() {
return board[0] * board[1] + this.hole; // 実行時間に大きく影響する }
public PuzzleBoard getParent() { return parent;
}
public String toString(){
StringBuffer sb = new StringBuffer();
int k = 0;
for(int i = 0; i < size; i++){
for(int j = 0; j < size; j++){
sb.append(this.board[k++]).append(' ');
}
sb.append('\n');
}
sb.append('\n');
12
結果の表示用
ハッシュテーブルを使うために equals()とhashCode()を実装
パズルの盤の定義(その2)
public static void generate(Collection<PuzzleBoard> from, Collection<PuzzleBoard> to, Collection<PuzzleBoard> other){
for(PuzzleBoard b: from){
int i = b.hole - size;
if(0 <= i){
PuzzleBoard new̲board = new PuzzleBoard(b, i);
if(!from.contains(new̲board)&&!to.contains(new̲board)&&!other.contains(new̲board)) to.add(new̲board);
}
i = b.hole + size;
if(i < b.board.length){
PuzzleBoard new̲board = new PuzzleBoard(b, i);
if(!from.contains(new̲board)&&!to.contains(new̲board)&&!other.contains(new̲board)) to.add(new̲board);
}
i = b.hole % size;
if(i != 0){
PuzzleBoard new̲board = new PuzzleBoard(b, b.hole - 1);
if(!from.contains(new̲board)&&!to.contains(new̲board)&&!other.contains(new̲board)) to.add(new̲board);
}
if(i != (size - 1)){
PuzzleBoard new̲board = new PuzzleBoard(b, b.hole + 1);
if(!from.contains(new̲board)&&!to.contains(new̲board)&&!other.contains(new̲board)) to.add(new̲board);
} } }
スペースを上に動かす
パズルの盤の定義(その3)
スペースを下に動かす
スペースを左に動かす
スペースを右に動かす
public class Puzzle {
private static int[] initial̲state = {5,3,6, 8,7,1, 2,0,4};
private static int[] final̲state = {0,1,2, 3,4,5, 6,7,8};
private static PuzzleBoard initial̲board = new PuzzleBoard(initial̲state);
private static PuzzleBoard final̲board = new PuzzleBoard(final̲state);
public static void main(String[] args) {
HashSet<PuzzleBoard> set1 = new HashSet<PuzzleBoard>();
HashSet<PuzzleBoard> set2 = new HashSet<PuzzleBoard>();
HashSet<PuzzleBoard> set3 = new HashSet<PuzzleBoard>();
HashSet<PuzzleBoard>[] aspect = new HashSet[]{set1, set2, set3}; // from, to, other aspect[1].add(initial̲board); // 初期状態
int step;
for(step = 1; !aspect[1].contains(final̲board); step++){ // 最終状態に到達するまで探索 HashSet<PuzzleBoard> tmp = aspect[0];
aspect[0] = aspect[1]; aspect[1] = aspect[2]; aspect[2] = tmp;
aspect[1].clear();
PuzzleBoard.generate(aspect[0], aspect[1], aspect[2]);
System.out.print(step + ": ");
System.out.println(aspect[1].size());
}
aspect[2].clear(); // ここから結果の表示
aspect[2].add(final̲board); // 最終状態だけからなるコレクション aspect[1].retainAll(aspect[2]); // 最終局面で最終状態だけ残す。
for(PuzzleBoard board: aspect[1]){
for(PuzzleBoard current = board; current != null; current = current.getParent()){
System.out.println("step: " + --step);
15
step: 6 5 3 6 2 1 0 7 8 4 step: 5 5 3 6 2 0 1 7 8 4 step: 4 5 3 6 2 8 1 7 0 4 step: 3 5 3 6 2 8 1 0 7 4 step: 2 5 3 6 0 8 1 2 7 4 step: 1 5 3 6 8 0 1 2 7 4 step: 0 5 3 6 8 7 1 2 0 4 1: 3
2: 5 3: 10 4: 14 5: 28 6: 42 7: 80 8: 108 9: 202 10: 278 11: 524 12: 726 13: 1348 14: 1804 15: 3283 16: 4193 17: 7322 18: 8596 19: 13930 20: 14713 21: 21721 22: 19827 23: 25132 24: 18197 25: 18978 26: 9929 27: 7359
step: 20 1 2 5 6 3 4 7 0 8 step: 19 1 2 5 6 3 4 7 8 0 step: 18 1 2 5 6 3 0 7 8 4 step: 17 1 2 5 6 0 3 7 8 4 step: 16 1 2 5 0 6 3 7 8 4 step: 15 0 2 5 1 6 3 7 8 4 step: 14 2 0 5 1 6 3 7 8 4 step: 27
0 1 2 3 4 5 6 7 8 step: 26 1 0 2 3 4 5 6 7 8 step: 25 1 2 0 3 4 5 6 7 8 step: 24 1 2 5 3 4 0 6 7 8 step: 23 1 2 5 3 0 4 6 7 8 step: 22 1 2 5 0 3 4 6 7 8 step: 21 1 2 5 6 3 4 0 7 8
step: 13 2 5 0 1 6 3 7 8 4 step: 12 2 5 3 1 6 0 7 8 4 step: 11 2 5 3 1 0 6 7 8 4 step: 10 2 5 3 0 1 6 7 8 4 step: 9 0 5 3 2 1 6 7 8 4 step: 8 5 0 3 2 1 6 7 8 4 step: 7 5 3 0 2 1 6 7 8 4
探索は10秒くらい
初期状態
最終状態 3
1 7 2 4 8
6 5
1 5 4 6 7 8 3
2
先手番
後手番
先手番
後手番
図 6.3.1 ゲームの木(の部分木だと考えてください)
ミニマックス法では、バックトラック法により木の葉から評価を決めていく。
ゲームの木の探索
(376ページ)
+1
-1
+1 -1
+1 -1
+1
+1 +1
+1
+1 -1
-1 -1
17
0
+1 -1
+1
+1 0 -1 -1
先手番
後手番
先
手 勝
後 手 勝 引
分
• ゲーム終了の状態に+1・0・‐1を与える
• ゲームの途中では自分に有利なほうの枝を辿る
• ゲームの木の途中の頂点の値を決定できる
• 手番が先手・後手に応じて最大・最小を選択
• 全手読みができれば
• 先手必勝・引き分け・後手必勝がわかる
• 全手読みは時間的にも空間的にも困難
• 全手読みが不可能な場合
• その局面での勝ちやすさ(負けやすさ)を求める
• 先手有利を正の数、後手有利を負の数…
• その数値を求める関数を評価関数という
• 先読みする深さを限定して評価する
• 確率的要素が入るゲームは、ここでは扱わない
• 完全情報ゲームのみを対象とする
18
S
A B
G F
E H
2 4 1 3 7 1 2 3
先手番
後手番
先手番
後手番 4以上
4以下なら打ち切り
4以下
4以上なら打ち切り
4 7以上 3
調べるだけ無駄
4
3以下• 数字は大きいほど先手に有利、つまり、小さいほど後手に有利
•
先手はより大きな数値を持つ方向を選ぶ
•
後手はより小さな数値を持つ方向を選ぶ
• 評価関数の値について
• 深さ制限つきのミニマックス法
• 一定の深さまで読んで、最大値もしくは最小値を選ぶ
• α-β法
3
調べるだけ無駄