minimax递归究竟是如何工作的?

时间:2012-07-28 19:07:30

标签: c++ recursion concept minimax

所以我在寻找一个Tic-Tac-Toe游戏的Mini-max,但是无法理解递归是如何工作的?好的,所以基本上我的问题是:

  1. 极小极大如何知道轮到谁了?什么是表明轮到它的球员的最佳方式?
  2. 你如何产生可能的动作?
  3. 您如何知道您何时在终端节点,以及如何生成终端节点?
  4. 例如在这个伪代码中

    function integer minimax(node, depth)
    if node is a terminal node or depth <= 0:
        return the heuristic value of node
    α = -∞
    for child in node: # evaluation is identical for both players
        α = max(α, -minimax(child, depth-1))
    return α
    

    node是正确的?并且代码在递归中的深度是多少?什么是max函数以及从哪里生成节点?

    现在,到目前为止,我已经有了创建电路板的代码:

    class Board{
        public:
            Board();
            ~Board(){};
        public: // The board
            // In the board, 1 is x, 2 is o, 0 is empty square.
            int board[3][3];
    };
    

    但我怎么知道轮到谁了?如何为电路板生成子节点?

3 个答案:

答案 0 :(得分:5)

我们首先以你的tic-tac-toe为例。

  • 极小极大算法最适合玩家轮流转换的游戏,但可以适应玩家每回合可以进行多次移动的游戏。为简单起见,我们假设前者。在这种情况下,您不需要为每个节点存储“X to move”或“O to move”,因为这可以通过节点深度的奇偶校验来确定(无论我是偶数步数,还是奇数从顶部开始的步骤数。
  • 从每个位置生成可能的移动需要您知道它的移动(可以像以前那样确定),以及合法移动到特定位置的规则。对于像tic-tac-toe这样的简单游戏,给定一个位置,它就足以枚举由当前位置的副本和属于当前玩家的新作品组成的所有状态,依次放置在每个空方块上。对于像奥赛罗这样的游戏,您还必须检查每个位置,以确保它遵循规则,并根据规则的后果更新最终位置(对于奥赛罗,翻转一堆碎片的颜色)。通常,从您跟踪的每个有效位置,您枚举新片段的所有可能位置,并检查规则集允许哪些位置。
  • 一般情况下,您永远不会生成整个树,因为游戏树的大小很容易超过地球的存储容量。您始终设置最大迭代深度。因此,终端节点只是最大深度的节点,或者不存在合法移动的节点(对于井字游戏,每个正方形填充的板)。您不预先生成终端节点;它们在游戏树构建过程中自然生成。 Tic-tac-toe很简单,你可以生成整个游戏树,但是不要试图使用你的井字游戏代码。奥赛罗。

查看你的伪代码:

  • max(a, b)是返回ab中较大者的任何函数。这通常由数学库或类似工具库提供。
  • depth是您要搜索的最大深度。
  • 您正在计算的启发式值是一些描述电路板价值的数值。对于像tic-tac-toe这样的游戏,它足够简单,您可以枚举整个游戏树,您可以为执行分析的玩家赢得一个棋盘位置1-1为其他玩家赢得的董事会职位,以及0任何不确定的职位。一般来说,你必须自己做一个启发式的方法,或者使用一个被广泛接受的方法。
  • 您可以在分析过程中根据父节点动态生成节点。您的根节点始终是您进行分析的位置。

如果你还没有使用图形或树木,我建议你先这样做;特别是树原语对于这个问题是必需的


作为对此线程中的注释的回答,请求确定给定节点的转向的示例,我提供了这个伪Python:

who_started_first = None

class TreeNode:
    def __init__(self, board_position = EMPTY_BOARD, depth = 0):
        self.board_position = board_position
        self.children = []
        self.depth = depth
    def construct_children(self, max_depth):
        # call this only ONCE per node!
        # even better, modify this so it can only ever be called once per node
        if max_depth > 0:

            ### Here's the code you're actually interested in.
            if who_started_first == COMPUTER:
                to_move = (COMPUTER if self.depth % 2 == 0 else HUMAN)
            elif who_started_first == HUMAN:
                to_move = (HUMAN if self.depth % 2 == 0 else COMPUTER)
            else:
                raise ValueError('who_started_first invalid!')

            for position in self.board_position.generate_all(to_move):
                # That just meant that we generated all the valid moves from the
                # currently stored position. Now we go through them, and...
                new_node = TreeNode(position, self.depth + 1)
                self.children.append(new_node)
                new_node.construct_children(max_depth - 1)

每个节点都能够跟踪“根”节点的绝对深度。当我们试图确定我们应该如何为下一步行动生成董事会职位时,我们会根据深度的平价(self.depth % 2的结果)以及我们先移动的记录来检查其移动情况。 / p>

答案 1 :(得分:2)

  

1)极小极大如何知道轮到谁了?什么是最好的方式来指示轮到它的球员?

你有depth个参数。如果深度是均匀的,那么它是一个玩家的回合,如果它是奇数,那么它是另一个玩家的回合。

  

2)你如何产生可能的动作?

使用游戏规则。在tic tac toe中,可能的移动意味着将一个标记放入一个空闲单元格中。

  

3)你怎么知道你在终端节点的时间,以及如何生成终端节点?

终端节点是某人获胜的节点。你通过递归生成它们。每个递归调用应该被赋予电路板的当前状态。我想这是伪代码中的nodechild参数。因此,如果在某种情况下有人赢了,那么它就是终点,否则你会尝试所有法律行动并递归。

答案 2 :(得分:2)

我可以提供一些关于你在寻找什么的想法,因为我为tic-tac-toe编写了一个minimax算法。

直接回答您的问题:

  1. 我的minimax算法没有确定。它接受了一个决定算法正在使用哪个玩家的论据。

  2. 知道要移动的玩家,循环遍历棋盘上的所有空白方块,并为每个方块生成一个节点,该节点包含该方块中当前玩家的标记。递归地从那里开始。

  3. 我使用了一个函数,该函数返回一个值,表明游戏是否结束,以及是抽奖还是赢。

  4. 我的基本算法做到了这一点:

    • 输入:要移动的玩家,以及棋盘的状态。
    • 找到主板上留下的所有空白区域。
      • 在玩家移动的情况下生成一块新棋盘。
      • 如果游戏结束,请生成一个包含游戏结果的节点。
      • 否则,运行算法,传入其他玩家和新棋盘,并生成一个具有对手理想移动结果的节点。
    • 确定哪个节点(移动)导致最坏的最坏情况。
    • 输出:最佳动作,以及有关游戏结果的信息。