それに対して、Judea Pearlによって考案されたScout探索[1]は、最悪の場合、αβ探索よりも遅くなることもあるが、多くの場合はαβ探索よりも速くなる手法である。
Scout探索はevalとtestの二種類の関数からなる。eval(node)はnodeのminmax値を求める関数で、まず一番左の子ノードのminmax値sを再帰的にevalを呼び出すことで求める。次に、maxノードの場合、左から2つ目以降の子ノードが先のminmax値sを超えているかどうかをtest関数を用いて求める。もし、超えていれば再帰的にeval関数を用いることでminmax値を求めsを更新する。minノードの場合も同様である。
αβ探索との違いはtest関数である。この関数はeval関数とは異なり値を超えているか、あるいは超えていないかだけを調べるため、子ノードを順に見ていく際に一つでも値が超えていた場合、そこでtrueを返すことができる。そうすることによって、通常のαβ探索以上に高速な探索が可能になる。
プログラムは次の通り。
abstract class ScoutPlayer[N <: Node[N]](val maxDepth: Int) extends Player[N] { override def play(ply: Int, node: N, last: Move): Move = { val (m, s) = evalMax(node, maxDepth) m } def evalMax(node: N, depth: Int): (Move, Int) = { if (depth == 0 || node.isTerminal) { return (Move.empty, score(node)) } var bestMove = Move.empty var s = Int.MinValue val moves = node.possibleMoves() bestMove = moves.head val n1 = node.play(bestMove).get s = evalMin(n1, depth - 1)._2 for (m <- moves.tail) { val n = node.play(m).get if (testGTMin(n, s, depth - 1)) { bestMove = m s = evalMin(n, depth - 1)._2 } } (bestMove, s) } def testGTMin(node: N, s: Int, depth: Int): Boolean = { if (depth == 0 || node.isTerminal) { return (score(node) > s) } val moves = node.possibleMoves() for (m <- moves) { val n = node.play(m).get if (!testGTMax(n, s, depth - 1)) { return false } } true } def testGTMax(node: N, s: Int, depth: Int): Boolean = { if (depth == 0 || node.isTerminal) { return (score(node) > s) } val moves = node.possibleMoves() for (m <- moves) { val n = node.play(m).get if (testGTMin(n, s, depth - 1)) { return true } } false } def evalMin(node: N, depth: Int): (Move, Int) = { if (depth == 0 || node.isTerminal) { return (Move.empty, score(node)) } var bestMove = Move.empty var s = Int.MaxValue val moves = node.possibleMoves() bestMove = moves.head val n1 = node.play(bestMove).get s = evalMax(n1, depth - 1)._2 for (m <- moves.tail) { val n = node.play(m).get if (testLTMax(n, s, depth - 1)) { bestMove = m s = evalMax(n, depth - 1)._2 } } (bestMove, s) } def testLTMax(node: N, s: Int, depth: Int): Boolean = { if (depth == 0 || node.isTerminal) { return (score(node) < s) } val moves = node.possibleMoves() for (m <- moves) { val n = node.play(m).get if (!testLTMin(n, s, depth - 1)) { return false } } true } def testLTMin(node: N, s: Int, depth: Int): Boolean = { if (depth == 0 || node.isTerminal) { return (score(node) < s) } val moves = node.possibleMoves() for (m <- moves) { val n = node.play(m).get if (testLTMax(n, s, depth - 1)) { return true } } false } def score(node: N): Int }実際にベンチマークをとってみると次のようになる。
depth name 2 3 4 5 6 7 8 9 10 negaalpha 13 117 139 866 1278 7055 11826 56216 102936 scout 17 122 184 1355 1746 10638 14448 90827 159611 , , test = end41.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 30 289 525 4332 6865 55580 78076 795144 637477 scout 31 147 523 1685 7813 16346 62773 259737 720301 , , test = end42.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 30 223 338 3747 4063 47325 48221 538729 579236 scout 46 208 507 1796 3929 17116 54538 242091 637888 , , test = end43.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 49 77 673 1388 10677 23115 115886 319520 1253254 scout 85 180 501 1792 5469 20568 75687 406273 1449598 , , test = end44.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 37 208 575 4023 6969 59839 53319 834028 541004 scout 51 181 831 1754 6878 14971 45735 135692 426776 , , test = end45.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 34 370 329 5777 2723 61868 26343 817688 226751 scout 36 262 313 2588 3061 24128 27437 184520 306221 , , test = end46.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 25 335 594 10135 11688 124574 152214 1237488 1574325 scout 25 269 600 4603 12058 52798 159862 639592 1366585 , , test = end47.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 47 77 788 843 14354 7810 191791 43789 1106182 scout 71 171 865 4122 15544 51052 170941 227355 710531 , , test = end48.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 29 313 470 6681 9337 89444 145149 1129818 1887604 scout 38 235 530 3214 9771 56977 123631 620781 1160128 , , test = end49.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 34 273 494 2408 6707 20749 125380 310137 1682987 scout 48 125 569 1718 7189 43022 168662 703102 1383906 , , test = end50.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 33 452 626 15963 10816 534240 202532 25385511 3547451 scout 42 325 711 5129 10956 102361 213611 1484040 3335089 , , test = end51.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 59 183 1160 2032 31843 24937 479486 347731 11632682 scout 97 391 1425 2848 20183 58913 516050 1038602 12026764 , , test = end52.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 63 204 992 1946 6498 22494 94032 247170 1771168 scout 78 404 966 2461 5686 30081 106730 390460 1331091 , , test = end53.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 43 350 1214 11389 29471 576664 332677 41483269 4314728 scout 58 239 1355 3738 26987 71788 377530 1003209 3523635 , , test = end54.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 23 239 586 2323 14222 22906 211911 292473 2569049 scout 23 125 385 2688 5905 21854 100181 502976 1062680 , , test = end55.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 49 322 841 5657 20688 82789 223240 1981849 3913589 scout 70 524 1076 7771 23559 105182 225428 1312618 4845125 , , test = end56.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 56 148 1532 2013 26438 37070 373227 380459 7594219 scout 92 466 1758 3716 15043 58080 226731 1140123 7096800 , , test = end57.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 38 289 458 5651 4485 107143 44017 1887772 575659 scout 60 145 528 1040 4698 13081 39672 105975 569878 , , test = end58.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 55 828 904 15508 15619 238957 206252 3275672 2451690 scout 84 298 1401 4369 22385 72483 292685 927379 3730359 , , test = end59.pos depth name 2 3 4 5 6 7 8 9 10 negaalpha 18 240 210 3747 2592 63628 31206 690918 271286 scout 20 150 221 1922 3015 24676 33356 283456 228431
実験結果から、速くなることもあれば、遅くなることもあることがわかる。
[1] Pearl. SCOUT: A Simple Game-Searching Algorithm with Proven Optimal Properties. AAAI (1980) pp. 143-145
0 件のコメント:
コメントを投稿