# 井字棋_minimax算法_alpha-beta剪枝 **Repository Path**: DaleGG/Tic-Tac-Toe ## Basic Information - **Project Name**: 井字棋_minimax算法_alpha-beta剪枝 - **Description**: 井字棋_minimax算法_alpha-beta剪枝 并且实现了4*4棋盘 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-06-03 - **Last Updated**: 2022-06-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 棋盘对弈 **MIN-MAX(minimax)算法 Alpha-Beta剪枝** ## 介绍 Minimax算法被广泛应用在棋类(零和游戏,回合制,双方都很聪明采用最优策略,完全信息)游戏中,是一种找出失败的最大可能性中的最小值的算法。是基于决策树和搜索的智能算法。 以“井字棋”为例 ![image-20220527115846518](https://s2.loli.net/2022/06/03/DHKU4GW6XMpvN7t.png) 我们通过穷举所有结果(或者有其他估值函数这里不展开) 并为输、赢、平局 打上标签(例如0表示平局,+1表示X赢) 对于X来说,目的是要上自己更可能地得到1(由于井字棋只有0,-1,1),X会尽量选取最大,所以MAX行就表示X要获得最大,反之O为MIN行,要选取最小。 由此,我们就可以从Terminal最终结果反推每层。 ----------- 井字棋多叉树深度比较深,这里我们用更简单的例子来说明 image-20220527114126570 圆圈为MAX层,力求结果最大,反之方形为MIN层,力求最小。 假设双方都选择了最优路径 1. 先手为根,力求最大会左走 2. 后手为方,依然左走 image-20220527114643470 最终游戏为60分 > 剪枝版见 alpha-beta剪枝 ## MinMax算法 (Java实现) 在已知叶子节点值的情况下,回溯生成每层的值,用到树形结构,递归。 ```java /** * 最基本 * 二叉树 minmax * @param node 当前节点 * @param who 区分先手后手 */ static int minmax(Node node, boolean who) { /*这是一棵完全二叉树*/ if (node.children[0] == null) { //叶子节点 直接返回 return node.value; } if (who) { int temp = Integer.MIN_VALUE; int value = minmax(node.left, !who);//向左孩子遍历,并换人 temp = Math.max(value, temp);//取左子树的"最大值" value = minmax(node.right, !who); temp = Math.max(value, temp);//取右子树的"最大值" node.value = temp; return temp; } else { int temp2 = Integer.MAX_VALUE; int value = minmax(node.left, !who);//向左孩子遍历,并换人 temp2 = Math.min(value, temp2);//取左子树的"最小值" value = minmax(node.right, !who);//向左孩子遍历,并换人 temp2 = Math.min(value, temp2);//取右子树的"最小值" node.value = temp2; return temp2; } } ``` 对代码简化 ```java /** * 多叉树 minmax 代码简化 */ static int minmax2(Node node, boolean who) { /*这是一棵完全二叉树*/ if (node.children[0] == null) { return node.value; } int temp = who ? Integer.MIN_VALUE : Integer.MAX_VALUE; /*改用数组实现多叉树*/ int value; for (int i = 0; i < 2 && node.children[i] != null; i++) { value = minmax2(node.children[i], !who);//孩子遍历,并换人 temp = who ? Math.max(value, temp) : Math.min(value, temp);//取右子树的"最大值" } node.value = temp; return temp; } ``` > 其他参考代码见尾 这里有一个很大的缺陷,对于局面复杂的问题(例如五子棋),需要很大的空间。以及部分节点跟最后的结果无关,无需展开局面和计算估值,所以我们需要剪枝。 ## alpha-beta剪枝 Alpha-beta(α − β)剪枝的名称来自计算过程中传递的两个边界,这些边界基于已经看到的搜索树部分来限制可能的解决方案集。 其中,Alpha(α)表示目前所有可能解中的最大下界,Beta(β)表示目前所有可能解中的最小上界。 因此,如果搜索树上的一个节点被考虑作为最优解的路上的节点(或者说是这个节点被认为是有必要进行搜索的节点),那么它一定满足以下条件(N 是当前节点的估价值): $$ α \leqslant N \leqslant β $$ 在我们进行求解的过程中,α 和β 会逐渐逼近。如果对于某一个节点,出现了α > β 的情况,那么,说明这个点一定不会产生最优解了,所以,我们就不再对其进行扩展(也就是不再生成子节点),这样就完成了对博弈树的剪枝。 还是用上面的例子: 1、 image-20220527211401413 初始参数alpha设为-∞ beta为+∞ ,按照递归顺序(其中叶子节点直接向上层返回),先处理左边正方形,更新最小值上界Beta为60。 2、 image-20220527212554772 递归返回到根,注意此时仍然是beta由+∞,更新最大值下界Alpha为 60(此时只受到左子树影响) 3、 image-20220527212939035 根向右递归,注意此时保留由根节点递归传来的alpha=60 beta=+∞ 参数,然后由孩子更新最小值上界Beta为15。此时违背alpha <= beta的条件,我们就可以对右子树,进行剪枝如下图(对于根节点已知最大下界至少为60,右子树存在不超过下界60时,如15,他一定会选择走左子树,所以对右子树多余部分可剪枝)。 image-20220528194708255 就这样,在规模更大的对弈中,我们将删除大量不需要的情况,依然能让我们进行最优的决策。 附上代码(java) ```java /** * 二叉树(完全),实现alpha-beta剪枝 minmax * * @param node 结点 * @param who 最大者为true 最小者为false * @param alpha 下界由最大者管理,若子树返回这大于alpha 则对其更新 * @param beta 上界由最小者管理---- */ static int ab_minmax1(Node node, boolean who, Integer alpha, Integer beta) { if (node.left == null) { return node.value; } if (who) { int temp = Integer.MIN_VALUE; int value = ab_minmax1(node.left, !who, alpha, beta);//向左孩子遍历,并换人 temp = Math.max(value, temp);//取左子树的"最大值" alpha = Math.max(alpha, temp); if (beta <= alpha) { node.right = null; } else { //剪枝了右边 就不用访问右边了 value = ab_minmax1(node.right, !who, alpha, beta); temp = Math.max(value, temp);//取右子树的"最大值" } node.value = temp; return temp; } else { int temp2 = Integer.MAX_VALUE; int value = ab_minmax1(node.left, !who, alpha, beta);//向左孩子遍历,并换人 temp2 = Math.min(value, temp2);//取左子树的"最小值" beta = Math.min(beta, temp2); if (beta <= alpha) { node.right = null; } else { //剪枝了右边 就不用访问右边了 value = ab_minmax1(node.right, !who, alpha, beta); temp2 = Math.min(value, temp2);//取右子树的"最小值" } node.value = temp2; return temp2; } } ``` ```java /** * 多叉树 alpha_beta剪枝 minmax */ static int ab_minmax3(Node node, boolean who, Integer alpha, Integer beta) { /*这是一棵完全二叉树*/ if (node.children[0] == null) { //若为叶子节点 直接返回值给上层 return node.value; } int temp = who ? Integer.MIN_VALUE : Integer.MAX_VALUE; /*改用数组实现多叉树*/ int value; for (int i = 0; i < node.children.length && node.children[i] != null; i++) { value = ab_minmax3(node.children[i], !who,alpha, beta);//孩子遍历,并换人 temp = who ? Math.max(value, temp) : Math.min(value, temp);//取孩子最大值和最小值 if (who) { //最大者改下限alpha alpha = Math.max(alpha, temp); //剪枝 if (beta <= alpha) { if (i + 1 < 2) { node.children[i + 1] = null; continue; } } } else { //最小者改上限beta beta = Math.min(beta, temp); if (beta <= alpha) { if (i + 1 < 2) { node.children[i + 1] = null; //剪掉后就不用继续访问下层了哈 continue; } } } } node.value = temp; return temp; } ``` ## 其他参考代码 树形结构定义 ```java class Node { Integer value; Node left; Node right; Node[] children; public Node(Integer value, Node left, Node right) { this.value = value; this.left = left; this.right = right; //此处多叉树用二叉树代替,读者可自行更改 this.children = new Node[]{left, right}; } @Override public String toString() { return value + " "; } } ``` 遍历方法 ```java /** * 多叉树遍历 * @param node */ static void bianli2(Node node) { if (node == null) { return; } //先根遍历 System.out.println(node); for (int i = 0; i < node.children.length; i++) { bianli2(node.children[i]); } } /** * 二叉树遍历 * @param node */ static void bianli(Node node) { if (node == null) { return; } //先序遍历 System.out.println(node); bianli(node.left); bianli(node.right); } ``` 主函数测试(实例为上文举例的简单例子) ```java public static void main(String[] args) { Node a = new Node(60, null, null); Node b = new Node(63, null, null); Node c = new Node(15, null, null); Node d = new Node(58, null, null); Node e = new Node(null, a, b); Node f = new Node(null, c, d); Node root = new Node(null, e, f); ab_minmax3(root, true, Integer.MIN_VALUE, Integer.MAX_VALUE); bianli2(root); } ``` ## 游戏 由此启发 设计了4字棋游戏 效果不错 ```java import java.util.Scanner; public class tic4 { static char[] chess = new char[16]; static Scanner input = new Scanner(System.in); public static void main(String[] args) { //初始化 StartGame(); //这里默认玩家先下棋 char who = 'x'; //当双方都没赢 并棋盘有空 游戏继续 while (!Win('x') && !Win('o') && !isFull()) { switch (who) { case 'x': playerGo(); who = 'o'; break; case 'o': computerGo(); who = 'x'; break; } } //最终的结果界面 显示结果 if (Win('o')) { System.out.println("Computer win!"); } else if (Win('x')) { System.out.println("player win!"); } else { System.out.println("it ends in a draw!"); } } private static void computerGo() { int best = 0; //记录最佳位置 int bestScore = -1000; int score; for (int i = 0; i < chess.length; i++) { if (chess[i] == '-') { chess[i] = 'o';//电脑尝试下这里 score = bestInput('x', -10000, 10000);//core if (score >= bestScore) { bestScore = score; System.out.println("电脑在"+i+"的分数:"+bestScore); best = i; } chess[i] = '-'; //只是试探并没下 重新置为空 } } //真正地下棋 chess[best] = 'o'; Print(); } /** * 核心 * * @param who 用于区分max 和 min * @param alpha * @param beta * @return */ private static int bestInput(char who, int alpha, int beta) { //递归先写结束 if (Win('o')) { //如果电脑赢了 (电脑取max) return 1; } else if (Win('x')) { return -1; } else if (isFull()) { //棋盘满了 平局 return 0; } else { int score; for (int i = 0; i < chess.length; i++) { if (chess[i] == '-') { chess[i] = who; //尝试计算下这一步 //并切换选手 继续计算 score = (who == 'x') ? bestInput('o', alpha, beta) : bestInput('x', alpha, beta); //因为是试下 所以要还原 chess[i] = '-'; if (who == 'o') { //max层 电脑 if (score > alpha) { alpha = score; } if (alpha >= beta) { //直接剪枝 返回给的是min ,min会选小也就是beta return beta; } } else { //min层 if (score < beta) { beta = score; } if (alpha >= beta) { //直接剪枝 返回 return alpha; } } } } //最终返回 if (who == 'o') return alpha; else return beta; } } private static void playerGo() { System.out.println("棋盘位置为0-15,请输入:"); int a = input.nextInt(); if (a >= 0 && a <= 15 && chess[a] == '-') { chess[a] = 'x'; Print(); } else { //输入错误给予提示 System.out.println("Your character input is wrong, please re-enter!"); playerGo(); } } public static void StartGame() { //初始化棋盘 for (int i = 0; i < chess.length; i++) { chess[i] = '-'; } System.out.println("开始游戏"); Print();//打印期盼 } /** * 打印棋盘 */ private static void Print() { System.out.println("**********"); for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { System.out.print("| "); System.out.print(chess[i * 4 + j] + " "); if (j == 3) System.out.println("|"); } if (i == 3) System.out.println("**********"); } } //判断输赢 public static Boolean Win(char player) { if (chess[0] == player && chess[1] == player && chess[2] == player && chess[3] == player) return true; if (chess[4] == player && chess[5] == player && chess[6] == player && chess[7] == player) return true; if (chess[8] == player && chess[9] == player && chess[10] == player && chess[11] == player) return true; if (chess[12] == player && chess[13] == player && chess[14] == player && chess[15] == player) return true; if (chess[0] == player && chess[4] == player && chess[8] == player && chess[12] == player) return true; if (chess[1] == player && chess[5] == player && chess[9] == player && chess[13] == player) return true; if (chess[2] == player && chess[6] == player && chess[10] == player && chess[14] == player) return true; if (chess[3] == player && chess[7] == player && chess[11] == player && chess[15] == player) return true; if (chess[0] == player && chess[5] == player && chess[10] == player && chess[15] == player) return true; if (chess[3] == player && chess[6] == player && chess[9] == player && chess[12] == player) return true; return false; } //判断棋盘是否还有地方能下棋子 public static boolean isFull() { //判断棋盘是否为空 for (int i = 0; i < chess.length; i++) { if (chess[i] == '-') return false; //棋盘还有位置可以下 不为空 } return true; } } ``` 效果如图: ![image-20220603221232909](https://s2.loli.net/2022/06/03/PWlx5p1trdLgHn2.png)