Skip to content

Bridges: DSA to LLMs

Priority queues and Dijkstra to beam search

Exploring the best partial sequences

The decoder's beam is a priority queue of half-finished sentences - the exact frontier Dijkstra expands, only scored by log-probability instead of distance.

In Shortest paths: Dijkstra you watched Dijkstra grow a shortest-path tree by repeatedly pulling the closest unsettled node off a priority queue and expanding it - best-first search. Beam search, the workhorse decoder behind translation and captioning, is the very same machine pointed at a different graph: its nodes are half-written sentences, its edge weights are per-token log-probabilities, and its heap keeps the highest-scoring partial sequences alive at every step. Learn one and you have learned the other; the only real differences are what a node is and the fact that the beam is bounded to width instead of holding the whole frontier.

The one-line idea

Best-first search keeps a frontier of the most promising partial solutions in a priority queue and always expands the best ones next. Dijkstra expands the lowest-cost node; A* adds a heuristic; beam search keeps the highest-scoring partial sentences. Swap “distance travelled” for “negative log-probability” and the language model's decoder is the shortest-path algorithm you already know - run on a graph whose nodes are half-written sentences.

The engine you already own: a heap-backed frontier

Every one of these algorithms is a loop over the same two moves: pop the most promising item from a priority queue, then push its expansions back in. Dijkstra pops the min-cost node and pushes relaxed neighbours; beam search pops (surfaces) the top- sequences and pushes each one's next-token children. The heap is what makes “which candidate is best?” cheap enough to ask thousands of times per sentence. Here is the translation, term for term - nothing on the right is a metaphor.

Graph search - Dijkstra / A*Beam-search decoding
node = a partial path from the sourcea partial sequence (the prefix so far)
edge weight
cumulative path cost - the negative log-probability
priority queue / binary heapthe beam - a top- frontier
pop the single min-cost nodesurface the highest-scoring beams
goal node reachedthe end-of-sequence token is emitted
A* heuristic steers the searchlength / reward terms steer decoding

The one honest difference: Dijkstra keeps every reachable node on the heap and is therefore exact. Beam search deliberately throws away all but the top at each step to stay affordable - the sequence graph has paths ( = vocabulary, = length), far too many to settle exactly. That truncation is what makes beam search a fast heuristic rather than a guaranteed optimum.

Scoring a path: add up the log-probabilities

A model gives you - the probability of each next token given the prefix. The probability of a whole sequence is the product of these, but a decoder scores it as the sum of log-probabilities:

Three reasons for the log. Multiplying hundreds of probabilities underflows to zero in floating point; is monotonic, so it never changes the ranking; and it turns a product into a sum, converting “probability of a path” into an additive cost - exactly the additive edge weights Dijkstra accumulates along a route. Negate the log-probs and “more probable” becomes “less costly,” and beam search is Dijkstra with a fixed-width frontier.

One subtlety follows immediately. Because every , longer paths keep adding negative terms, so a raw score always favours shorter sequences. Beam search corrects this with length normalization, dividing by the length raised to a power :

Within a single step every candidate on the beam has the same length, so this rescaling changes nothing there - it only matters when you finally compare finished sentences of different lengths (once an end-of-sequence token can fire).

Beam width : from greedy to (almost) exhaustive

The demo below decodes a toy language model, three candidate tokens per step, three steps deep. Drag the beam width and step through the levels. Each step pushes every surviving beam's children into the priority queue on the right; the heap sorts them by score and surfaces the top ; everything below the cut is pruned and its branch fades in the tree. Watch how is plain greedy decoding and larger widens toward exhaustive search.

Beam search, level by level. Drag the beam width k and step through the levels.

Tree: each node is a partial sentence, showing its last token and its running score (sum of log-probs). Solid nodes are the beams the heap kept; faded nodes were pruned this step. Queue: the same candidates, sorted by score - the top-k above the dashed cut survive.

kept beam (heap top-k)pruned candidatewinning sequence

Start: the queue holds one beam - the empty sequence - with cumulative score log P = 0.

Priority queue

The queue holds one beam - the empty sequence - with score 0.00. Press Step › to push its children.
Greedy (k = 1):the cat sat” · log P -2.09Beam (k = 2):a cat purred” · log P -1.84 - 29% more probable, a sentence greedy never saw.

The aha: greedy's trap, and why width saves it

Drag to 1 and the tree collapses to a single greedy path - the → cat → sat, always the locally best token. Drag it to 2 and a second beam survives: a slightly worse first token (a instead of the) that unlocks a far better continuation, a cat purred - a sentence greedy pruned away at step one and could never recover. That is the entire reason beam search exists: a locally worse token can lead to a globally better sequence, and only a frontier wider than one can hold it alive long enough to find out. Notice too that here explores more candidates but lands on the same winner - width has diminishing returns, and each extra beam costs another full model evaluation per step.
import heapq, math

def beam_search(next_dist, k, steps):
    # each beam is (cumulative log-prob score, list of tokens)
    beams = [(0.0, ["<s>"])]
    for _ in range(steps):
        frontier = []                              # the priority queue for this level
        for score, seq in beams:
            for token, p in next_dist(seq):        # PUSH every child
                heapq.heappush(frontier, (score + math.log(p), seq + [token]))
        beams = heapq.nlargest(k, frontier)        # SURFACE top-k, prune the rest
    return max(beams)                              # best-scoring complete sequence

# Same loop as Dijkstra: swap `next_dist` for graph edges and `log(p)`
# for -weight, drop the top-k cap, and you are back to shortest paths.
Beam search, powered by a heap

Check yourself

With beam width k = 1, beam search reduces to exactly:

The heap in beam search orders candidate sequences by their:

How is beam-search decoding the same algorithm as Dijkstra's shortest path?

Try to state it, then check.

Lock it in

  • Beam search is best-first frontier expansion: a priority queue of partial sequences, the same pop-then-push loop Dijkstra runs on a graph.
  • The mapping is exact - nodes are partial sequences, edge weights are per-token negative log-probs, cumulative cost is , and the beam is the frontier.
  • Logs turn the product of probabilities into an additive cost (and dodge underflow); length normalization by stops raw scores from favouring shorter sequences.
  • Beam width spans greedy () to near-exhaustive; capping to top- makes it a fast heuristic, not the exact optimum Dijkstra guarantees.

Primary source

Ground the priority-queue engine in Abdul Bari - Dijkstra & shortest paths: the extract-min / relax loop is precisely the pop-push loop beam search runs. Then see the same machinery applied to text - beam search, log-prob scoring, and how decoding differs from sampling - in the Hugging Face LLM Course.

Ask your teacher

Beam search decoding is Dijkstra for text: the same priority queue that finds shortest paths keeps the best partial sentences alive. This is the connection the earlier lessons kept pointing at. Ask me why beam search often produces bland, repetitive text (and how sampling trades optimality for diversity), how an A* heuristic maps onto reward-guided decoding, or why the exact search over sequences is hopeless without the top- truncation that makes beam search a heuristic.