Dynamic Programming

Algorithmics study and revision notes

Jaak Vilo

2026-08-22

Reuse a solved subproblem

Dynamic programming (DP) is a way to organise a computation whose subproblems recur. Instead of solving the same state again along every recursive path, we solve it once and retain its result. The result may be an optimum, a count, a Boolean answer, or some other summary; DP is not restricted to optimisation.

Two ideas have different roles:

A table is only a representation. The algorithmic work is to define states that contain exactly the information required for future decisions.

Learning goals

After studying this note, you should be able to:

Prerequisites

You should be comfortable with arrays, recursion, asymptotic notation, and directed acyclic graphs. We use one-based string notation such as X[1..i]X[1..i] and reserve row or column zero for the empty prefix. No earlier string-matching material is assumed.

A design recipe

For each new problem, write down the following items before writing code.

  1. State. Give one precise sentence defining every index and stored value.
  2. Transition. Express a state using strictly smaller or earlier states.
  3. Boundaries. Define empty, smallest, and impossible states.
  4. Order. Ensure every dependency is available before it is used.
  5. Answer. Identify the state or aggregate that answers the original question.
  6. Witness. Store a predecessor or decision if an actual solution is required.
  7. Cost. Count reachable states and the work and storage per state.

For example, Fibonacci numbers satisfy Fi=Fi1+Fi2F_i=F_{i-1}+F_{i-2} with F0=0F_0=0 and F1=1F_1=1. Direct recursion repeats the same calls. A bottom-up computation keeps only two previous values:

fib(n):
    if n = 0: return 0
    previous = 0
    current  = 1
    for i = 2 .. n:
        previous, current = current, previous + current
    return current

This uses O(n)O(n) arithmetic operations and O(1)O(1) stored values. Top-down memoisation instead keeps a map from an argument to its computed result. It mirrors a recursive specification and can avoid unreachable states; tabulation avoids recursive call overhead and often exposes a better memory order.

Why a recurrence is correct

A standard proof follows the dependency order. Prove the base states directly. Then assume every predecessor state already has its stated meaning. Show that every candidate in the transition constructs a feasible solution for the current state, and that every feasible solution must make one of the enumerated final decisions. Taking the best candidate therefore neither invents an impossible solution nor misses the optimum.

This is induction on an index, interval length, number of selected objects, or any other well-founded state order. Merely displaying a table is not a correctness argument.

Edit distance from the final operation

Let D[i,j]D[i,j] be the minimum cost of transforming prefix X[1..i]X[1..i] into prefix Y[1..j]Y[1..j]. Under unit-cost insertion, deletion, and substitution, the last operation is exactly one of the following:

D[i,j]=min{D[i1,j]+1delete Xi,D[i,j1]+1insert Yj,D[i1,j1]+[XiYj]match or substitute. D[i,j]=\min\begin{cases} D[i-1,j]+1 & \text{delete }X_i,\\ D[i,j-1]+1 & \text{insert }Y_j,\\ D[i-1,j-1]+[X_i\ne Y_j] & \text{match or substitute.} \end{cases}

The boundaries are D[i,0]=iD[i,0]=i and D[0,j]=jD[0,j]=j. Fill rows left-to-right and top-to-bottom, or use any order in which the upper, left, and diagonal predecessor have already been computed.

There are (|X|+1)(|Y|+1)(|X|+1)(|Y|+1) states and constant work per state, so the running time is Θ(|X||Y|)\Theta(|X||Y|) and the full table uses the same asymptotic space. If only the value is needed, one previous row and the current row use O(min{|X|,|Y|})O(\min\{|X|,|Y|\}) space after orienting the shorter string along a row.

Weighted and symbol-dependent edits

Applications need not assign every operation cost 1. Let cdel(x)c_{del}(x), cins(y)c_{ins}(y), and csub(x,y)c_{sub}(x,y) be nonnegative costs, with csub(x,x)=0c_{sub}(x,x)=0. Replace the three added constants in the recurrence by these functions. A case-only change may cost 0.10.1, for example, while an implausible letter change costs 1.

The chosen costs are part of the problem definition. The resulting function is not automatically symmetric or a metric. Symmetry, identity, and the triangle inequality require suitable relationships among insertion, deletion, and substitution costs.

An adjacent transposition can be added when i,j2i,j\ge2, Xi=Yj1X_i=Y_{j-1}, and Xi1=YjX_{i-1}=Y_j:

D[i,j]min(D[i,j],D[i2,j2]+ctr(Xi1,Xi)). D[i,j]\gets\min\bigl(D[i,j],D[i-2,j-2]+c_{tr}(X_{i-1},X_i)\bigr).

This simple recurrence computes the optimal string alignment variant, in which a substring is not edited repeatedly. The unrestricted Damerau–Levenshtein distance needs additional last-occurrence information; the two models should not be named interchangeably.

Worked edit-distance trace

For cat and cut, rows are prefixes of cat and columns are prefixes of cut:

DD ε\varepsilon c u t
ε\varepsilon 0 1 2 3
c 1 0 1 2
a 2 1 1 2
t 3 2 2 1

The bottom-right value is 1. A traceback chooses the diagonal predecessors: match c, substitute a by u, and match t. The recurrence also considered insertions and deletions; the trace is evidence of one optimum, not a greedy decision made at the first mismatch.

Store a predecessor direction in every cell to recover an edit script. If several predecessors tie, there may be several optimal scripts. Keeping all tied predecessors represents a DAG of optimal solutions, which can be exponentially numerous even though the value table is small.

Global and local biological sequence alignment

Edit distance minimises a nonnegative cost. Biological sequence comparison is usually written as score maximisation: a substitution score s(a,b)s(a,b) rewards a match or a biologically plausible replacement, while gaps subtract penalties. DNA may use a simple match/mismatch score; protein alignment commonly uses an empirically derived substitution matrix.

Choose the alignment scope before choosing scores. A global alignment explains both complete sequences, a local alignment selects the best pair of substrings, and a semi-global alignment makes specified end gaps free. The recurrence, boundary values, traceback start, and traceback stop must all describe the same scope. A substitution matrix is simply a table giving s(a,b)s(a,b) for every symbol pair in the chosen alphabet; it does not determine the gap model.

Let A=a1anA=a_1\ldots a_n and B=b1bmB=b_1\ldots b_m. Let go0g_o\ge0 be the gap-opening penalty and ge0g_e\ge0 the extension penalty. This note charges a gap of length 1\ell\ge1 by

go+(1)ge.g_o+(\ell-1)g_e.

Thus the first missing symbol costs gog_o and each later symbol in the same gap costs geg_e. Some software instead charges go+geg_o+\ell g_e; scores are comparable only after the convention is stated.

A linear gap cost needs no memory of how the previous column was formed. An affine cost does: opening a gap and extending an existing run must use different predecessor states.

Three affine-gap states

For prefixes A[1..i]A[1..i] and B[1..j]B[1..j], define:

For i,j>0i,j>0 use

M[i,j]=s(ai,bj)+max{M[i1,j1],GB[i1,j1],GA[i1,j1]}, M[i,j]=s(a_i,b_j)+ \max\{M[i-1,j-1],G_B[i-1,j-1],G_A[i-1,j-1]\},

GB[i,j]=max{M[i1,j]go,GA[i1,j]go,GB[i1,j]ge,GA[i,j]=max{M[i,j1]go,GB[i,j1]go,GA[i,j1]ge. G_B[i,j]=\max\begin{cases} M[i-1,j]-g_o,\\ G_A[i-1,j]-g_o,\\ G_B[i-1,j]-g_e, \end{cases} \qquad G_A[i,j]=\max\begin{cases} M[i,j-1]-g_o,\\ G_B[i,j-1]-g_o,\\ G_A[i,j-1]-g_e. \end{cases}

The first two candidates in each gap state open a new gap; the last extends the same gap. These recurrences permit adjacent gap runs in opposite sequences. A model that forbids such a switch omits the cross-gap opening candidate; that convention can change an optimum when mismatch scores are unusually severe.

Global alignment: Needleman–Wunsch with affine gaps

A global alignment consumes both complete sequences. Initialise impossible states to -\infty and set

M[0,0]=0,GA[0,0]=GB[0,0]=.M[0,0]=0,\qquad G_A[0,0]=G_B[0,0]=-\infty.

For i>0i>0,

GB[i,0]=go(i1)ge,M[i,0]=GA[i,0]=,G_B[i,0]=-g_o-(i-1)g_e,\qquad M[i,0]=G_A[i,0]=-\infty,

and for j>0j>0,

GA[0,j]=go(j1)ge,M[0,j]=GB[0,j]=.G_A[0,j]=-g_o-(j-1)g_e,\qquad M[0,j]=G_B[0,j]=-\infty.

The optimum global score is

max{M[n,m],GB[n,m],GA[n,m]}.\max\{M[n,m],G_B[n,m],G_A[n,m]\}.

Leading and trailing gaps are charged because no sequence end may be discarded. With a linear gap penalty this is the familiar Needleman–Wunsch grid; the three-state affine version is commonly called Gotoh’s refinement.

Local alignment: Smith–Waterman with affine gaps

A local alignment chooses the best-scoring pair of substrings. Set M[0,j]=M[i,0]=0M[0,j]=M[i,0]=0 and keep the two gap-state borders at -\infty. Replace the paired-state recurrence by

M[i,j]=max(0,s(ai,bj)+max{M[i1,j1],GB[i1,j1],GA[i1,j1]}). M[i,j]=\max\left(0, s(a_i,b_j)+\max\{M[i-1,j-1],G_B[i-1,j-1],G_A[i-1,j-1]\} \right).

The gap recurrences are unchanged. The zero candidate starts a new local alignment, and the answer is the maximum of 0 and all three states over all cells. Traceback begins at that best cell and stops at the restart marker. If no nonempty alignment has positive score, the conventional answer is the empty alignment with score zero.

This is why local alignment is naturally expressed with scores rather than nonnegative edit costs: if empty substrings were allowed while minimising cost, zero would always be an uninformative optimum.

Pseudocode and traceback

affine_alignment(A, B, score, gap_open, gap_extend, local):
    fill M, GB, GA with -infinity
    create matching predecessor tables PM, PGB, PGA

    if local:
        M[0,j] = 0, PM[0,j] = STOP for every j
        M[i,0] = 0, PM[i,0] = STOP for every i
    else:
        M[0,0] = 0
        for i = 1 .. n:
            GB[i,0] = -gap_open-(i-1)*gap_extend
            PGB[i,0] = M if i = 1 else GB
        for j = 1 .. m:
            GA[0,j] = -gap_open-(j-1)*gap_extend
            PGA[0,j] = M if j = 1 else GA

With the boundaries in place, fill all interior cells and remember which state attained each maximum:

    best = (0, STOP, 0, 0)
    for i = 1 .. n:
        for j = 1 .. m:
            GB[i,j], PGB[i,j] = maximum of
                (M[i-1,j]-gap_open, M),
                (GA[i-1,j]-gap_open, GA),
                (GB[i-1,j]-gap_extend, GB)

            GA[i,j], PGA[i,j] = maximum of
                (M[i,j-1]-gap_open, M),
                (GB[i,j-1]-gap_open, GB),
                (GA[i,j-1]-gap_extend, GA)

            previous, PM[i,j] = maximum of
                (M[i-1,j-1], M),
                (GB[i-1,j-1], GB),
                (GA[i-1,j-1], GA)
            M[i,j] = previous + score(A[i], B[j])
            if local and M[i,j] < 0: M[i,j], PM[i,j] = 0, STOP

            if local: update best from M[i,j], GB[i,j], and GA[i,j]

    endpoint = best if local else maximum of M[n,m], GB[n,m], GA[n,m]
    traceback from endpoint, following the stored state:
        M  outputs A[i] with B[j] and moves diagonally
        GB outputs A[i] with '-'  and moves upward
        GA outputs '-'  with B[j] and moves leftward
        stop at (0,0) globally or at STOP locally
    reverse and return the two output rows and their score

One predecessor per state reconstructs one optimum. Keeping every tied predecessor represents all optimum alignments, whose number can be exponential.

Worked comparison

Use match score +2+2, mismatch score 1-1, go=2g_o=2, and ge=1g_e=1 for

A = ACGTT
B = ACT

One optimum global alignment is

ACGTT
AC--T

It has three matches and one gap of length two, so its score is

32(2+(21)1)=3.3\cdot2-\bigl(2+(2-1)\cdot1\bigr)=3.

A local alignment may discard poorly scoring ends. Aligning AC with AC scores 4, which exceeds the best global score because the local problem need not explain every symbol.

For correctness, inspect the last alignment column. A paired column comes from one of the three diagonal states. A gap column either opens a new gap or extends the corresponding gap state. The recurrences therefore construct only valid alignments and enumerate every possible last case. Induction on i+ji+j proves every state optimal; the local restart and maximum over all cells additionally enumerate every substring start and end.

There are three states per prefix pair and constant work per transition, so both variants take Θ(nm)\Theta(nm) time. Full values and traceback pointers use Θ(nm)\Theta(nm) space. Scores alone need only O(min{n,m})O(\min\{n,m\}) working space, but ordinary traceback requires retained pointers or a more advanced reconstruction method.

Searching inside a longer text

Pairwise distance forces both complete strings to participate. To match pattern PP against any substring ending in text prefix T[1..j]T[1..j], keep the same recurrence but initialise

D[0,j]=0for every j,D[i,0]=i.D[0,j]=0\quad\text{for every }j,\qquad D[i,0]=i.

The free top row allows the alignment to start after any text prefix. A value D[m,j]kD[m,j]\le k reports a match ending at jj. Traceback until row zero recovers a corresponding start. Reporting every tied start may require following several predecessor paths and must include output size in the complexity. This initialisation will be used again in the approximate-matching note.

Dynamic time warping

DTW aligns numeric sequences that may progress at different speeds. Let local discrepancy be d(xi,yj)0d(x_i,y_j)\ge0, for example |xiyj||x_i-y_j| or squared Euclidean distance. For global alignment define

W[0,0]=0,W[i,0]=W[0,j]= for i,j>0, W[0,0]=0,\qquad W[i,0]=W[0,j]=\infty\text{ for }i,j>0,

and

W[i,j]=d(xi,yj)+min{W[i1,j],W[i,j1],W[i1,j1]}. W[i,j]=d(x_i,y_j)+\min\{W[i-1,j],W[i,j-1],W[i-1,j-1]\}.

The three moves produce a monotone path: each observation is used in order, while a horizontal or vertical move repeats an alignment partner. This is why DTW can align a stretched peak that Euclidean distance at equal timestamps treats as different.

For X=(1,2,3)X=(1,2,3) and Y=(1,1,2,3)Y=(1,1,2,3) with absolute difference, an optimal path pairs the first 1 in XX with both initial 1s in YY, then pairs 2 and 3 directly. Its total cost is 0. A traceback through the minimum predecessors reveals this warping path.

The unrestricted table takes Θ(|X||Y|)\Theta(|X||Y|) time. A Sakoe–Chiba window permits only cells satisfying |ij|w|i-j|\le w after the sequences have been put on a compatible time scale. It reduces work to O(wmax{|X|,|Y|})O(w\max\{|X|,|Y|\}) for comparable lengths, but it changes the feasible set. For global DTW one must have at least w||X||Y||w\ge\bigl||X|-|Y|\bigr|; an arbitrary narrow window can exclude the correct alignment.

Raw DTW tends to grow with sequence length and is not generally a metric. Applications should specify local cost, permitted steps, window, endpoint rules, and any normalisation by path length. Subsequence DTW uses free or selected boundary conditions when a short query is sought inside a long signal; that is a different problem from global alignment.

Matrix-chain multiplication

The product A1A2AnA_1A_2\cdots A_n has fixed matrix order, but associativity permits different parenthesisations with very different costs. If AiA_i has dimensions pi1×pip_{i-1}\times p_i, define M[i,j]M[i,j] as the minimum number of scalar multiplications needed for AiAjA_i\cdots A_j.

The base case is M[i,i]=0M[i,i]=0. If the final multiplication splits after AkA_k, its cost is

M[i,k]+M[k+1,j]+pi1pkpj.M[i,k]+M[k+1,j]+p_{i-1}p_kp_j.

Therefore

M[i,j]=minik<j{M[i,k]+M[k+1,j]+pi1pkpj}. M[i,j]=\min_{i\le k<j}\{M[i,k]+M[k+1,j]+p_{i-1}p_kp_j\}.

Fill intervals by increasing length and store the best split in S[i,j]S[i,j]:

for i = 1 .. n: M[i,i] = 0
for length = 2 .. n:
    for i = 1 .. n-length+1:
        j = i+length-1
        M[i,j] = infinity
        for k = i .. j-1:
            candidate = M[i,k] + M[k+1,j] + p[i-1]*p[k]*p[j]
            if candidate < M[i,j]:
                M[i,j], S[i,j] = candidate, k

For dimensions 10×3010\times30, 30×530\times5, and 5×605\times60, computing (A1A2)A3(A_1A_2)A_3 costs 10305+10560=450010\cdot30\cdot5+10\cdot5\cdot60=4500. Computing A1(A2A3)A_1(A_2A_3) costs 30560+103060=2700030\cdot5\cdot60+10\cdot30\cdot60=27000.

There are O(n2)O(n^2) intervals and O(n)O(n) possible splits per interval, giving O(n3)O(n^3) time and O(n2)O(n^2) space. Correctness follows because the root multiplication of every full parenthesisation has one split kk; if either side were not optimally parenthesised, replacing it would improve the whole solution.

Time, space, and reconstruction trade-offs

Rolling arrays are valid only when transitions use a bounded number of recent layers. They may destroy information needed to reconstruct a witness. Alternatives include retaining predecessor decisions, recomputing selected regions, or using divide-and-conquer reconstruction. State explicitly whether a bound is for the optimum value, one witness, or all witnesses.

Memoisation and tabulation both require a well-founded dependency relation in the standard DP setting. If a formulation has genuine cycles, a simple recursive table is not enough; it may instead be a shortest-path, fixed-point, or iterative-relaxation problem.

Connections

Common mistakes

Self-check

  1. What are the state, transition, boundaries, answer, and evaluation order for edit distance?
  2. Why does changing the entire top row to zero turn pairwise distance into substring search?
  3. Which assumptions make weighted edit distance a metric?
  4. Why do affine gaps require separate paired and gap states?
  5. Which initialisation and endpoint rules distinguish global from local alignment?
  6. Why can DTW distance be zero for sequences of different lengths?
  7. Derive both parenthesisation costs for matrices of dimensions 5×105\times10, 10×310\times3, and 3×123\times12.
  8. Give the induction parameter for a correctness proof of matrix-chain DP.

Revision summary

Implementations and hands-on exploration

Small experiments

  1. Implement memoised and bottom-up unit-cost edit distance. For every pair of strings over {a,b} of length at most 4, assert equal answers and compare memoisation misses with the number of table cells actually filled.
  2. Reproduce the ACGTT versus ACT example with Biopython or Parasail using the note’s gap convention. Then vary only the extension penalty and record when the optimal alignment or score changes.

Sources and further study

References

Cormen, Thomas H., Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. 2022. Introduction to Algorithms. 4th ed. MIT Press.
Kleinberg, Jon, and Éva Tardos. 2006. Algorithm Design. Pearson.