2014.07.08 22:43
简介:
“搜索”与“剪枝”几乎是如影随形的。此处的“搜索”指的是带有回溯算法的深度优先搜索。
在之前的“Minimax策略”中我们给出了一个三连棋的程序,运行后你就知道计算一步棋要花多少时间。
为了计算最优的一步棋,我们可能需要递归9万多次。毫无疑问这种阶乘式的穷举过程必须通过剪枝来加速。
本篇介绍一种用于Minimax策略的剪枝思路——α-β剪枝。
剪枝的英语是pruning,所以不要想当然说成trimming。
图示:
在上一篇讲解Minimax策略的博文中我们给出了一棵博弈树:
如果按照这棵树的形状来看,延伸下去应该会造成指数级的时间增长。
那么如果我们想做到“全知全能”,就得在下第一步棋的时候穷举所有可能的结局。因此你走一步棋就得推理9万多次。
按照这种脑力消耗,就是诸葛亮也吃不消,于是我们有两种办法可以偷懒:
1. 截断:我的脑容量没那么大,所以我只考虑三步棋以内的结果。三步棋之外的结果,姑且不管了。
2. 剪枝:有些结局实际上根本不需要分析,因为我们有充分的理由提前放弃那条路径。
本次不考虑截断的问题,只谈剪枝。
我们将刚才那棵博弈树中的两个结点标记为红色:
此处我们强调两条性质:
1. 对于Min层的每个节点,它的任何子节点的值都不小于它自身。
2. 对于Max层的每个节点,它的任何子节点的值都不大于它自身。
这棵树中78和36两个结点下方的节点不会被访问到,因为没有必要。
注意78所处的是Min层,对手为了使我的利益最小化,会尽力找出最小值。而68比78小,因此在78的下方不可能找到小于68的节点。所以没有必要继续深入78中。
36的道理相同,只不过处于Max层,所以解释自然是相反的。
在每一层都可能会有这样的节点,因此随时留意这样的剪枝机会,能极大地提高效率。
如果我没记错,经过这种剪枝的程序最多只需要递归一百多次,和9万多次相比确实快了不少。
因为Min和Max层交替出现,因此这种剪枝也需要两套代码逻辑,分别称为α剪枝与β剪枝。二者合称α-β剪枝。
教材中只给出了其中一个剪枝的伪代码,如果你没想明白,肯定写不出另一个的。
接下来请运行代码吧,看看效率上的差异。不过可能的话,我还是建议你在不看代码的情况下,自己修改三连棋的程序来实现α-β剪枝。
别忘了确保程序的正确性:如果你赢了电脑,那程序肯定是错的。
(提问:对于一个没有必胜策略的游戏,完美的AI是无法打败的,你只能打平手或者输给它,是这样吗?)
实现:
1 // Optimization for Minimax game strategy, using Alpha-Beta Pruning. 2 // You can watch over the 'function_call_count' variable. 3 #include <iostream> 4 #include <vector> 5 using namespace std; 6 7 int function_call_count; 8 9 bool computerWin(const vector<int> &board) 10 { 11 int i, j; 12 13 for (i = 0; i < 3; ++i) { 14 for (j = 0; j < 3; ++j) { 15 if (board[i * 3 + j] != -1) { 16 break; 17 } 18 } 19 if (j == 3) { 20 return true; 21 } 22 } 23 24 for (i = 0; i < 3; ++i) { 25 for (j = 0; j < 3; ++j) { 26 if (board[j * 3 + i] != -1) { 27 break; 28 } 29 } 30 if (j == 3) { 31 return true; 32 } 33 } 34 35 if (board[0] == board[4] && board[4] == board[8] && board[8] == -1) { 36 return true; 37 } 38 39 if (board[2] == board[4] && board[4] == board[6] && board[6] == -1) { 40 return true; 41 } 42 43 return false; 44 } 45 46 bool humanWin(const vector<int> &board) 47 { 48 int i, j; 49 50 for (i = 0; i < 3; ++i) { 51 for (j = 0; j < 3; ++j) { 52 if (board[i * 3 + j] != 1) { 53 break; 54 } 55 } 56 if (j == 3) { 57 return true; 58 } 59 } 60 61 for (i = 0; i < 3; ++i) { 62 for (j = 0; j < 3; ++j) { 63 if (board[j * 3 + i] != 1) { 64 break; 65 } 66 } 67 if (j == 3) { 68 return true; 69 } 70 } 71 72 if (board[0] == board[4] && board[4] == board[8] && board[8] == 1) { 73 return true; 74 } 75 76 if (board[2] == board[4] && board[4] == board[6] && board[6] == 1) { 77 return true; 78 } 79 80 return false; 81 } 82 83 bool fullBoard(const vector<int> &board) 84 { 85 for (int i = 0; i < 9; ++i) { 86 if (board[i] == 0) { 87 return false; 88 } 89 } 90 91 return true; 92 } 93 94 void findComputerMove(vector<int> &board, int &best_move, int &result, 95 int alpha, int beta) 96 { 97 void findHumanMove(vector<int> &, int &, int &, int, int); 98 int dc, i, response; 99 100 ++function_call_count; 101 best_move = -1; 102 103 if (fullBoard(board)) { 104 result = 0; 105 return; 106 } 107 108 if (computerWin(board)) { 109 result = -1; 110 return; 111 } 112 113 result = alpha; 114 for (i = 0; i < 9 && result > beta; ++i) { 115 if (board[i] != 0) { 116 continue; 117 } 118 board[i] = -1; 119 findHumanMove(board, dc, response, result, beta); 120 board[i] = 0; 121 122 if (best_move == -1 || response < result) { 123 result = response; 124 best_move = i; 125 } 126 } 127 } 128 129 void findHumanMove(vector<int> &board, int &best_move, int &result, int alpha, 130 int beta) 131 { 132 void findComputerMove(vector<int> &, int &, int &, int, int); 133 int dc, i, response; 134 135 ++function_call_count; 136 best_move = -1; 137 138 if (fullBoard(board)) { 139 result = 0; 140 return; 141 } 142 143 if (humanWin(board)) { 144 result = 1; 145 return; 146 } 147 148 result = beta; 149 for (i = 0; i < 9 && result < alpha; ++i) { 150 if (board[i] != 0) { 151 continue; 152 } 153 board[i] = 1; 154 findComputerMove(board, dc, response, alpha, result); 155 board[i] = 0; 156 157 if (best_move == -1 || response > result) { 158 result = response; 159 best_move = i; 160 } 161 } 162 } 163 164 void printBoard(const vector<int> &board) 165 { 166 cout << " 1 2 3" << endl; 167 int i, j; 168 169 for (i = 0; i < 3; ++i) { 170 cout << i + 1; 171 for (j = 0; j < 3; ++j) { 172 cout << ' '; 173 switch(board[i * 3 + j]) { 174 case -1: 175 cout << 'X'; 176 break; 177 case 0: 178 cout << '.'; 179 break; 180 case 1: 181 cout << 'O'; 182 break; 183 } 184 } 185 cout << endl; 186 } 187 } 188 189 int main() 190 { 191 vector<int> board; 192 int n; 193 int result; 194 195 board.resize(9, 0); 196 while (cin >> n) { 197 if (n < 0 || n >= 9 || board[n]) { 198 cout << "Invalid move" << endl; 199 continue; 200 } 201 202 board[n] = 1; 203 printBoard(board); 204 if (humanWin(board)) { 205 cout << "You win." << endl; 206 break; 207 } 208 209 if (fullBoard(board)) { 210 cout << "Draw." << endl; 211 break; 212 } 213 214 result = 1; 215 function_call_count = 0; 216 findComputerMove(board, n, result, 1, -1); 217 cout << "Number of function calls: " << function_call_count << endl; 218 board[n] = -1; 219 printBoard(board); 220 if (computerWin(board)) { 221 cout << "Computer win." << endl; 222 break; 223 } 224 225 if (fullBoard(board)) { 226 cout << "Draw." << endl; 227 break; 228 } 229 } 230 231 return 0; 232 }