James A Rosen

Staff Engineer, AI Systems

James A Rosen, pixelated, wearing a bow tie

2D Dynamic Programming (LCS)

2D Dynamic Programming

Overview

Session opened with two spaced-repetition reviews: K Closest Points (heaps) and Walls & Gates (multi-source BFS). Both patterns were recognized correctly. New material introduced 2D DP via Longest Common Subsequence — James derived the recurrence independently and filled the table correctly on the first try. Implementation had three bugs: grid sizing offset, > vs < loop condition, and a ^/** confusion.

Concept

2D DP: extend 1D DP to two dimensions. dp[i][j] represents the answer to a subproblem over two inputs simultaneously — typically two strings or a grid.

LCS recurrence: dp[i][j] = length of LCS of s[0..i-1] and t[0..j-1] (1-indexed into an (s.length+1) × (t.length+1) table with a zero base-case row/column).

if s[i-1] === t[j-1]:  dp[i][j] = dp[i-1][j-1] + 1
else:                  dp[i][j] = max(dp[i-1][j], dp[i][j-1])

Complexity: O(m × n) time and space.

Implementation

James’s implementation with bugs corrected:

function lcs(s: string, t: string): number {
  // Grid is (s.length+1) × (t.length+1) to accommodate empty-string base cases.
  // Row 0 and col 0 are all zeros — LCS with an empty string is always 0.
  const grid: number[][] = Array.from({ length: s.length + 1 }, () =>
    new Array(t.length + 1).fill(0)
  )

  for (let i = 1; i <= s.length; i++) {
    for (let j = 1; j <= t.length; j++) {
      // Bug fixed: was j > grid[i].length
      if (s[i - 1] === t[j - 1]) {
        // Bug fixed: index offset from base-case row/col
        grid[i][j] = grid[i - 1][j - 1] + 1
      } else {
        grid[i][j] = Math.max(grid[i - 1][j], grid[i][j - 1])
      }
    }
  }

  return grid[s.length][t.length]
}

Bugs caught:

  1. Grid was s.length rows — needs s.length + 1 to fit the zero base-case row; same for columns. Comparison inside the loop must use s[i-1] / t[j-1].
  2. Inner loop used j > grid[i].length (loop never ran) — should be j < or j <=.
  3. s[i]^2 uses bitwise XOR, not exponentiation — use s[i]**2 or s[i] * s[i].

Problems

K Closest Points to Origin (spaced rep — Heaps)

Return the k closest points to the origin from an array of [x, y] points.

Approach: min-heap ordered by squared distance, extract k times. O(n log n). Efficient variant: max-heap of size k — insert each point, pop when size > k. O(n log k).

Watch out: ^ in JS is bitwise XOR. Use x**2 + y**2 or x*x + y*y for squared distance.

Walls and Gates (spaced rep — BFS/DFS)

Fill each empty room in a grid with its distance to the nearest gate.

Approach: multi-source BFS — seed queue with all gates simultaneously. BFS expands in true distance order, so each room is visited once and gets the correct minimum automatically.

Watch out: Array.at(-1) wraps around. When generating neighbors in a grid, use explicit bounds checking (x >= 0 && x < cols && y >= 0 && y < rows) and index with grid[y][x], not .at().

Longest Common Subsequence

Given strings s and t, return the length of their longest common subsequence.

Approach: 2D DP table. See recurrence and implementation above.

Key insight: the “magical outside realm” framing — when characters don’t match, you discard one with max(...), you don’t add both. The max is what prevents double-counting.

Session transcript

Tutor: This is Session 6. Spaced rep due: K Closest Points (Heaps) and Walls & Gates (BFS/DFS). Then new topic: 2D DP.


K Closest Points: James gave correct approach (min-heap by squared distance, extract k times). Used a.x^2 — flagged as bitwise XOR bug. Correctly identified bounded max-heap variant unprompted. Stated complexity as O(n * k log k) — corrected to O(n log k).


Walls and Gates: James initially asked for clarification on the problem. After seeing the example grid, identified the multi-source challenge: “BFS from each gate, set value to min(existing, distance).” Tutor introduced multi-source BFS — James immediately described it as “all gates are 1 away from a magical outside realm.” Implementation was clean. Bug found: Array.at(-1) wraps around for negative indices; grid neighbors need explicit bounds check with [] indexing.


LCS: James correctly defined dp[i][j] as LCS of s[0..i] and t[0..j]. Worried about double-counting; tutor explained the two-case split. James filled the 4×3 example table correctly on first try and articulated each cell’s recurrence. Implementation had the grid-sizing offset bug (needed s.length+1 rows), the j > direction typo, and the ^ XOR issue from earlier.

James stopped session after LCS, before Unique Paths. Session saved.