13.6 Two-Dimensional Dynamic Programming

One-dimensional dynamic programming models progress along a single axis.

13.6 Two-Dimensional Dynamic Programming

One-dimensional dynamic programming models progress along a single axis. Many problems, however, involve two independent dimensions. A string can be compared against another string. A path can move across rows and columns of a grid. A resource allocation problem may track both items processed and capacity remaining.

In such situations, a single state variable is insufficient. The solution requires a state that captures progress in two directions simultaneously. This leads naturally to two-dimensional dynamic programming.

The techniques introduced in this section appear throughout algorithm design. Longest common subsequence, edit distance, knapsack, sequence alignment, matrix path problems, and many graph algorithms rely on two-dimensional state spaces. Understanding how to design, visualize, and optimize these state spaces is a critical step toward more advanced dynamic programming methods.

Problem

How do we design and implement dynamic programming solutions whose states depend on two variables?

Understanding Two-Dimensional States

A typical two-dimensional state has the form:

$$ dp[i][j] $$

The variables may represent:

  • row and column
  • two sequence positions
  • item count and capacity
  • start and end positions
  • time and resource level

Unlike one-dimensional dynamic programming, where states form a line, two-dimensional states form a grid.

For example:

dp[i][j]

might represent:

best solution using
first i elements of A
and first j elements of B

Each coordinate identifies a unique subproblem.

Example: Grid Path Counting

Consider an (m \times n) grid.

You begin at the upper-left corner and may move only:

  • right
  • down

How many paths lead to cell ((i,j))?

State:

$$ dp[i][j] $$

Meaning:

number of paths
to reach cell (i,j)

To arrive at ((i,j)), the previous step must come from:

  • above
  • left

Therefore:

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

Base conditions:

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

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

Implementation:

for i := 1; i < rows; i++ {
    for j := 1; j < cols; j++ {
        dp[i][j] =
            dp[i-1][j] +
            dp[i][j-1]
    }
}

This example demonstrates the most fundamental two-dimensional dependency pattern.

Visualizing the State Space

A two-dimensional DP table can be viewed as a coordinate system.

(0,0) (0,1) (0,2)

(1,0) (1,1) (1,2)

(2,0) (2,1) (2,2)

Each cell represents a subproblem.

The recurrence determines dependency directions.

For grid paths:

      ↑

left → current

Because dependencies point upward and leftward, a row-major traversal naturally satisfies all requirements.

Example: Minimum Path Sum

Suppose each grid cell contains a cost.

Goal:

minimum cost
from start to finish

State:

$$ dp[i][j] $$

Meaning:

minimum cost
to reach cell (i,j)

Recurrence:

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

The structure is identical to path counting.

Only the aggregation operator changes:

  • counting uses addition
  • optimization uses minimum

This pattern appears repeatedly in dynamic programming.

Example: Longest Common Subsequence

Given strings:

A
B

State:

$$ dp[i][j] $$

Meaning:

length of the LCS
between prefixes

A[0...i)
B[0...j)

Suppose:

A[i-1] == B[j-1]

Then:

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

Otherwise:

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

Notice the dependency pattern:

upper-left
up
left

This triangular dependency structure is common in sequence comparison problems.

Implementation:

for i := 1; i <= n; i++ {
    for j := 1; j <= m; j++ {

        if a[i-1] == b[j-1] {
            dp[i][j] =
                dp[i-1][j-1] + 1
        } else {
            dp[i][j] =
                max(
                    dp[i-1][j],
                    dp[i][j-1],
                )
        }
    }
}

Example: Edit Distance

The edit distance problem extends the LCS idea.

Allowed operations:

  • insert
  • delete
  • replace

State:

$$ dp[i][j] $$

Meaning:

minimum edits
to transform

A[0...i)

into

B[0...j)

If characters match:

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

Otherwise:

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

The recurrence directly models the three possible operations.

This illustrates an important principle:

A recurrence often mirrors the legal operations of the problem.

Example: 0/1 Knapsack

State:

$$ dp[i][w] $$

Meaning:

maximum value
using first i items
with capacity w

Two decisions exist:

Skip Item

$$ dp[i-1][w] $$

Take Item

$$ dp[i-1][w-weight_i] + value_i $$

Recurrence:

$$ dp[i][w] = \max ( dp[i-1][w], dp[i-1][w-weight_i]+value_i ) $$

Unlike grid problems, the second dimension represents a resource rather than a position.

The underlying dynamic programming principles remain unchanged.

Dependency Direction

Every two-dimensional recurrence defines movement through the table.

Common patterns include:

Left and Up

←
↑

Examples:

  • grid paths
  • edit distance
  • LCS

Previous Row

Examples:

  • knapsack
  • scheduling

Diagonal

Examples:

  • sequence matching
  • string alignment

Understanding these directions determines iteration order.

Initialization Strategies

Most bugs in two-dimensional dynamic programming occur near table boundaries.

For LCS:

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

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

For grid paths:

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

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

For edit distance:

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

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

The correct initialization follows directly from the meaning of the state.

A useful habit is to write the state definition first and derive boundary conditions afterward.

Memory Layout

A two-dimensional table:

dp := make([][]int, rows)

typically requires:

$$ O(rows \times cols) $$

memory.

For large state spaces, memory consumption may dominate running time.

Understanding dependencies often reveals opportunities for compression.

Row Compression

Consider:

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

Only the current row and previous row may be needed.

Instead of storing:

all rows

store:

previous row
current row

Memory decreases from:

$$ O(nm) $$

to:

$$ O(m) $$

This optimization is extremely common in production implementations.

Common Mistakes

Incorrect State Meaning

Example:

dp[i][j]

sometimes interpreted as:

first i elements

and elsewhere as:

up to index i

These meanings differ by one position and frequently produce off-by-one errors.

Wrong Iteration Order

Dependencies must be computed before use.

Always verify dependency directions before writing loops.

Boundary Access

Expressions such as:

dp[i-1][j]

become invalid when:

i = 0

Initialization should eliminate such cases.

Excessive State Dimensions

Many beginners introduce unnecessary variables.

Ask:

Can one dimension be derived from the others?

If yes, remove it.

Complexity Analysis

Suppose:

$$ n $$

possible values for the first dimension and:

$$ m $$

possible values for the second.

State count:

$$ O(nm) $$

If each state performs constant work:

$$ O(nm) $$

time.

Memory:

$$ O(nm) $$

before optimization.

These formulas appear repeatedly throughout dynamic programming literature.

Takeaway

Two-dimensional dynamic programming extends the ideas of one-dimensional state spaces to problems involving two independent variables. Each state represents a coordinate in a grid of subproblems, and recurrence relations describe how information flows through that grid. Many of the most important dynamic programming algorithms, including longest common subsequence, edit distance, knapsack, and grid optimization problems, rely on this model. Once state meanings, dependency directions, and boundary conditions are clearly defined, the resulting implementations often become straightforward and highly systematic.