13.16 Optimization Dynamic Programming

Optimization dynamic programming is the form most programmers first associate with DP.

13.16 Optimization Dynamic Programming

Optimization dynamic programming is the form most programmers first associate with DP. The goal is not to count solutions or compute probabilities, but to find the best possible value among many choices.

The word "best" depends on the problem. It may mean minimum cost, maximum profit, shortest time, largest score, fewest edits, lowest risk, or greatest utility. The structure, however, is the same: each state records the optimal value for a subproblem, and each transition represents a legal decision.

This section consolidates the optimization pattern that has appeared throughout the chapter. Knapsack maximizes value. Edit distance minimizes operations. Matrix chain multiplication minimizes cost. Longest increasing subsequence maximizes length. These are different problems, but they share the same design method.

Problem

How do we design dynamic programming algorithms that choose the best solution among many valid alternatives?

The Optimization Pattern

A typical optimization state looks like:

$$ dp[state] $$

Meaning:

The best value achievable for this subproblem.

The recurrence usually has the form:

$$ dp[state] = \min_{choice} cost(choice) $$

or:

$$ dp[state] = \max_{choice} value(choice) $$

The state defines the subproblem.

The transition defines the choice.

The aggregation operator defines the objective.

Example: Minimum Coin Change

Given coin values:

1, 3, 4

Find the minimum number of coins needed to form amount x.

State:

$$ dp[x] $$

Meaning:

minimum number of coins
needed to form amount x

Transition:

choose the last coin

Recurrence:

$$ dp[x] = 1+ \min_{coin} dp[x-coin] $$

where:

$$ x-coin \ge 0 $$

Base case:

$$ dp[0]=0 $$

This is a minimization DP.

Implementation

func MinCoins(coins []int, target int) int {

    const inf = int(1e9)

    dp := make([]int, target+1)

    for x := 1; x <= target; x++ {
        dp[x] = inf
    }

    dp[0] = 0

    for x := 1; x <= target; x++ {

        for _, coin := range coins {

            if x >= coin {
                dp[x] = min(
                    dp[x],
                    dp[x-coin]+1,
                )
            }
        }
    }

    if dp[target] == inf {
        return -1
    }

    return dp[target]
}

The sentinel value inf represents an unreachable state.

Min Versus Max

Optimization DP comes in two common forms.

Minimization

Use when the objective asks for:

least cost

fewest steps

shortest distance

minimum penalty

Typical initialization:

infinity

Transition:

$$ dp[state] = \min(...) $$

Maximization

Use when the objective asks for:

largest profit

maximum score

longest sequence

greatest reward

Typical initialization:

negative infinity

Transition:

$$ dp[state] = \max(...) $$

Using the wrong default value is a common source of silent errors.

Example: Maximum Value Path

Given a grid of values, move only right or down. Find the maximum value collected from the top-left cell to the bottom-right cell.

State:

$$ dp[i][j] $$

Meaning:

maximum value collected
when reaching cell (i,j)

Recurrence:

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

The same grid structure used for path counting now becomes an optimization problem.

Only the aggregation changes.

Example: House Robber

Given a line of houses, each containing money, choose houses so that no two adjacent houses are selected.

State:

$$ dp[i] $$

Meaning:

maximum money obtainable
from the first i houses

At house (i), two choices exist.

Skip it:

$$ dp[i-1] $$

Take it:

$$ dp[i-2]+value[i] $$

Recurrence:

$$ dp[i] = \max ( dp[i-1], dp[i-2]+value[i] ) $$

This is the same include/exclude pattern used in knapsack and tree independent set.

Local Choice, Global Value

Optimization DP works because local choices are evaluated through already optimized subproblems.

For a transition:

$$ choice + dp[subproblem] $$

the subproblem value already assumes the best continuation.

This avoids enumerating complete solutions.

The recurrence considers only the immediate decision and delegates the rest to the DP table.

Optimal Substructure

Optimization DP requires optimal substructure.

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

For example, if the cheapest path to cell (i,j) comes from (i-1,j), then the path to (i-1,j) must also be cheapest. If it were not, replacing it with a cheaper path would improve the final solution.

This property is what makes dynamic programming valid.

Overlapping Subproblems

Optimization alone is not enough.

Dynamic programming is useful when the same subproblems appear repeatedly.

If every subproblem is unique, recursion may still be correct, but memoization provides little benefit.

Good DP candidates usually have both:

optimal substructure

overlapping subproblems

The first gives correctness.

The second gives efficiency.

Choice Enumeration

Many optimization recurrences follow this structure:

best := initialValue

for _, choice := range choices {

    if valid(choice) {

        candidate :=
            transitionCost(choice) +
            dp[nextState(choice)]

        best = better(best, candidate)
    }
}

dp[state] = best

This template appears in coin change, knapsack, interval DP, shortest paths on DAGs, scheduling, and many game problems.

Reconstructing the Optimum

The DP table stores the optimal value.

To recover the actual decision sequence, store which choice produced that value.

Example:

parent[state] = choice

or:

take[i][w] = true

During reconstruction, follow recorded choices backward from the final state.

Without this auxiliary information, you may know the value but not the solution.

Tie Handling

Sometimes several choices produce the same optimal value.

Examples:

multiple shortest paths

multiple optimal knapsack selections

multiple longest subsequences

Tie handling should be deliberate.

You may choose:

first solution

lexicographically smallest solution

fewest items

stable reconstruction order

The DP value alone does not specify which optimal solution to return.

Sentinel Values

Optimization DP often needs sentinel values for impossible states.

For minimization:

const inf = int(1e9)

For maximization:

const negInf = -int(1e9)

Be careful when adding to sentinels.

This is unsafe:

candidate := dp[x] + cost

if:

dp[x] == inf

Guard impossible states explicitly when overflow is possible.

Example: Weighted Interval Scheduling

Given jobs with:

start time

end time

profit

Select non-overlapping jobs with maximum total profit.

After sorting by end time, define:

$$ dp[i] $$

as:

maximum profit using first i jobs

For job (i):

Skip it:

$$ dp[i-1] $$

Take it:

$$ profit_i + dp[p(i)] $$

where (p(i)) is the last job that ends before job (i) starts.

Recurrence:

$$ dp[i] = \max ( dp[i-1], profit_i + dp[p(i)] ) $$

This is a production-grade example of optimization DP.

The only extra work is computing (p(i)), usually with binary search.

Optimization DP and Shortest Paths

Shortest paths in a DAG can be interpreted as optimization DP.

State:

$$ dp[u] $$

Meaning:

shortest distance
from source to node u

Transition:

$$ dp[v] = \min ( dp[v], dp[u]+weight(u,v) ) $$

processed in topological order.

This viewpoint helps connect dynamic programming with graph algorithms.

Dijkstra and Bellman-Ford generalize similar relaxation ideas to broader graph classes.

Common Mistakes

Using Greedy Choice Without Proof

Optimization DP often solves problems where greedy fails.

Do not replace DP with a local heuristic unless an exchange argument or matroid property proves correctness.

Losing Necessary State

If future decisions depend on information not stored in the state, the recurrence may appear plausible but produce incorrect answers.

Bad Initialization

Minimization requires large initial values.

Maximization often requires small initial values.

Base cases must reflect the state definition.

Failing to Reconstruct

A DP table gives the optimal value.

It does not automatically give the selected objects, path, or sequence.

Overflow with Sentinels

Adding costs to inf or negInf can corrupt results.

Guard impossible states.

Complexity Analysis

Let:

$$ S $$

be the number of states.

Let:

$$ K $$

be the number of choices per state.

Then:

$$ O(SK) $$

time is typical.

Memory is:

$$ O(S) $$

unless reconstruction, multiple layers, or full tables are required.

The usual optimization levers are:

reduce state count

reduce choices per state

compress memory

precompute transition helpers

Recognizing Optimization DP

Strong indicators include:

minimum

maximum

best

least

fewest

largest

optimal

choose among alternatives

cost or profit

If the problem also has overlapping subproblems and a reusable state definition, dynamic programming is a strong candidate.

Takeaway

Optimization dynamic programming stores the best achievable value for each subproblem and derives larger solutions by enumerating valid choices. The recurrence uses min or max, base cases describe trivial subproblems, and sentinel values represent unreachable states. This pattern unifies many classic algorithms, including coin change, knapsack, edit distance, weighted scheduling, grid path optimization, and shortest paths on DAGs. Once the state is correct, the main design task is to enumerate all legal choices without omission, duplication, or unnecessary state.