先說說極大極小算法,是指給可能出現的所有狀態賦予一個評估值,兩個玩家通過計算不同下棋策略對應不同的評估值,來決定如何下棋。對於井字棋游戲來說,它的博弈樹(各種走法組合形成的樹)如下:
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分的答案。
還要說的是井字棋策略樹下,可以完全遍歷,直接使用極大極小算法,面對更復雜的棋時,還需要采用α-β剪枝等更高級的方法。
參考書籍:人工智能——一種現代方法。