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.
Why Does This Exist?
In sequential decision-making domains with enormous branching factors—such as Go ( 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):
- 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.
- Expansion: When they reach the edge of their detailed map, they sketch out one plausible new tactical move.
- 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.
- 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 and mean value bounds 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 and directed edges represent actions . Each node maintains two statistics:
- : The number of times action was selected from state .
- : The estimated value (e.g., average win rate or return) of taking action in state :
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:
Where:
- is the exploitation term, rewarding moves with historically high success.
- is the exploration bonus, derived from the UCB1 multi-armed bandit algorithm.
- is the total visit count of the parent node.
- is the exploration constant, theoretically set to to balance exploration against exploitation.
- If an action has never been visited (), its UCT score is set to , 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 , and attaches it to the tree. The new node is initialized with:
3. Simulation (Rollout)
From the newly expanded node , 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):
This simulation requires no tree expansion. At the conclusion of the rollout, the environment yields a terminal reward (e.g., for a win, for a draw, for a loss).
4. Backpropagation
The terminal reward is propagated backward along the exact sequence of nodes traversed during the Selection and Expansion phases:
- For every node along the path, its visit count is incremented:
- Its value estimate is updated incrementally to reflect the running sample average:
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 -value:
Because UCT directs search iterations toward higher-performing paths, visit count 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 with two candidate actions, and .
Parameters:
- Exploration constant:
- Current root visits:
- Action stats: , accumulated wins
- Action stats: , accumulated wins
Phase 1: Selection at the Root
Evaluate the UCT formula for both actions:
-
Action :
-
Action :
Selection Choice: Even though has a higher historical win rate ( vs. ), has been sampled far fewer times, giving it a much larger uncertainty bonus ( vs. ). UCT selects ().
Phase 2: Expansion
The search reaches node (the child reached via ). Action leads to an unexpanded action . A new child node is added to the tree with initial stats .
Phase 3: Simulation (Rollout)
A fast random rollout is launched from to terminal depth. The game concludes in a victory:
Phase 4: Backpropagation
The score is backed up to the root:
- At leaf :
- At parent node : (Equivalently, )
- At the root node:
In the very next iteration, has risen from to , 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 instead of .
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 with . Meanwhile, the truly dominant line was tested times against difficult defense and achieved .
- If you execute , the agent picks the fragile, unverified action (), walking straight into a tactical trap.
- If you execute , the agent reliably selects the stress-tested action (). Because UCT naturally channels computational iterations into moves with higher sustained expected returns, visit count acts as an automatic filter against sample variance.
Secondary Traps:
- Unbounded Exploration Constant: Setting too high forces uniform breadth-first search, abandoning promising paths. Setting turns selection purely greedy, causing the search to freeze inside early sub-optimal branches.
- Forgetting to Handle Unvisited Nodes: Forgetting to return when causes zero-division errors or biases early selection against unexplored branches.
The Fix:
- Always select real-world actions using .
- Keep when returns are normalized in .
- Explicitly guard against by returning 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 and 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 rather than maximum -value, providing resilience against rollout noise.