Algorithmics study and revision notes
2026-08-22
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.
After studying this note, you should be able to:
You should be comfortable with arrays, recursion, asymptotic notation, and directed acyclic graphs. We use one-based string notation such as and reserve row or column zero for the empty prefix. No earlier string-matching material is assumed.
For each new problem, write down the following items before writing code.
For example, Fibonacci numbers satisfy with and . 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 arithmetic operations and 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.
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.
Let be the minimum cost of transforming prefix into prefix . Under unit-cost insertion, deletion, and substitution, the last operation is exactly one of the following:
The boundaries are and . 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 states and constant work per state, so the running time is and the full table uses the same asymptotic space. If only the value is needed, one previous row and the current row use space after orienting the shorter string along a row.
Applications need not assign every operation cost 1. Let , , and be nonnegative costs, with . Replace the three added constants in the recurrence by these functions. A case-only change may cost , 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 , , and :
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.
For cat and cut, rows are prefixes of
cat and columns are prefixes of cut:
| c | u | t | ||
|---|---|---|---|---|
| 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.
Edit distance minimises a nonnegative cost. Biological sequence comparison is usually written as score maximisation: a substitution score 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 for every symbol pair in the chosen alphabet; it does not determine the gap model.
Let and . Let be the gap-opening penalty and the extension penalty. This note charges a gap of length by
Thus the first missing symbol costs and each later symbol in the same gap costs . Some software instead charges ; 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.
For prefixes and , define:
For use
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.
A global alignment consumes both complete sequences. Initialise impossible states to and set
For ,
and for ,
The optimum global score is
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.
A local alignment chooses the best-scoring pair of substrings. Set and keep the two gap-state borders at . Replace the paired-state recurrence by
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.
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.
Use match score , mismatch score , , and 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
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 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 time. Full values and traceback pointers use space. Scores alone need only working space, but ordinary traceback requires retained pointers or a more advanced reconstruction method.
Pairwise distance forces both complete strings to participate. To match pattern against any substring ending in text prefix , keep the same recurrence but initialise
The free top row allows the alignment to start after any text prefix. A value reports a match ending at . 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.
DTW aligns numeric sequences that may progress at different speeds. Let local discrepancy be , for example or squared Euclidean distance. For global alignment define
and
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
and
with absolute difference, an optimal path pairs the first 1
in
with both initial 1s in
,
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 time. A Sakoe–Chiba window permits only cells satisfying after the sequences have been put on a compatible time scale. It reduces work to for comparable lengths, but it changes the feasible set. For global DTW one must have at least ; 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.
The product has fixed matrix order, but associativity permits different parenthesisations with very different costs. If has dimensions , define as the minimum number of scalar multiplications needed for .
The base case is . If the final multiplication splits after , its cost is
Therefore
Fill intervals by increasing length and store the best split in :
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 , , and , computing costs . Computing costs .
There are intervals and possible splits per interval, giving time and space. Correctness follows because the root multiplication of every full parenthesisation has one split ; if either side were not optimally parenthesised, replacing it would improve the whole solution.
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.
functools.cache
turns a pure recursive recurrence into an unbounded memoised function.
Arguments must be hashable, and the cache retains arguments and results
until cleared; use cache_info() to measure hits and
misses.PairwiseAligner exposes global and local alignment,
substitution matrices, affine gaps, and separate end-gap scores. Its
documented formula open + (length-1)*extend matches this
note’s convention when penalties are supplied as negative scores. Set
every relevant score explicitly: its defaults do not represent a
generally accepted biological model.gap_cost_affine
scores a gap as open_score + length*extension_score, not
with this note’s parameter names. To represent penalty
,
use extension_score = -g_e and
open_score = -g_o+g_e, then configure free ends
separately.numpy.linalg.multi_dot
selects an efficient parenthesisation and performs the product. It
returns the numerical result, not the DP cost table or split tree, so
instrument your own recurrence when studying reconstruction.{a,b} of length at most 4, assert
equal answers and compare memoisation misses with the number of table
cells actually filled.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.