Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Hard Problems & Adversarial Search  ›  Lesson

Minimax & Alpha-Beta

Minimax & Alpha-Beta 34 minSearch a tree of moves and counter-moves; prune a whole subtree the instant it can no longer change the outcome
You're building a piece ofMeridian — the bid desk
This piece — best_counter_bid(): Searches a tree of bid and counter-bid moves against a competitor who is also trying to win, pruning branches that can't change the outcome.
Scenario A rival bidder is also optimising, move for move, against Meridian's own bids on a contract. The bid desk needs the value of the position both sides will settle on -- without wasting time exploring branches that can't possibly change the outcome.
Your task
Build best_counter_bid(tree). tree is a nested list game tree: a leaf is a number (the value if play reaches it); an internal node is a list of children. The root always maximises (Meridian is bidding); levels alternate min (the competitor) and max going down. Run minimax with alpha-beta pruning: at a max node keep raising alpha, at a min node keep lowering beta, and the moment alpha >= beta, skip every remaining child of that node -- counting every leaf under a skipped child as pruned. Return {"value": ..., "nodes_visited": ..., "nodes_pruned": ..., "total_leaves": ...}. Example: best_counter_bid([[3,5,2],[9,1,0],[7,4,8]]) -> value 4, nodes_visited 8, nodes_pruned 1, total_leaves 9.

Minimax & Alpha-Beta

In short: when a competitor is also optimising, searching their replies is a tree: Meridian's bid desk picks a move to maximise the outcome, the rival picks their reply to minimise it, alternating all the way down to the leaves. Plain minimax visits every leaf. Alpha-beta pruning keeps two running numbers — alpha, the best the maximiser can already guarantee, and beta, the best the minimiser can already guarantee — and the moment alpha >= beta, the rest of the current node's children can be skipped entirely: nothing left in that subtree could ever change the parent's decision. The value found is always identical to plain minimax. Only the amount of tree actually looked at changes, and how the leaves are ordered changes that amount a lot.

A MAX root over three MIN nodes with leaves 3-5-2, 9-1-0 and 7-4-8; the middle node's third leaf (0) is pruned with a dashed line and a red "pruned" tag, next to an (α=2, β=1) badge showing why

Why alpha ≥ beta ends the search early

At a MIN node, beta only ever falls as better (lower) replies are found. The moment beta drops to or below the alpha the MAX parent already has in hand from an earlier sibling, the parent already has something at least as good available elsewhere — so this MIN node can never be the parent's actual choice, no matter what its remaining children turn out to be:

watch it move · tree

MAX at the root, MIN at L and R, MAX again at LL/LR/RL/RR. Alpha starts at the value the root already has in hand from an earlier branch; the moment it meets or beats beta, the rest of that branch is skipped -- here, all of RR.

LL (a MAX node over leaves 3 and 5) settles at 5. Binary tree, 15 nodes, height 4. Settled: LL.

1 / 6

Plain minimax first, so you know what pruning has to match

python

Every leaf gets visited here — 9 of them. Alpha-beta pruning, added next, never changes this number. It only changes how many of the 9 it has to look at to be sure of it.

What does this print?
def minimax_with_pruning(node, maximizing, alpha=float("-inf"), beta=float("inf")):
    if not isinstance(node, list):
        return node
    if maximizing:
        value = float("-inf")
        for child in node:
            value = max(value, minimax_with_pruning(child, False, alpha, beta))
            alpha = max(alpha, value)
            if alpha >= beta:
                break
        return value
    else:
        value = float("inf")
        for child in node:
            value = min(value, minimax_with_pruning(child, True, alpha, beta))
            beta = min(beta, value)
            if alpha >= beta:
                break
        return value

a = minimax_with_pruning([[3, 5, 2], [9, 1, 0], [7, 4, 8]], True)
b = minimax_with_pruning([[7, 4, 8], [9, 1, 0], [3, 5, 2]], True)
print(a, b)

Move ordering changes the search, not the answer

Put the strongest-looking branch first and alpha rises fast, which makes later siblings easy to cut. Put it last and the search has to do almost everything before it finds a reason to stop:

broken — fix it

This prints 0.0 instead of 0.1111. Add the cutoff: `if alpha >= beta: break`, in both branches.

Quick practice

What does this print?
tree = [[1, 2], [3, 4]]
print(len([x for group in tree for x in group]))
What does this print?
alpha, beta = 5, 3
print(alpha >= beta)

🎯 Your turn

Build best_counter_bid(tree). tree is a nested list game tree: a leaf is a number, an internal node is a list of children, and the root always maximises. Run minimax with alpha-beta pruning and return {"value": ..., "nodes_visited": ..., "nodes_pruned": ..., "total_leaves": ...}, where nodes_pruned counts every leaf under a subtree that was skipped.

  • best_counter_bid([[3,5,2],[9,1,0],[7,4,8]]) → value 4, 8 visited, 1 pruned, 9 total leaves.

Hint: thread alpha and beta through the recursion; at a max node raise alpha, at a min node lower beta, and the instant alpha >= beta, stop looping over that node's remaining children — but still add up the leaves underneath each one you skip.

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upHow many leaves does this tree have?

Write leaf_count(node): if node is a list, sum leaf_count over its children; otherwise it's a single leaf, so return 1. leaf_count([[3,5,2],[9,1,0],[7,4,8]]) -> 9.

adversarial-search
DrillMinimax, no pruning yet

Write minimax_value(node, maximizing): a leaf returns itself; an internal node returns max (if maximizing) or min (otherwise) of its children's minimax_value, with maximizing flipped one level down. minimax_value([[3,5,2],[9,1,0],[7,4,8]], True) -> 4.

adversarial-search
BuildAdd the pruning, measure what it saves

Write minimax_with_pruning(node, maximizing), returning [value, nodes_visited]: the same minimax value as before, but stopping early with alpha-beta whenever alpha >= beta, and counting only the leaves actually visited. minimax_with_pruning([[3,5,2],[9,1,0],[7,4,8]], True) -> [4, 8].

adversarial-searchalpha-beta
BossWhat fraction of the tree got skipped?

Write pruned_fraction(tree), returning the fraction (0 to 1, rounded to 4 places) of total_leaves that alpha-beta pruned while finding the minimax value with the root maximising. pruned_fraction([[3,5,2],[9,1,0],[7,4,8]]) -> 0.1111.

adversarial-searchalpha-beta
CapstoneMove ordering, measured

Write compare_move_orderings(tree_variants): given a list of trees (the same leaves, reordered differently), return a list of each one's pruned_fraction, in order. compare_move_orderings([[[3,5,2],[9,1,0],[7,4,8]], [[7,4,8],[9,1,0],[3,5,2]]]) -> [0.1111, 0.3333].

adversarial-searchalpha-beta
best_counter_bid.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
best_counter_bid(tree) → dictThe minimax value Meridian can guarantee, how many leaves alpha-beta had to look at, how many it pruned, and the tree's total leaf count.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.