AnyLearn
All lessons
Computer Scienceintermediate

Optimal Substructure: The Property Both Techniques Need

Dynamic programming and greedy algorithms both rest on a structural property the problem either has or does not have. This lesson establishes it precisely, shows a problem that lacks it, and separates the three properties that decide which technique applies.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

Two ways of not enumerating

Most interesting computational problems are optimisation problems: among exponentially many candidates, find the best one. Choosing a subset of nn jobs means searching 2n2^n subsets. Aligning two strings of length nn means searching an astronomical number of alignments. Enumeration is not an option.

Dynamic programming and greedy algorithms are two ways of not enumerating. Both make the same underlying move: express the answer in terms of answers to smaller versions of the same problem, so an exponential search collapses into a polynomial number of reusable pieces.

Neither works on every problem. Both rest on a structural property that the problem either has or does not have, and the real skill is recognising that property rather than memorising recurrences. This lesson establishes the property. The rest of the path builds both techniques on top of it.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. Two ways of not enumerating

Most interesting computational problems are optimisation problems: among exponentially many candidates, find the best one. Choosing a subset of nn jobs means searching 2n2^n subsets. Aligning two strings of length nn means searching an astronomical number of alignments. Enumeration is not an option.

Dynamic programming and greedy algorithms are two ways of not enumerating. Both make the same underlying move: express the answer in terms of answers to smaller versions of the same problem, so an exponential search collapses into a polynomial number of reusable pieces.

Neither works on every problem. Both rest on a structural property that the problem either has or does not have, and the real skill is recognising that property rather than memorising recurrences. This lesson establishes the property. The rest of the path builds both techniques on top of it.

2. Optimal substructure, stated precisely

A problem has optimal substructure when an optimal solution to it contains, inside it, optimal solutions to its subproblems.

Shortest paths are the canonical example. Suppose the shortest route from A to Z happens to pass through M. Then the A-to-M portion must itself be a shortest A-to-M route. If it were not, you could cut it out, paste a shorter one in its place, and end up with a shorter A-to-Z route, contradicting the assumption that you started with the best one.

That cut-and-paste argument is the standard proof of optimal substructure, and it is worth performing rather than assuming. The property is what licenses a recurrence: once you know an optimal solution decomposes into optimal sub-solutions, you can define the answer in terms of smaller answers, and that recurrence is your algorithm.

3. A problem that does not have it

The property is not automatic. Change shortest path to longest simple path, a path that visits no vertex twice, and it disappears.

Take four vertices in a square: edges A-B, B-C, C-D, D-A. The longest simple path from A to C through B is A, B, C. Its A-to-B piece is the single edge A-B, which is emphatically not the longest simple A-to-B path: that would be A, D, C, B. Try the cut-and-paste substitution and you get A, D, C, B, C, which visits C twice and is not a path at all.

The substitution fails because the subproblems are not independent. The A-to-B leg and the B-to-C leg compete for one shared resource, the pool of unvisited vertices. Shortest path has no such coupling. Longest simple path is NP-hard, and the missing property is a genuine signal of that difficulty.

4. Overlap: what separates DP from divide and conquer

Optimal substructure alone does not make dynamic programming worthwhile. Merge sort has it: sorting an array reduces to sorting each half and merging the results. But merge sort's subproblems are disjoint. The left half and the right half share nothing, so there is nothing to reuse and no table worth building. That is divide and conquer.

Dynamic programming needs a second property: overlapping subproblems, meaning the recursion reaches the same subproblem many times over.

The Fibonacci recurrence is the smallest illustration. Computing F(5)F(5) calls F(4)F(4) and F(3)F(3); then F(4)F(4) calls F(3)F(3) all over again. The recursion tree has exponentially many nodes, yet the number of distinct subproblems is only nn. Every additional node is pure recomputation, and once you see it that way the fix writes itself.

5. The recursion tree is really a DAG

Drawn as a tree, this recursion has exponentially many nodes. Drawn honestly, as a graph where identical subproblems are the same node, it has nn nodes and a handful of edges. Notice that F(3)F(3) has two parents and F(2)F(2) has two parents: those shared nodes are the overlap.

Every dynamic program is a directed acyclic graph of subproblems. The nodes are the states, the edges are the dependencies, and the answer flows from the leaves upward. That reframing is the whole technique, and the two standard implementations are just two ways of walking this graph: memoisation explores it lazily from the top, tabulation fills it in topological order from the bottom.

flowchart TD
A["F(5)"] --> B["F(4)"]
A --> C["F(3)"]
B --> C
B --> D["F(2)"]
C --> D
C --> E["F(1)"]
D --> E
D --> F["F(0)"]

6. Memoisation: the mechanical fix

Memoisation caches each subproblem's answer the first time it is computed. Here is edit distance, the minimum number of insertions, deletions and substitutions turning one string into another, following the recurrence Wagner and Fischer published in the Journal of the ACM in 1974:

from functools import lru_cache

def edit_distance(a, b):
    @lru_cache(maxsize=None)
    def d(i, j):
        if i == 0: return j
        if j == 0: return i
        sub = 0 if a[i - 1] == b[j - 1] else 1
        return min(d(i - 1, j) + 1,      # delete
                   d(i, j - 1) + 1,      # insert
                   d(i - 1, j - 1) + sub)  # substitute
    return d(len(a), len(b))

The decorator is the entire optimisation. Without it the function is exponential. With it there are only a×b|a| \times |b| distinct argument pairs and each costs constant work beyond its recursive calls, so the running time is O(nm)O(nm).

7. Tabulation walks the same graph bottom up

Tabulation allocates the table up front and fills it in an order that guarantees every dependency is ready before it is needed. For edit distance that means looping ii from 0 upward and jj from 0 upward, because cell (i,j)(i,j) depends only on cells above and to the left.

The two forms compute identical values, so choose on practical grounds:

  • Tabulation has no recursion overhead, cannot blow the call stack, and touches memory in a predictable pattern that the cache prefetcher likes. It is usually faster.
  • Memoisation only ever visits states the problem actually reaches. When the table is large but sparsely used, that is the difference between feasible and not, and it saves you from working out a valid fill order by hand.

A recursion depth limit is the practical tell: if the natural recursion is thousands deep, tabulate.

8. The greedy-choice property is a stronger demand

Greedy algorithms need a third property, and it asks for much more. The greedy-choice property holds when a globally optimal solution can be reached by making a locally optimal choice, without consulting the subproblems at all.

Stated in dynamic programming terms, that is unusually sharp. A DP recurrence takes a minimum or maximum over several options, and it evaluates all of them because it does not know in advance which wins. Greedy applies exactly when you can prove ahead of time which branch is always in the optimum. Then you never build a table: make the choice, recurse once, done.

This is why greedy algorithms are so much faster and so much rarer. Dynamic programming spends polynomial time to avoid needing to know. Greedy converts that cost into a proof obligation.

9. The three properties, side by side

TechniqueOptimal substructureOverlapping subproblemsGreedy-choice property
Divide and conquerRequiredAbsentNot needed
Dynamic programmingRequiredPresentNot needed
GreedyRequiredIrrelevantRequired

The asymmetry in the last column is the most useful thing on this page. Greedy demands strictly more than DP, so any problem greedy solves, DP also solves, usually more slowly. The reverse is false. When you are unsure, dynamic programming is the safe answer and greedy is a speedup you earn by proving something.

And testing on examples proves nothing. Making change greedily is optimal with US coin denominations, but with denominations of 1, 3 and 4, making 6 greedily yields 4 + 1 + 1, three coins, while the optimum is 3 + 3, two coins.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Merge sort has optimal substructure. Why is it not a dynamic programming algorithm?
    • Its subproblems are disjoint, so there is nothing to reuse
    • It lacks optimal substructure once you account for the merge step
    • Its recurrence has no minimum or maximum operation
    • It sorts in place, so a memo table cannot be attached
  2. Why does the longest simple path problem lack optimal substructure?
    • Longest paths can have infinite length in a cyclic graph
    • The subproblems are not independent: they compete for the same unvisited vertices
    • The cut-and-paste argument needs non-negative edge weights
    • There can be several longest paths, so the optimum is not unique
  3. In the edit distance code, what happens if the caching decorator is removed?
    • The result becomes incorrect because states are recomputed inconsistently
    • Nothing changes, since Python caches pure functions automatically
    • The running time grows from roughly n times m to exponential
    • It raises a recursion error immediately on any input
  4. Stated in dynamic programming terms, what does the greedy-choice property give you?
    • A guarantee that the table fits in linear space
    • A way to evaluate the recurrence in parallel across states
    • A proof that the subproblem graph is acyclic
    • Advance knowledge of which branch of the recurrence is optimal, so you never evaluate the others
  5. With coin denominations of 1, 3 and 4, what does the greedy algorithm return when making change for 6?
    • Two coins, 3 + 3, matching the optimum
    • Three coins, 4 + 1 + 1, while the optimum uses two
    • Two coins, 4 + 3, which overshoots the target
    • It fails to find any valid combination

Related lessons

Computer Science
advanced

Conditioning and Stability: Whose Fault Is the Error

Two different things can make a numerical answer inaccurate: the problem may amplify any input perturbation, or the algorithm may introduce its own. They are separate quantities, one belongs to the problem and one to the code, and only the second is fixable. This lesson separates them and shows what follows.

8 steps·~12 min
Computer Science
advanced

Cancellation: Where the Digits Actually Go

Every operation is correctly rounded, yet results can be wrong in the first digit. The reason is that subtracting nearly equal numbers is exact and still catastrophic: it does not create error, it promotes error that was already there. This lesson builds that mechanism and the summation techniques that recover the lost accuracy.

8 steps·~12 min
Computer Science
advanced

Count-Min, and Why Sketches Compose

The count-min sketch estimates how often an item occurred using a fixed grid of counters, and it never underestimates. This lesson builds it, states the error bound that makes it usable, shows where it is useless, and ends on the property shared by all three structures that explains why they run distributed systems.

8 steps·~12 min
Computer Science
advanced

HyperLogLog: Counting Distinct Items in Kilobytes

Counting distinct items exactly needs memory proportional to the count. HyperLogLog answers the same question in a fixed twelve kilobytes, for cardinalities into the billions, by measuring an improbable event rather than storing anything. This lesson builds that idea from the leading-zeros intuition up.

8 steps·~12 min