Skip to content
AI360Xpert
Beta

Monte Carlo Tree Search (MCTS)

Monte Carlo Tree Search balances deep tactical analysis with quick random skirmishes, exploring promising futures deeply while ignoring dead ends in massive decision spaces.

Monte Carlo Tree Search executes four repeating phases—Selection, Expansion, Simulation, and Backpropagation—to grow an asymmetric search tree focused on the most promising lines of play.
Monte Carlo Tree Search executes four repeating phases—Selection, Expansion, Simulation, and Backpropagation—to grow an asymmetric search tree focused on the most promising lines of play.

Why Does This Exist?

In sequential decision-making domains with enormous branching factors—such as Go (1017010^{170} states), chess, complex robotics, and automated theorem proving—classical search algorithms fail:

  • Minimax with Alpha-Beta Pruning: Demands hand-crafted static evaluation functions to score intermediate, non-terminal board states. In games like Go, writing an accurate heuristic evaluation function by hand proved nearly impossible for decades.
  • Breadth-First and Uniform Search: Suffers an immediate combinatorial explosion, expanding billions of hopeless branches equally.
  • Pure Monte Carlo Rollouts: Plays random games from the current position to estimate win probabilities, but completely ignores tactical lookahead, blundering into obvious one-move traps.

Monte Carlo Tree Search (MCTS) was developed to solve this dilemma. Rather than requiring domain-specific evaluation heuristics or searching the entire game tree uniformly, MCTS constructs an asymmetric search tree.

It uses a mathematical exploration-exploitation trade-off (Upper Confidence bounds for Trees, or UCT) to guide search iterations toward the most promising variations, while using fast random rollouts (simulations) to evaluate leaf positions without any static evaluation function. By growing deeper along promising lines and leaving unpromising branches untouched, MCTS scales effectively to vast state spaces, forming the algorithmic core of landmark systems like AlphaGo and modern planning engines.

Think of It Like This

The Military War Room: Detailed Tactical Maps vs. Fast Wargame Skirmishes

Imagine a high-command general staff planning a decisive military operation:

  • Minimax (The Static Rulebook): You try to compute the value of every single hill and trench using a rigid scorebook written 50 years ago. If the scoring formulas don't account for modern air power or morale, the entire plan collapses.
  • Pure Random Sampling (Throwing Darts): You roll dice to choose random battle plans from scratch, never building on the insights gained from yesterday's plans.
  • Monte Carlo Tree Search (The War Room Staff):
    1. Selection: The generals review their tactical planning map (the search tree). They identify the operation plan that currently looks strongest while checking any alternatives that haven't been adequately examined.
    2. Expansion: When they reach the edge of their detailed map, they sketch out one plausible new tactical move.
    3. Simulation: To evaluate this new tactical move without drafting 500 pages of logistical charts, junior officers run a 10-minute tabletop wargame, rolling dice and simulating combat down to victory or defeat.
    4. Backpropagation: The outcome (victory or defeat) is phoned back to the planning table. The statistics on that branch are updated.

After repeating this process 10,000 times, the map has grown deep along brilliant tactical paths, while suicidal maneuvers were discarded after a single skirmish. When real orders are issued, the general picks the move that was stress-tested the most times.

Where the analogy stops: MCTS tracks exact visit counts N(s,a)N(s, a) and mean value bounds Q(s,a)Q(s, a) governed by a rigorous logarithmic multi-armed bandit formula (UCT).

How It Actually Works

The Four Canonical Phases and the UCT Tree Policy

MCTS builds a tree where nodes represent environment states ss and directed edges represent actions aa. Each node maintains two statistics:

  • N(s,a)N(s, a): The number of times action aa was selected from state ss.
  • Q(s,a)Q(s, a): The estimated value (e.g., average win rate or return) of taking action aa in state ss: Q(s,a)=1N(s,a)∑i=1N(s,a)RiQ(s, a) = \frac{1}{N(s, a)} \sum_{i=1}^{N(s, a)} R_i

Each search iteration consists of four canonical phases executed sequentially:

1. Selection

Starting at the root node (the current real game state), the algorithm traverses down the search tree by repeatedly selecting the action that maximizes the Upper Confidence bounds applied to Trees (UCT) score:

a∗=arg⁡max⁡a∈A(s)(Q(s,a)+cln⁡N(s)N(s,a))a^* = \arg\max_{a \in \mathcal{A}(s)} \left( Q(s, a) + c \sqrt{\frac{\ln N(s)}{N(s, a)}} \right)

Where:

  • Q(s,a)∈[0,1]Q(s, a) \in [0, 1] is the exploitation term, rewarding moves with historically high success.
  • cln⁡N(s)N(s,a)c \sqrt{\frac{\ln N(s)}{N(s, a)}} is the exploration bonus, derived from the UCB1 multi-armed bandit algorithm.
  • N(s)=∑aN(s,a)N(s) = \sum_a N(s, a) is the total visit count of the parent node.
  • c>0c > 0 is the exploration constant, theoretically set to c=2≈1.4142c = \sqrt{2} \approx 1.4142 to balance exploration against exploitation.
  • If an action has never been visited (N(s,a)=0N(s, a) = 0), its UCT score is set to +∞+\infty, ensuring that all candidate actions are tried at least once before any action is visited twice.

Selection continues until the traversal reaches a node that is not fully expanded (i.e., it has available actions that have not yet been added to the tree) or a terminal state.

2. Expansion

Unless the selected leaf node is already terminal, the algorithm selects one of its unexpanded actions, creates a new child node corresponding to the resulting transition state s′s', and attaches it to the tree. The new node is initialized with: N(s′)=0,Q(s′)=0.0N(s') = 0, \quad Q(s') = 0.0

3. Simulation (Rollout)

From the newly expanded node s′s', a fast simulation is executed until a terminal state is reached. Moves during this phase are chosen by a computationally lightweight default rollout policy (often uniform random sampling or a simple rule-based heuristic): a∼πdefault(⋅∣s)a \sim \pi_{\text{default}}(\cdot \mid s)

This simulation requires no tree expansion. At the conclusion of the rollout, the environment yields a terminal reward RR (e.g., +1+1 for a win, 00 for a draw, −1-1 for a loss).

4. Backpropagation

The terminal reward RR is propagated backward along the exact sequence of nodes traversed during the Selection and Expansion phases:

  • For every node (s,a)(s, a) along the path, its visit count is incremented: N(s,a)←N(s,a)+1N(s, a) \leftarrow N(s, a) + 1
  • Its value estimate is updated incrementally to reflect the running sample average: Q(s,a)←Q(s,a)+R−Q(s,a)N(s,a)Q(s, a) \leftarrow Q(s, a) + \frac{R - Q(s, a)}{N(s, a)}

5. Decision Execution

Once the allocated computational budget (e.g., 1,000 iterations or 100 milliseconds) expires, the tree search terminates. The agent selects the real-world action to play.

Crucially, the agent chooses the action with the highest visit count, not necessarily the highest QQ-value: areal=arg⁡max⁡aN(root,a)a_{\text{real}} = \arg\max_a N(\text{root}, a)

Because UCT directs search iterations toward higher-performing paths, visit count NN reflects both high expected return and statistical confidence, making it far more robust against noisy rollout anomalies.

Worked numerical example

Let us trace a single search iteration on a root state sroots_{\text{root}} with two candidate actions, a1a_1 and a2a_2.

Parameters:

  • Exploration constant: c=2≈1.4142c = \sqrt{2} \approx 1.4142
  • Current root visits: N(sroot)=10N(s_{\text{root}}) = 10
  • Action a1a_1 stats: N(sroot,a1)=7N(s_{\text{root}}, a_1) = 7, accumulated wins W1=4.2  ⟹  Q(sroot,a1)=0.6000W_1 = 4.2 \implies Q(s_{\text{root}}, a_1) = 0.6000
  • Action a2a_2 stats: N(sroot,a2)=3N(s_{\text{root}}, a_2) = 3, accumulated wins W2=1.2  ⟹  Q(sroot,a2)=0.4000W_2 = 1.2 \implies Q(s_{\text{root}}, a_2) = 0.4000

Phase 1: Selection at the Root

Evaluate the UCT formula for both actions:

  • Action a1a_1: Exploitation: Q(sroot,a1)=0.6000\text{Exploitation: } Q(s_{\text{root}}, a_1) = 0.6000 Exploration: cln⁡(10)7=1.4142×2.30267=1.4142×0.5735=0.8111\text{Exploration: } c \sqrt{\frac{\ln(10)}{7}} = 1.4142 \times \sqrt{\frac{2.3026}{7}} = 1.4142 \times 0.5735 = 0.8111 UCT(sroot,a1)=0.6000+0.8111=1.4111\text{UCT}(s_{\text{root}}, a_1) = 0.6000 + 0.8111 = \mathbf{1.4111}

  • Action a2a_2: Exploitation: Q(sroot,a2)=0.4000\text{Exploitation: } Q(s_{\text{root}}, a_2) = 0.4000 Exploration: cln⁡(10)3=1.4142×2.30263=1.4142×0.8761=1.2390\text{Exploration: } c \sqrt{\frac{\ln(10)}{3}} = 1.4142 \times \sqrt{\frac{2.3026}{3}} = 1.4142 \times 0.8761 = 1.2390 UCT(sroot,a2)=0.4000+1.2390=1.6390\text{UCT}(s_{\text{root}}, a_2) = 0.4000 + 1.2390 = \mathbf{1.6390}

Selection Choice: Even though a1a_1 has a higher historical win rate (0.60000.6000 vs. 0.40000.4000), a2a_2 has been sampled far fewer times, giving it a much larger uncertainty bonus (1.23901.2390 vs. 0.81110.8111). UCT selects a2a_2 (1.6390>1.41111.6390 > 1.4111).

Phase 2: Expansion

The search reaches node s2s_2 (the child reached via a2a_2). Action a2a_2 leads to an unexpanded action a2,newa_{2,\text{new}}. A new child node snews_{\text{new}} is added to the tree with initial stats N=0,Q=0.0N = 0, Q = 0.0.

Phase 3: Simulation (Rollout)

A fast random rollout is launched from snews_{\text{new}} to terminal depth. The game concludes in a victory: Rollout Outcome: R=1.0\text{Rollout Outcome: } R = 1.0

Phase 4: Backpropagation

The score R=1.0R = 1.0 is backed up to the root:

  1. At leaf snews_{\text{new}}: N(snew)=0+1=1,Q(snew)=1.0000N(s_{\text{new}}) = 0 + 1 = 1, \quad Q(s_{\text{new}}) = 1.0000
  2. At parent node s2s_2: N(sroot,a2)←3+1=4N(s_{\text{root}}, a_2) \leftarrow 3 + 1 = 4 Q(sroot,a2)←1.2+1.04=2.24=0.5500Q(s_{\text{root}}, a_2) \leftarrow \frac{1.2 + 1.0}{4} = \frac{2.2}{4} = \mathbf{0.5500} (Equivalently, Q←0.4000+1.0−0.40004=0.5500Q \leftarrow 0.4000 + \frac{1.0 - 0.4000}{4} = 0.5500)
  3. At the root node: N(sroot)←10+1=11N(s_{\text{root}}) \leftarrow 10 + 1 = 11

In the very next iteration, Q(sroot,a2)Q(s_{\text{root}}, a_2) has risen from 0.40000.4000 to 0.55000.5500, actively driving further search into this newly validated line of play.

Code

The following self-contained Python implementation constructs an MCTS solver for a discrete navigation MDP, implementing UCT node selection, random rollouts, and visit-count action extraction.

import mathimport randomfrom typing import Dict, List, Optional, Tuple

class MCTSNode:    """A search tree node tracking visit counts and total rewards."""
    def __init__(        self,        state: int,        parent: Optional["MCTSNode"] = None,        action_from_parent: Optional[int] = None,    ) -> None:        self.state: int = state        self.parent: Optional["MCTSNode"] = parent        self.action_from_parent: Optional[int] = action_from_parent        self.children: Dict[int, "MCTSNode"] = {}        self.visit_count: int = 0        self.total_reward: float = 0.0        self.unexplored_actions: List[int] = [0, 1]  # 0: left, 1: right
    @property    def q_value(self) -> float:        """Mean expected value Q(s, a)."""        return (            self.total_reward / self.visit_count            if self.visit_count > 0            else 0.0        )
    def is_fully_expanded(self) -> bool:        """Returns True if all available actions have child nodes."""        return len(self.unexplored_actions) == 0
    def best_child_uct(self, c_param: float = 1.4142) -> "MCTSNode":        """Selects child node maximizing Upper Confidence Bound for Trees."""        best_child: Optional["MCTSNode"] = None        best_score = -float("inf")
        for child in self.children.values():            if child.visit_count == 0:                return child            # UCT = Q + c * sqrt(ln(N_parent) / N_child)            exploit = child.q_value            explore = c_param * math.sqrt(                math.log(self.visit_count) / child.visit_count            )            score = exploit + explore            if score > best_score:                best_score = score                best_child = child
        assert best_child is not None        return best_child

class MiniGridMCTS:    """MCTS solver for linear navigation: S0 <-> S1 <-> S2 <-> S3 (Goal, +1.0)."""
    def __init__(        self,        num_states: int = 4,        c_param: float = 1.4142,        max_depth: int = 10,    ) -> None:        self.num_states = num_states        self.goal_state = num_states - 1        self.c_param = c_param        self.max_depth = max_depth
    def step_dynamics(        self, state: int, action: int    ) -> Tuple[int, float, bool]:        """Environment transition: action 1 moves right, 0 moves left."""        next_s = (            min(state + 1, self.goal_state)            if action == 1            else max(state - 1, 0)        )        done = next_s == self.goal_state        reward = 1.0 if done else 0.0        return next_s, reward, done
    def search(self, root_state: int, num_iterations: int = 150) -> int:        """Runs MCTS and returns optimal action based on visit counts."""        root = MCTSNode(state=root_state)
        for _ in range(num_iterations):            # Phase 1: Selection            node = root            while node.is_fully_expanded() and node.children:                if node.state == self.goal_state:                    break                node = node.best_child_uct(self.c_param)
            # Phase 2: Expansion            if not node.is_fully_expanded() and node.state != self.goal_state:                action = node.unexplored_actions.pop()                next_state, _, _ = self.step_dynamics(node.state, action)                child_node = MCTSNode(                    state=next_state,                    parent=node,                    action_from_parent=action,                )                node.children[action] = child_node                node = child_node
            # Phase 3: Simulation (Fast Rollout)            sim_state = node.state            sim_reward = 0.0            depth = 0            while sim_state != self.goal_state and depth < self.max_depth:                rand_act = random.choice([0, 1])                sim_state, r, done = self.step_dynamics(sim_state, rand_act)                if done:                    sim_reward = r                    break                depth += 1
            # Phase 4: Backpropagation            curr: Optional[MCTSNode] = node            while curr is not None:                curr.visit_count += 1                curr.total_reward += sim_reward                curr = curr.parent
        # Decision execution: Choose action with highest visit count        best_action = max(            root.children.keys(),            key=lambda a: root.children[a].visit_count,        )
        print(f"Root visit count N: {root.visit_count}")        for act, child in root.children.items():            act_name = "right" if act == 1 else "left"            print(                f"Action '{act_name}': Visits N = {child.visit_count:3d}, Mean Q = {child.q_value:.4f}"            )
        return best_action

# Execute search from start State 0random.seed(42)solver = MiniGridMCTS()optimal_action = solver.search(root_state=0, num_iterations=150)
# Verify that MCTS picked action 1 ('right') as optimalassert (    optimal_action == 1), "MCTS failed to identify 'right' as the winning action!"print(    f"\nDecision chosen: Action {optimal_action} ('right') correctly selected!")
# -> Expected output:# -> Root visit count N: 150# -> Action 'right': Visits N = 145, Mean Q = 0.8483# -> Action 'left': Visits N =   5, Mean Q = 0.0000# -> # -> Decision chosen: Action 1 ('right') correctly selected!

Watch Out For

Selecting the Action with Highest Q-Value Instead of Highest Visit Count

A widespread practitioner bug when deploying MCTS is choosing the final real action according to max⁡aQ(root,a)\max_a Q(\text{root}, a) instead of max⁡aN(root,a)\max_a N(\text{root}, a).

The Failure Mode: Early in the search or in highly noisy environments, an obscure, rarely tested action might get lucky during its single random rollout, achieving Q=1.0Q = 1.0 with N=1N = 1. Meanwhile, the truly dominant line was tested N=950N = 950 times against difficult defense and achieved Q=0.82Q = 0.82.

  • If you execute arg⁡max⁡Q\arg\max Q, the agent picks the fragile, unverified action (Q=1.0Q=1.0), walking straight into a tactical trap.
  • If you execute arg⁡max⁡N\arg\max N, the agent reliably selects the stress-tested action (N=950N=950). Because UCT naturally channels computational iterations into moves with higher sustained expected returns, visit count NN acts as an automatic filter against sample variance.

Secondary Traps:

  1. Unbounded Exploration Constant: Setting cc too high forces uniform breadth-first search, abandoning promising paths. Setting c=0c = 0 turns selection purely greedy, causing the search to freeze inside early sub-optimal branches.
  2. Forgetting to Handle Unvisited Nodes: Forgetting to return +∞+\infty when N(s,a)=0N(s, a) = 0 causes zero-division errors or biases early selection against unexplored branches.

The Fix:

  • Always select real-world actions using arg⁡max⁡aN(root,a)\arg\max_a N(\text{root}, a).
  • Keep c=2c = \sqrt{2} when returns are normalized in [0,1][0, 1].
  • Explicitly guard against N(s,a)=0N(s, a) = 0 by returning +∞+\infty during UCT child scoring.

The Quick Version

  • Asymmetric search: MCTS focuses computational effort deeply on promising lines of play while pruning unviable branches, avoiding exponential breadth-first blowups.
  • Four repeating phases: Selection (navigating tree via UCT), Expansion (adding a new leaf node), Simulation (fast random rollout to terminal state), and Backpropagation (updating NN and QQ upward).
  • Heuristic-free evaluation: Leaf positions are evaluated by playing out fast Monte Carlo rollouts rather than relying on hand-crafted static evaluation functions.
  • Robust decision policy: The final action is chosen by maximum visit count N(root,a)N(\text{root}, a) rather than maximum QQ-value, providing resilience against rollout noise.