Skip to content
AI360Xpert
Beta

Monte Carlo Tree Search (MCTS)

A planning method that grows a lopsided lookahead tree by playing thousands of fast pretend games, digging deeper where wins pile up and dropping lines that lose.

MCTS repeats select, expand, roll out, and back up; here UCT scores 1.6390 for a2 against 1.4111 for a1, and the resulting win lifts Q(a2) from 0.40 to 0.55
MCTS repeats select, expand, roll out, and back up; here UCT scores 1.6390 for a2 against 1.4111 for a1, and the resulting win lifts Q(a2) from 0.40 to 0.55

Why Does This Exist?

Some games are too big to search the normal way. Go has around 1017010^{170} legal positions, so breadth first search drowns in branches. Minimax needs a hand written score for every middle position, and nobody could write a good one for Go for decades. Pure random playouts need no scoring but blunder, because they never look one move ahead on purpose.

Monte Carlo Tree Search, from Kocsis and Szepesvari (2006), takes the middle path. It grows a search tree that is deliberately lopsided: deep along lines that keep winning, shallow where losses pile up. Each round it walks down the tree with a bandit formula called UCT, adds one new node, plays one fast random game from there, and backs the result up the path. No hand written evaluation, no uniform expansion. That recipe became the planning core of AlphaGo, so the details here pay off directly in MuZero and AlphaZero.

The one idea you need first

This page leans on the multi-armed bandit: one state, several levers, unknown payouts, and the tension between pulling the current best and testing the rest. UCT is the bandit rule UCB1 applied at every tree node. If bandits are new, read that page first, then come back.

Think of It Like This

Mapping caves with one spool of rope

You lead a caving club with a single spool of rope and a weekend. Some passages look promising from the entrance, most are unknown. Your rule: each trip, walk down the passages with the best mix of past finds and least exploration, tie off one step of new rope at the frontier, then run ahead with a flashlight as far and fast as you can and report back good or bad. Chalk marks at each junction record visits and finds.

By Sunday the map is lopsided: the rich galleries have long rope lines, dead ends have one chalk mark each. Rope is compute, chalk marks are the visit counts NN and win rates QQ, and the mix of finds plus novelty is the UCT score.

Where it stops: cavers use gut feel for the mix, while MCTS uses a fixed logarithmic formula with a constant cc, and cave reports are stories whereas playout results are numbers that average cleanly.

How It Actually Works

Nodes are states ss, edges are actions aa. Every edge stores two numbers: N(s,a)N(s, a), how many times it was tried, and Q(s,a)Q(s, a), the average result after trying it. One iteration runs four phases.

Select, expand, roll out, back up

Select. From the root, repeatedly pick the action with the highest UCT score:

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

Q(s,a)Q(s, a) exploits what won before. The square root term explores: ln⁡N(s)\ln N(s) grows with total parent visits N(s)N(s) while N(s,a)N(s, a) in the denominator shrinks the bonus each time the action is tried. The constant cc sets the trade. With rewards in [0,1][0, 1], theory says c=2≈1.4142c = \sqrt{2} \approx 1.4142. An untried action gets infinite score, so everything is tried once first.

Expand. At the first node with an untried action, or a terminal node, stop. If it is not terminal, add one child for an untried action with N=0N = 0 and Q=0Q = 0.

Roll out. From the new node, play to the end with a fast default policy, often uniform random. No tree, no learning here, just one terminal reward RR, such as +1+1 for a win.

Back up. Walk the recorded path upward. For each edge, add one visit and fold RR into the running average:

N(s,a)←N(s,a)+1,Q(s,a)←Q(s,a)+R−Q(s,a)N(s,a)N(s, a) \leftarrow N(s, a) + 1, \qquad Q(s, a) \leftarrow Q(s, a) + \frac{R - Q(s, a)}{N(s, a)}

When the budget of iterations or milliseconds runs out, play the action with the most visits, arg⁡max⁡aN(root,a)\arg\max_a N(\text{root}, a), not the highest QQ. Visits already blend value with confidence, which is why they survive noisy playouts.

Worked example

Root state with two actions, c=2≈1.4142c = \sqrt{2} \approx 1.4142, parent visits N=10N = 10. Action a1a_1 has N=7N = 7 and Q=0.60Q = 0.60. Action a2a_2 has N=3N = 3 and Q=0.40Q = 0.40. Since ln⁡10≈2.3026\ln 10 \approx 2.3026:

UCT(a1)=0.60+1.4142×2.3026/7=0.60+0.8111=1.4111\text{UCT}(a_1) = 0.60 + 1.4142 \times \sqrt{2.3026 / 7} = 0.60 + 0.8111 = 1.4111 UCT(a2)=0.40+1.4142×2.3026/3=0.40+1.2390=1.6390\text{UCT}(a_2) = 0.40 + 1.4142 \times \sqrt{2.3026 / 3} = 0.40 + 1.2390 = 1.6390

Selection picks a2a_2: worse record, far less tested, bigger uncertainty bonus. Expansion adds a child under a2a_2. The rollout wins, R=1.0R = 1.0. Backup updates the a2a_2 edge from totals 1.21.2 over 33 visits to 2.22.2 over 44:

Q(a2)=2.2/4=0.55,N(root)=11Q(a_2) = 2.2 / 4 = 0.55, \qquad N(\text{root}) = 11

Next round starts with a2a_2 at 0.550.55 instead of 0.400.40, so search leans into the line that just paid off. The diagram above draws exactly these numbers.

Code

This snippet replays the worked example: score both actions with UCT, then back up the win. It uses only the standard library.

import math

def uct(q: float, n_child: int, n_parent: int, c: float = math.sqrt(2)) -> float:    """UCT score for one edge. Untried edges score infinite."""    if n_child == 0:        return float("inf")    return q + c * math.sqrt(math.log(n_parent) / n_child)

def back_up(q: float, n: int, reward: float) -> tuple[float, int]:    """Fold one terminal reward into the running average."""    n_new = n + 1    return q + (reward - q) / n_new, n_new

s_a1 = uct(0.60, 7, 10)s_a2 = uct(0.40, 3, 10)print(f"UCT(a1)={s_a1:.4f} UCT(a2)={s_a2:.4f}")  # -> UCT(a1)=1.4111 UCT(a2)=1.6390q_new, n_new = back_up(0.40, 3, 1.0)print(f"Q(a2)={q_new:.4f} N={n_new}")  # -> Q(a2)=0.5500 N=4

The printed scores match the hand computation: 1.41111.4111 against 1.63901.6390, and the win moves Q(a2)Q(a_2) to 0.550.55.

Watch Out For

Playing the highest Q instead of the most visited move

After search, picking arg⁡max⁡aQ\arg\max_a Q feels natural and is wrong. A line tried once that lucked into a win sits at Q=1.0Q = 1.0 with N=1N = 1, while the genuinely strong line sits at Q=0.82Q = 0.82 with N=950N = 950. Playing max QQ walks into the fragile line. Playing max NN picks the line UCT kept returning to, which is the whole point of the search. Symptom: occasional shocking blunders from long searches. Fix: decide by visit count, always.

Setting the exploration constant without thinking

With c=0c = 0 selection is pure greed and the tree freezes inside whatever looked good early. With cc far above 2\sqrt{2} on [0,1][0, 1] rewards, bonuses swamp values and search spreads uniformly, burning the budget on hopeless branches. Symptom: search depth never builds, or the same weak line repeats forever. Fix: keep c=2c = \sqrt{2} for win rate rewards, and scale cc with the reward range when rewards live elsewhere.

The Quick Version

  • MCTS plans by repeating four steps: select with UCT, expand one node, roll out randomly, back up the result.
  • UCT adds an exploration bonus cln⁡N(s)/N(s,a)c \sqrt{\ln N(s) / N(s, a)} to each win rate, so untried and stale lines get revisited on schedule.
  • The tree grows lopsided: deep where wins pile up, shallow where losses do, which is how it survives huge branching factors.
  • Leaf positions need no hand written score; fast random playouts supply the signal instead.
  • The played move is the most visited one, because visit counts already combine value with confidence.