先说说极大极小算法,是指给可能出现的所有状态赋予一个评估值,两个玩家通过计算不同下棋策略对应不同的评估值,来决定如何下棋。对于井字棋游戏来说,它的博弈树(各种走法组合形成的树)如下:
Alice(MAX)下X,Bob(MIN)下O,直到到达了树的终止状态即一位棋手占领一行,一列、一对角线或所有方格都被填满。Utility指效用函数,定义游戏者在状态S下的数值。在这道题中,就是指:
- 对于Alice已经获胜的局面,评估得分为(棋盘上的空格子数+1);
- 对于Bob已经获胜的局面,评估得分为 -(棋盘上的空格子数+1);
- 对于平局的局面,评估得分为0;
所以,在上图策略树中,无论当前局势如何,Alice(MAX)总会选择最大的评估分对应的走法,Bob(MIN)总会选择最小的评估分对应的走法。这样才能使自己尽快的赢得比赛(这一点是关键,要想清楚)。题目中只给出了策略树中叶子节点的评估分的计算方法(赢,输或平局情况的评估分计算方法),那如何计算策略树中每个非叶子节点对应的评估分值呢?
答案是采用深度优先搜索对整个策略树进行后序遍历,这样,先计算策略树中叶子节点的评估值,在一层层的往上计算非叶子节点的评估值,最终,会得到整个策略树的评估值,这样就可以确定玩家在当前情况下应该如何走棋了。
根据以上思路:
#include <bits/stdc++.h> using namespace std; int q[10]; int checkok(){ int i1, i2, ok = 0; for(i1 = 1; i1 <= 3; i1++) { i2 = 3 * (i1 - 1); if ((q[i1] == q[i1 + 3])&&(q[i1 + 3] == q[i1 + 6]) && (q[i1] != 0)){ if(q[i1] == 1) ok = 1; else ok = 2; break; } if ((q[i2 + 1] == q[i2 + 2]) &&(q[i2 + 2] == q[i2 + 3]) && (q[i2 + 1] != 0)){ if(q[i2 + 1] == 1) ok = 1; else ok = 2; break; } } if( (!ok) && ((q[1] == q[5]) && (q[5] == q[9]) && (q[1] != 0)) ) if(q[1] == 1) ok = 1; else ok = 2; if( (!ok) && (q[3] == q[5]) && (q[5] == q[7]) && (q[3] != 0)) if(q[3] == 1) ok = 1; else ok = 2; i2 = 0; for(i1 = 1; i1 <= 9; i1++) if(q[i1] == 0) i2++; if(ok == 1) return (i2 + 1); else if(ok == 2) return -(i2 + 1); else if(i2 == 0) return 0; else return 100; } int dfs(int turn){ int value = checkok(); if(value != 100) return value; int i1,i2; if(turn == 1) i2 = -100; else i2 = 100; for(i1 = 1; i1 <= 9; i1++){ if(q[i1] != 0) continue; if(turn == 1){ q[i1] = 1; i2 = max(i2, dfs(0)); }else{ q[i1] = 2; i2 = min(i2, dfs(1)); } q[i1] = 0; } return i2; } int main() { //freopen("a.txt", "r", stdin); int T,i1,i2; scanf("%d", &T); while(T--) { for(i1 = 1; i1 <= 9; i1++){ scanf("%d", &i2); q[i1] = i2; } printf("%d\n", dfs(1)); } return 0; }
可以得到100分的答案。
还要说的是井字棋策略树下,可以完全遍历,直接使用极大极小算法,面对更复杂的棋时,还需要采用α-β剪枝等更高级的方法。
参考书籍:人工智能——一种现代方法。