13.20 Dynamic Programming Design Patterns and Problem-Solving Framework

Throughout this chapter, we have studied many different forms of dynamic programming: * one-dimensional DP * two-dimensional DP * knapsack DP

13.20 Dynamic Programming Design Patterns and Problem-Solving Framework

Throughout this chapter, we have studied many different forms of dynamic programming:

  • one-dimensional DP
  • two-dimensional DP
  • knapsack DP
  • longest common subsequence
  • longest increasing subsequence
  • edit distance
  • interval DP
  • tree DP
  • bitmask DP
  • probability DP
  • counting DP
  • optimization DP
  • convex hull trick
  • divide-and-conquer optimization
  • Knuth optimization

At first glance, these techniques may appear unrelated. In practice, experienced programmers approach them using the same underlying process.

The difference between beginners and experts is rarely knowledge of more recurrences. Experts recognize patterns quickly and systematically transform a problem into states, transitions, and base cases.

This final section presents a practical framework for designing dynamic programming solutions.

Problem

How do we recognize dynamic programming opportunities and systematically derive correct solutions?

Step 1: Identify the Subproblem

Every dynamic programming solution begins with a question:

What smaller version of the problem should be solved?

For example:

first i elements

first i and j characters

interval [l,r]

subtree rooted at node u

subset represented by mask

The subproblem should resemble the original problem as closely as possible.

Good subproblems allow recursion to emerge naturally.

Bad subproblems create awkward transitions.

Example: Fibonacci

Question:

What is Fibonacci(n)?

Smaller versions:

Fibonacci(n-1)

Fibonacci(n-2)

State:

$$ dp[i] $$

The state follows directly from the smaller problem definition.

Step 2: Define the State Precisely

The state is the most important part of a dynamic programming solution.

A state must contain all information required for future decisions.

Nothing more.

Nothing less.

Examples:

Coin Change

$$ dp[x] $$

Meaning:

minimum coins
needed for amount x

LCS

$$ dp[i][j] $$

Meaning:

LCS length
between first i characters
and first j characters

Knapsack

$$ dp[i][w] $$

Meaning:

maximum value
using first i items
with capacity w

Write the state meaning in plain English before writing any recurrence.

This simple habit prevents many mistakes.

Step 3: Determine the Final Decision

Most recurrences arise from examining the last decision.

Ask:

What must happen immediately before this state is reached?

Examples:

Staircase

Final move:

1 step

or

2 steps

Edit Distance

Final operation:

insert

delete

replace

match

Interval DP

Final action:

choose split point k

Tree DP

Final action:

combine child solutions

The recurrence often emerges directly from this question.

Step 4: Derive the Transition

Once the final decision is known, express the state in terms of smaller states.

Examples:

Fibonacci

$$ dp[i] = dp[i-1] + dp[i-2] $$

Coin Change

$$ dp[x] = 1 + \min(dp[x-c]) $$

LCS

$$ dp[i][j] = \max ( dp[i-1][j], dp[i][j-1] ) $$

or

$$ dp[i-1][j-1]+1 $$

Knapsack

$$ dp[i][w] = \max ( skip, take ) $$

Transitions are the mathematical expression of decisions.

Step 5: Determine Base Cases

Base cases terminate recursion.

Ask:

What are the smallest possible subproblems?

Examples:

Fibonacci

$$ dp[0]=0 $$

$$ dp[1]=1 $$

LCS

$$ dp[0][j]=0 $$

$$ dp[i][0]=0 $$

Edit Distance

$$ dp[0][j]=j $$

$$ dp[i][0]=i $$

Many incorrect solutions have correct transitions but incorrect base cases.

Step 6: Choose Computation Order

Dependencies determine evaluation order.

Examples:

One-Dimensional DP

left to right

Interval DP

short intervals
to long intervals

Tree DP

children before parent

DAG DP

topological order

A useful rule:

Compute dependencies before dependents.

Everything else follows from this principle.

Step 7: Verify State Coverage

Ask:

Does every valid solution correspond to a state path?

and:

Does every state path correspond to a valid solution?

This prevents:

  • missing solutions
  • invalid solutions
  • double counting

Verification is often more important than implementation.

Common Dynamic Programming Patterns

Most problems fit a small number of recurring templates.

Pattern 1: Linear Progression

State:

$$ dp[i] $$

Examples:

  • Fibonacci
  • staircase
  • coin change
  • house robber

Clue:

position

length

amount

time

Pattern 2: Sequence Comparison

State:

$$ dp[i][j] $$

Examples:

  • LCS
  • edit distance
  • sequence alignment

Clue:

two strings

two sequences

Pattern 3: Resource Allocation

State:

$$ dp[i][capacity] $$

Examples:

  • knapsack
  • scheduling
  • budgeting

Clue:

resource constraint

Pattern 4: Interval Optimization

State:

$$ dp[l][r] $$

Examples:

  • matrix chain multiplication
  • file merging
  • burst balloons

Clue:

split interval

Pattern 5: Tree Aggregation

State:

$$ dp[node] $$

or:

$$ dp[node][state] $$

Examples:

  • subtree calculations
  • independent set
  • tree diameter

Clue:

hierarchical structure

Pattern 6: Subset Selection

State:

$$ dp[mask] $$

or:

$$ dp[mask][i] $$

Examples:

  • TSP
  • assignment
  • Hamiltonian path

Clue:

small set
of choices

Pattern 7: Counting

State:

number of ways

Transition:

addition

Clue:

how many?

Pattern 8: Optimization

State:

best value

Transition:

min

or

max

Clue:

best

largest

smallest

Top-Down Versus Bottom-Up

Every dynamic programming solution can be expressed in two forms.

Top-Down

Use recursion and memoization.

func solve(state) {

    if memoized {
        return value
    }

    compute value

    return value
}

Advantages:

  • natural derivation
  • easy implementation

Disadvantages:

  • recursion overhead
  • stack limitations

Bottom-Up

Build states iteratively.

Advantages:

  • predictable performance
  • easier optimization

Disadvantages:

  • requires explicit ordering

Both approaches compute the same states.

Space Optimization Checklist

After obtaining a correct solution, ask:

Are Older States Needed?

Example:

$$ dp[i] = dp[i-1] + dp[i-2] $$

Only two values are needed.

Are Entire Rows Needed?

Example:

$$ dp[i][j] $$

depends only on:

previous row

Use rolling arrays.

Can State Dimensions Be Removed?

Example:

current item index

may be derivable from:

number of selected elements

Removing dimensions often produces the largest gains.

Debugging Dynamic Programming

When a solution fails:

Verify State Meaning

Many bugs originate from ambiguous states.

Check Base Cases

Most incorrect tables fail immediately near boundaries.

Trace Small Inputs

Compute:

n = 1

n = 2

n = 3

by hand.

Visual inspection often reveals incorrect transitions.

Compare with Brute Force

For small inputs:

generate all solutions

compare answers

This technique catches subtle errors quickly.

Recognizing Dynamic Programming

Strong indicators include:

optimal value

number of ways

overlapping subproblems

repeated calculations

small state space

recursive structure

Ask:

If I solve a smaller version of this problem, can it help solve a larger version?

If the answer is yes, dynamic programming may be applicable.

Dynamic Programming Versus Other Techniques

Not every optimization problem requires dynamic programming.

Technique Typical Clue
Greedy local choice always optimal
Divide and Conquer independent subproblems
Dynamic Programming overlapping subproblems
Backtracking exhaustive search
Graph Algorithms path or connectivity structure
Binary Search monotonic answer space

Choosing the correct paradigm is often more important than implementation details.

A Dynamic Programming Checklist

Before coding, answer these questions:

  1. What is the state?
  2. What does the state mean?
  3. What is the final answer state?
  4. What decisions lead into the state?
  5. What is the recurrence?
  6. What are the base cases?
  7. What computation order satisfies dependencies?
  8. Can memory be reduced?
  9. Can transitions be optimized?
  10. How can correctness be verified?

If all ten questions have clear answers, implementation is usually straightforward.

Takeaway

Dynamic programming is not a collection of isolated tricks but a systematic method for solving problems with overlapping subproblems and optimal substructure. Whether the state represents a position, a pair of strings, an interval, a subtree, or a subset, the design process remains remarkably consistent: define the subproblem, identify the final decision, derive the recurrence, establish base cases, and compute states in dependency order. Mastering these patterns allows you to move beyond memorizing individual algorithms and begin designing dynamic programming solutions for entirely new problems. This ability is the true goal of studying dynamic programming.