25: Dynamic Programming Fundamentals

25: Dynamic Programming Fundamentals Advanced
Section titled “25: Dynamic Programming Fundamentals Advanced”Overview
Section titled “Overview”Dynamic Programming is one of the most powerful problem-solving techniques in computer science, transforming exponential-time solutions into polynomial or even linear-time algorithms. While it might seem intimidating at first, DP follows clear patterns that you can master with practice.
This technique solves complex problems by breaking them down into simpler overlapping subproblems and storing their solutions to avoid redundant computation. Think of it as “smart recursion” - we remember what we’ve already computed so we never waste time calculating the same thing twice.
In this chapter, you’ll master both fundamental DP approaches: top-down memoization (adding caching to recursive solutions) and bottom-up tabulation (building solutions iteratively from base cases). You’ll solve classic DP problems like Fibonacci, coin change, knapsack, and longest common subsequence, learning to recognize when DP is the right tool and how to optimize your solutions.
By the end of this chapter, you’ll understand how DP can transform solutions from O(2ⁿ) exponential time to O(n) or O(n²) polynomial time, making previously intractable problems solvable in milliseconds.
Prerequisites
Section titled “Prerequisites”Before starting this chapter, you should have:
- ✓ Complete understanding of recursion fundamentals (Chapter 3)
- ✓ Strong knowledge of arrays and dynamic arrays (Chapter 15)
- ✓ Basic understanding of Big O notation and algorithm complexity
- ✓ Experience breaking down problems into smaller subproblems
Estimated Time: ~45 minutes
What You’ll Build
Section titled “What You’ll Build”By the end of this chapter, you will have created:
- Memoization and tabulation implementations for Fibonacci and other classic problems
- A coin change solver that finds minimum coins and counts combinations
- A subset sum solver that determines if a target sum can be achieved
- Both 0/1 knapsack (bounded) and unbounded knapsack solvers with rod cutting optimization
- Longest common subsequence (LCS) and longest increasing subsequence (LIS) algorithms
- Grid DP solvers for counting paths and finding minimum cost paths
- DP pattern recognition system for identifying when to use dynamic programming
- Space-optimized versions of classic DP problems with understanding of forward vs backward iteration
Objectives
Section titled “Objectives”- Understand when and why dynamic programming is the right tool for the job
- Master both fundamental approaches: top-down memoization and bottom-up tabulation
- Recognize and solve classic DP problems like Fibonacci, coin change, and knapsack
- Apply DP to eliminate redundant calculations and optimize recursive solutions
- Build real-world applications for inventory optimization and resource allocation
- Identify overlapping subproblems and optimal substructure in new problems
Core Concepts
Section titled “Core Concepts”Dynamic Programming relies on two fundamental properties that determine whether a problem can be solved efficiently using DP. Understanding these concepts is crucial for recognizing when DP is the right approach.
1. Overlapping Subproblems
Section titled “1. Overlapping Subproblems”A problem has overlapping subproblems if the same subproblems are solved multiple times.
<?php
declare(strict_types=1);
// Example: Fibonacci without DP (exponential time)function fibonacciNaive(int $n): int{ if ($n <= 1) { return $n; } return fibonacciNaive($n - 1) + fibonacciNaive($n - 2);}
// fib(5) calls fib(3) twice, fib(2) three times// Overlapping subproblems!2. Optimal Substructure
Section titled “2. Optimal Substructure”A problem has optimal substructure if an optimal solution can be constructed from optimal solutions of its subproblems.
<?php
declare(strict_types=1);
// Example: Shortest path has optimal substructure// If A→B→C is shortest path from A to C,// then A→B must be shortest path from A to BMemoization (Top-Down DP)
Section titled “Memoization (Top-Down DP)”Store results of expensive function calls and return cached result when same inputs occur.
Fibonacci with Memoization
Section titled “Fibonacci with Memoization”<?php
declare(strict_types=1);
class FibonacciMemoization{ private array $memo = [];
public function fib(int $n): int { // Base cases if ($n <= 1) { return $n; }
// Check if already computed if (isset($this->memo[$n])) { return $this->memo[$n]; }
// Compute and store $this->memo[$n] = $this->fib($n - 1) + $this->fib($n - 2); return $this->memo[$n]; }
public function getMemo(): array { return $this->memo; }}
// Example usage$fib = new FibonacciMemoization();echo $fib->fib(10) . "\n"; // 55print_r($fib->getMemo());// [2 => 1, 3 => 2, 4 => 3, 5 => 5, 6 => 8, 7 => 13, 8 => 21, 9 => 34, 10 => 55]
// Time: O(n), Space: O(n)DP State Transition Visualization
Section titled “DP State Transition Visualization”Understanding how dynamic programming builds solutions:
<?php
declare(strict_types=1);
class DPStateVisualizer{ // Visualize Fibonacci DP state transitions public function visualizeFibonacci(int $n): void { echo "=== Fibonacci DP State Transitions ===\n\n"; echo "Computing fib($n)\n\n";
// Show naive recursive approach echo "Without DP (Exponential - many repeated calculations):\n"; echo "fib($n)\n"; if ($n >= 2) { echo "├─ fib(" . ($n-1) . ")\n"; echo "│ ├─ fib(" . ($n-2) . ") [REPEATED]\n"; echo "│ └─ fib(" . ($n-3) . ")\n"; echo "└─ fib(" . ($n-2) . ") [REPEATED]\n"; echo " ├─ fib(" . ($n-3) . ") [REPEATED]\n"; echo " └─ fib(" . ($n-4) . ")\n"; } echo "\nWith DP (Linear - each value computed once):\n";
$dp = []; for ($i = 0; $i <= $n; $i++) { if ($i <= 1) { $dp[$i] = $i; echo "dp[$i] = $i (base case)\n"; } else { $dp[$i] = $dp[$i-1] + $dp[$i-2]; echo "dp[$i] = dp[" . ($i-1) . "] + dp[" . ($i-2) . "] = {$dp[$i-1]} + {$dp[$i-2]} = {$dp[$i]}\n"; } }
echo "\nFinal answer: fib($n) = {$dp[$n]}\n"; echo "DP table: [" . implode(', ', $dp) . "]\n"; }
// Visualize coin change DP table construction public function visualizeCoinChange(array $coins, int $amount): void { echo "\n=== Coin Change DP Table Construction ===\n\n"; echo "Coins: [" . implode(', ', $coins) . "], Amount: $amount\n\n";
$dp = array_fill(0, $amount + 1, PHP_INT_MAX); $dp[0] = 0;
echo "Initial: dp[0] = 0 (0 coins needed for amount 0)\n"; echo "All others: ∞ (impossible initially)\n\n";
for ($i = 1; $i <= $amount; $i++) { echo "Computing dp[$i] (minimum coins for amount $i):\n";
foreach ($coins as $coin) { if ($coin <= $i && $dp[$i - $coin] !== PHP_INT_MAX) { $newValue = $dp[$i - $coin] + 1; echo " Try coin $coin: dp[$i - $coin] + 1 = {$dp[$i - $coin]} + 1 = $newValue\n";
if ($newValue < $dp[$i]) { $dp[$i] = $newValue; echo " → Update dp[$i] = $newValue\n"; } } }
$result = $dp[$i] === PHP_INT_MAX ? '∞' : $dp[$i]; echo " Final: dp[$i] = $result\n\n"; }
echo "Complete DP table:\n"; echo "Amount: "; for ($i = 0; $i <= $amount; $i++) { printf("%4d", $i); } echo "\nCoins: "; for ($i = 0; $i <= $amount; $i++) { $val = $dp[$i] === PHP_INT_MAX ? '∞' : $dp[$i]; printf("%4s", $val); } echo "\n"; }
// Visualize LCS DP table public function visualizeLCS(string $text1, string $text2): void { echo "\n=== Longest Common Subsequence DP Table ===\n\n"; echo "Text1: \"$text1\"\n"; echo "Text2: \"$text2\"\n\n";
$m = strlen($text1); $n = strlen($text2); $dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
// Build DP table for ($i = 1; $i <= $m; $i++) { for ($j = 1; $j <= $n; $j++) { if ($text1[$i - 1] === $text2[$j - 1]) { $dp[$i][$j] = $dp[$i - 1][$j - 1] + 1; } else { $dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]); } } }
// Print DP table echo "DP Table (dp[i][j] = LCS length of text1[0..i-1] and text2[0..j-1]):\n\n"; echo " "; for ($j = 0; $j < $n; $j++) { echo " " . $text2[$j]; } echo "\n";
for ($i = 0; $i <= $m; $i++) { if ($i === 0) { echo " "; } else { echo $text1[$i - 1] . " "; }
for ($j = 0; $j <= $n; $j++) { printf("%3d", $dp[$i][$j]); } echo "\n"; }
echo "\nLCS Length: {$dp[$m][$n]}\n";
// Show how to trace back echo "\nTraceback (how we got the answer):\n"; $i = $m; $j = $n; $lcs = '';
while ($i > 0 && $j > 0) { if ($text1[$i - 1] === $text2[$j - 1]) { $lcs = $text1[$i - 1] . $lcs; echo " Match '{$text1[$i-1]}' at ($i,$j) → move diagonal\n"; $i--; $j--; } elseif ($dp[$i - 1][$j] > $dp[$i][$j - 1]) { echo " At ($i,$j): up ({$dp[$i-1][$j]}) > left ({$dp[$i][$j-1]}) → move up\n"; $i--; } else { echo " At ($i,$j): left ({$dp[$i][$j-1]}) >= up ({$dp[$i-1][$j]}) → move left\n"; $j--; } }
echo "\nLCS: \"$lcs\"\n"; }}
// Example visualizations$visualizer = new DPStateVisualizer();
$visualizer->visualizeFibonacci(7);/*=== Fibonacci DP State Transitions ===
Computing fib(7)
Without DP (Exponential - many repeated calculations):fib(7)├─ fib(6)│ ├─ fib(5) [REPEATED]│ └─ fib(4)└─ fib(5) [REPEATED] ├─ fib(4) [REPEATED] └─ fib(3)
With DP (Linear - each value computed once):dp[0] = 0 (base case)dp[1] = 1 (base case)dp[2] = dp[1] + dp[0] = 1 + 0 = 1dp[3] = dp[2] + dp[1] = 1 + 1 = 2dp[4] = dp[3] + dp[2] = 2 + 1 = 3dp[5] = dp[4] + dp[3] = 3 + 2 = 5dp[6] = dp[5] + dp[4] = 5 + 3 = 8dp[7] = dp[6] + dp[5] = 8 + 5 = 13
Final answer: fib(7) = 13DP table: [0, 1, 1, 2, 3, 5, 8, 13]*/
$visualizer->visualizeCoinChange([1, 5, 10], 12);
$visualizer->visualizeLCS("AGGTAB", "GXTXAYB");Memoization vs Tabulation Detailed Comparison
Section titled “Memoization vs Tabulation Detailed Comparison”Understanding the tradeoffs between top-down and bottom-up DP:
<?php
declare(strict_types=1);
class MemoizationVsTabulation{ // Memoization (Top-Down) - with call tracking public function fibMemo(int $n, array &$callCount = []): int { static $memo = []; static $calls = 0;
$calls++; $callCount['total'] = ($callCount['total'] ?? 0) + 1;
if ($n <= 1) { return $n; }
if (!isset($memo[$n])) { $callCount['computed'] = ($callCount['computed'] ?? 0) + 1; $memo[$n] = $this->fibMemo($n - 1, $callCount) + $this->fibMemo($n - 2, $callCount); } else { $callCount['cached'] = ($callCount['cached'] ?? 0) + 1; }
return $memo[$n]; }
// Tabulation (Bottom-Up) public function fibTab(int $n, array &$stats = []): int { if ($n <= 1) { return $n; }
$dp = [0, 1]; $stats['iterations'] = 0;
for ($i = 2; $i <= $n; $i++) { $dp[$i] = $dp[$i - 1] + $dp[$i - 2]; $stats['iterations']++; }
$stats['space_used'] = count($dp);
return $dp[$n]; }
// Comparison public function compare(int $n): void { echo "=== Memoization vs Tabulation Comparison (n=$n) ===\n\n";
// Memoization $memoCallCount = []; $memoStart = microtime(true); $memoResult = $this->fibMemo($n, $memoCallCount); $memoTime = microtime(true) - $memoStart;
echo "MEMOIZATION (Top-Down):\n"; echo " Result: $memoResult\n"; echo " Time: " . round($memoTime * 1000, 4) . " ms\n"; echo " Function calls: {$memoCallCount['total']}\n"; echo " Computed: " . ($memoCallCount['computed'] ?? 0) . "\n"; echo " From cache: " . ($memoCallCount['cached'] ?? 0) . "\n"; echo " Approach: Recursive (uses call stack)\n"; echo " When to use: When you don't need all subproblems\n\n";
// Tabulation $tabStats = []; $tabStart = microtime(true); $tabResult = $this->fibTab($n, $tabStats); $tabTime = microtime(true) - $tabStart;
echo "TABULATION (Bottom-Up):\n"; echo " Result: $tabResult\n"; echo " Time: " . round($tabTime * 1000, 4) . " ms\n"; echo " Iterations: {$tabStats['iterations']}\n"; echo " Array size: {$tabStats['space_used']} elements\n"; echo " Approach: Iterative (no recursion)\n"; echo " When to use: When you need all subproblems\n\n";
echo "COMPARISON:\n"; echo " Speed: Tabulation is typically faster (no function call overhead)\n"; echo " Space: Memoization may use less (only computes needed values)\n"; echo " Clarity: Memoization often more intuitive (follows recursive logic)\n"; echo " Stack safety: Tabulation avoids stack overflow for large n\n"; }
// Show execution patterns public function visualizeExecutionPatterns(): void { echo "\n=== Execution Pattern Visualization ===\n\n";
echo "MEMOIZATION (Fibonacci n=5):\n"; echo "Call tree:\n"; echo "fib(5)\n"; echo "├─ fib(4) [compute]\n"; echo "│ ├─ fib(3) [compute]\n"; echo "│ │ ├─ fib(2) [compute]\n"; echo "│ │ │ ├─ fib(1) = 1\n"; echo "│ │ │ └─ fib(0) = 0\n"; echo "│ │ └─ fib(1) = 1\n"; echo "│ └─ fib(2) [from cache!]\n"; echo "└─ fib(3) [from cache!]\n\n";
echo "TABULATION (Fibonacci n=5):\n"; echo "Builds table iteratively:\n"; echo "dp[0] = 0\n"; echo "dp[1] = 1\n"; echo "dp[2] = dp[1] + dp[0] = 1\n"; echo "dp[3] = dp[2] + dp[1] = 2\n"; echo "dp[4] = dp[3] + dp[2] = 3\n"; echo "dp[5] = dp[4] + dp[3] = 5\n"; echo "Direct access to result: dp[5] = 5\n\n";
echo "KEY DIFFERENCES:\n"; echo "1. Direction:\n"; echo " Memoization: Top-down (starts from problem, breaks down)\n"; echo " Tabulation: Bottom-up (starts from base cases, builds up)\n\n";
echo "2. Order of computation:\n"; echo " Memoization: Computes as needed (lazy evaluation)\n"; echo " Tabulation: Computes all states in order (eager evaluation)\n\n";
echo "3. Implementation style:\n"; echo " Memoization: Recursive + cache (natural for many problems)\n"; echo " Tabulation: Iterative loops (better performance)\n\n"; }}
$comparator = new MemoizationVsTabulation();$comparator->compare(30);$comparator->visualizeExecutionPatterns();Generic Memoization Wrapper
Section titled “Generic Memoization Wrapper”<?php
declare(strict_types=1);
class Memoizer{ private array $cache = [];
public function memoize(callable $fn): callable { return function (...$args) use ($fn) { $key = serialize($args);
if (!isset($this->cache[$key])) { $this->cache[$key] = $fn(...$args); }
return $this->cache[$key]; }; }
public function clearCache(): void { $this->cache = []; }
public function getCacheSize(): int { return count($this->cache); }}
// Example usage$memoizer = new Memoizer();
$factorial = function(int $n) use (&$factorial, $memoizer) { if ($n <= 1) return 1; $memoizedFactorial = $memoizer->memoize($factorial); return $n * $memoizedFactorial($n - 1);};
echo $factorial(5) . "\n"; // 120Tabulation (Bottom-Up DP)
Section titled “Tabulation (Bottom-Up DP)”Build solution iteratively from smallest subproblems to larger ones, filling a table.
Fibonacci with Tabulation
Section titled “Fibonacci with Tabulation”<?php
declare(strict_types=1);
class FibonacciTabulation{ public function fib(int $n): int { if ($n <= 1) { return $n; }
// Create table to store results $dp = array_fill(0, $n + 1, 0); $dp[0] = 0; $dp[1] = 1;
// Fill table bottom-up for ($i = 2; $i <= $n; $i++) { $dp[$i] = $dp[$i - 1] + $dp[$i - 2]; }
return $dp[$n]; }
// Space-optimized version (only keep last 2 values) public function fibOptimized(int $n): int { if ($n <= 1) { return $n; }
$prev2 = 0; $prev1 = 1;
for ($i = 2; $i <= $n; $i++) { $current = $prev1 + $prev2; $prev2 = $prev1; $prev1 = $current; }
return $prev1; }}
// Example$fib = new FibonacciTabulation();echo $fib->fib(10) . "\n"; // 55echo $fib->fibOptimized(10) . "\n"; // 55
// Time: O(n), Space: O(n) or O(1) optimizedClassic DP Problems
Section titled “Classic DP Problems”1. Climbing Stairs
Section titled “1. Climbing Stairs”You can climb 1 or 2 steps at a time. How many ways to reach the top?
<?php
declare(strict_types=1);
class ClimbingStairs{ // Memoization approach private array $memo = [];
public function climbStairsMemo(int $n): int { if ($n <= 2) { return $n; }
if (isset($this->memo[$n])) { return $this->memo[$n]; }
$this->memo[$n] = $this->climbStairsMemo($n - 1) + $this->climbStairsMemo($n - 2); return $this->memo[$n]; }
// Tabulation approach public function climbStairsTab(int $n): int { if ($n <= 2) { return $n; }
$dp = array_fill(0, $n + 1, 0); $dp[1] = 1; $dp[2] = 2;
for ($i = 3; $i <= $n; $i++) { $dp[$i] = $dp[$i - 1] + $dp[$i - 2]; }
return $dp[$n]; }
// Space-optimized public function climbStairsOptimized(int $n): int { if ($n <= 2) { return $n; }
$prev2 = 1; $prev1 = 2;
for ($i = 3; $i <= $n; $i++) { $current = $prev1 + $prev2; $prev2 = $prev1; $prev1 = $current; }
return $prev1; }}
// Example$stairs = new ClimbingStairs();echo $stairs->climbStairsTab(5) . "\n"; // 8 ways// 1+1+1+1+1, 1+1+1+2, 1+1+2+1, 1+2+1+1, 2+1+1+1, 1+2+2, 2+1+2, 2+2+12. Coin Change Problem
Section titled “2. Coin Change Problem”Find minimum coins needed to make a given amount.
<?php
declare(strict_types=1);
class CoinChange{ // Minimum coins to make amount public function minCoins(array $coins, int $amount): int { $dp = array_fill(0, $amount + 1, PHP_INT_MAX); $dp[0] = 0; // 0 coins needed for amount 0
for ($i = 1; $i <= $amount; $i++) { foreach ($coins as $coin) { if ($coin <= $i && $dp[$i - $coin] !== PHP_INT_MAX) { $dp[$i] = min($dp[$i], $dp[$i - $coin] + 1); } } }
return $dp[$amount] === PHP_INT_MAX ? -1 : $dp[$amount]; }
// Number of ways to make amount public function coinCombinations(array $coins, int $amount): int { $dp = array_fill(0, $amount + 1, 0); $dp[0] = 1; // One way to make 0: use no coins
foreach ($coins as $coin) { for ($i = $coin; $i <= $amount; $i++) { $dp[$i] += $dp[$i - $coin]; } }
return $dp[$amount]; }
// Get actual coins used public function minCoinsWithPath(array $coins, int $amount): ?array { $dp = array_fill(0, $amount + 1, PHP_INT_MAX); $usedCoin = array_fill(0, $amount + 1, -1); $dp[0] = 0;
for ($i = 1; $i <= $amount; $i++) { foreach ($coins as $coin) { if ($coin <= $i && $dp[$i - $coin] !== PHP_INT_MAX) { if ($dp[$i - $coin] + 1 < $dp[$i]) { $dp[$i] = $dp[$i - $coin] + 1; $usedCoin[$i] = $coin; } } } }
if ($dp[$amount] === PHP_INT_MAX) { return null; }
// Reconstruct coins used $result = []; $current = $amount; while ($current > 0) { $coin = $usedCoin[$current]; $result[] = $coin; $current -= $coin; }
return $result; }}
// Example$coinChange = new CoinChange();
$coins = [1, 5, 10, 25];echo "Min coins for 63 cents: " . $coinChange->minCoins($coins, 63) . "\n"; // 6 (25+25+10+1+1+1)echo "Ways to make 10 cents: " . $coinChange->coinCombinations($coins, 10) . "\n"; // 4
print_r($coinChange->minCoinsWithPath($coins, 63));// [25, 25, 10, 1, 1, 1]3. Subset Sum Problem
Section titled “3. Subset Sum Problem”Determine if there exists a subset of numbers that sums to a target value.
<?php
declare(strict_types=1);
class SubsetSum{ // Check if subset exists that sums to target public function subsetExists(array $nums, int $target): bool { $n = count($nums);
// dp[i][j] = true if sum j can be achieved using first i elements $dp = array_fill(0, $n + 1, array_fill(0, $target + 1, false));
// Base case: sum 0 can always be achieved (empty subset) for ($i = 0; $i <= $n; $i++) { $dp[$i][0] = true; }
for ($i = 1; $i <= $n; $i++) { for ($j = 1; $j <= $target; $j++) { // Don't include current element $dp[$i][$j] = $dp[$i - 1][$j];
// Include current element if it fits if ($nums[$i - 1] <= $j) { $dp[$i][$j] = $dp[$i][$j] || $dp[$i - 1][$j - $nums[$i - 1]]; } } }
return $dp[$n][$target]; }
// Space-optimized version (1D array) public function subsetExistsOptimized(array $nums, int $target): bool { $dp = array_fill(0, $target + 1, false); $dp[0] = true; // Sum 0 always possible
foreach ($nums as $num) { // Traverse backwards to avoid using same element twice for ($j = $target; $j >= $num; $j--) { $dp[$j] = $dp[$j] || $dp[$j - $num]; } }
return $dp[$target]; }
// Count number of subsets that sum to target public function countSubsets(array $nums, int $target): int { $dp = array_fill(0, $target + 1, 0); $dp[0] = 1; // One way to achieve sum 0 (empty subset)
foreach ($nums as $num) { for ($j = $target; $j >= $num; $j--) { $dp[$j] += $dp[$j - $num]; } }
return $dp[$target]; }
// Get actual subset that sums to target public function findSubset(array $nums, int $target): ?array { $n = count($nums); $dp = array_fill(0, $n + 1, array_fill(0, $target + 1, false));
for ($i = 0; $i <= $n; $i++) { $dp[$i][0] = true; }
for ($i = 1; $i <= $n; $i++) { for ($j = 1; $j <= $target; $j++) { $dp[$i][$j] = $dp[$i - 1][$j]; if ($nums[$i - 1] <= $j) { $dp[$i][$j] = $dp[$i][$j] || $dp[$i - 1][$j - $nums[$i - 1]]; } } }
if (!$dp[$n][$target]) { return null; }
// Reconstruct subset $subset = []; $j = $target; for ($i = $n; $i > 0 && $j > 0; $i--) { if (!$dp[$i - 1][$j]) { // Element i-1 was included $subset[] = $nums[$i - 1]; $j -= $nums[$i - 1]; } }
return array_reverse($subset); }}
// Example$subsetSum = new SubsetSum();
$nums = [3, 34, 4, 12, 5, 2];echo $subsetSum->subsetExists($nums, 9) ? "true" : "false"; // true (4 + 5)echo "\n";
echo "Count subsets summing to 9: " . $subsetSum->countSubsets($nums, 9) . "\n"; // 2print_r($subsetSum->findSubset($nums, 9)); // [4, 5]
// Time: O(n × target), Space: O(target) optimizedWhy It Works: This is a classic boolean DP problem. dp[i][j] represents whether sum j can be achieved using the first i elements. For each element, we have two choices: include it or exclude it. The space-optimized version uses a 1D array and traverses backwards (like knapsack) to ensure each element is used at most once per subset.
4. Longest Common Subsequence (LCS)
Section titled “4. Longest Common Subsequence (LCS)”Find longest subsequence common to two sequences.
<?php
declare(strict_types=1);
class LongestCommonSubsequence{ // Length of LCS public function lcsLength(string $text1, string $text2): int { $m = strlen($text1); $n = strlen($text2);
// dp[i][j] = LCS length of text1[0...i-1] and text2[0...j-1] $dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
for ($i = 1; $i <= $m; $i++) { for ($j = 1; $j <= $n; $j++) { if ($text1[$i - 1] === $text2[$j - 1]) { $dp[$i][$j] = $dp[$i - 1][$j - 1] + 1; } else { $dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]); } } }
return $dp[$m][$n]; }
// Get actual LCS string public function lcs(string $text1, string $text2): string { $m = strlen($text1); $n = strlen($text2); $dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
// Fill DP table for ($i = 1; $i <= $m; $i++) { for ($j = 1; $j <= $n; $j++) { if ($text1[$i - 1] === $text2[$j - 1]) { $dp[$i][$j] = $dp[$i - 1][$j - 1] + 1; } else { $dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]); } } }
// Reconstruct LCS $lcs = ''; $i = $m; $j = $n;
while ($i > 0 && $j > 0) { if ($text1[$i - 1] === $text2[$j - 1]) { $lcs = $text1[$i - 1] . $lcs; $i--; $j--; } elseif ($dp[$i - 1][$j] > $dp[$i][$j - 1]) { $i--; } else { $j--; } }
return $lcs; }
// Space-optimized (only need 2 rows) public function lcsLengthOptimized(string $text1, string $text2): int { $m = strlen($text1); $n = strlen($text2);
$prev = array_fill(0, $n + 1, 0); $curr = array_fill(0, $n + 1, 0);
for ($i = 1; $i <= $m; $i++) { for ($j = 1; $j <= $n; $j++) { if ($text1[$i - 1] === $text2[$j - 1]) { $curr[$j] = $prev[$j - 1] + 1; } else { $curr[$j] = max($prev[$j], $curr[$j - 1]); } } [$prev, $curr] = [$curr, $prev]; }
return $prev[$n]; }}
// Example$lcs = new LongestCommonSubsequence();echo $lcs->lcsLength('ABCDGH', 'AEDFHR') . "\n"; // 3 (ADH)echo $lcs->lcs('ABCDGH', 'AEDFHR') . "\n"; // ADHecho $lcs->lcs('AGGTAB', 'GXTXAYB') . "\n"; // GTAB5. 0/1 Knapsack Problem
Section titled “5. 0/1 Knapsack Problem”Maximize value of items in knapsack with weight limit.
<?php
declare(strict_types=1);
class Knapsack01{ // Maximum value achievable public function maxValue(array $weights, array $values, int $capacity): int { $n = count($weights);
// dp[i][w] = max value using first i items with capacity w $dp = array_fill(0, $n + 1, array_fill(0, $capacity + 1, 0));
for ($i = 1; $i <= $n; $i++) { for ($w = 0; $w <= $capacity; $w++) { // Don't include item i-1 $dp[$i][$w] = $dp[$i - 1][$w];
// Include item i-1 if it fits if ($weights[$i - 1] <= $w) { $includeValue = $values[$i - 1] + $dp[$i - 1][$w - $weights[$i - 1]]; $dp[$i][$w] = max($dp[$i][$w], $includeValue); } } }
return $dp[$n][$capacity]; }
// Get items included public function maxValueWithItems(array $weights, array $values, int $capacity): array { $n = count($weights); $dp = array_fill(0, $n + 1, array_fill(0, $capacity + 1, 0));
// Fill DP table for ($i = 1; $i <= $n; $i++) { for ($w = 0; $w <= $capacity; $w++) { $dp[$i][$w] = $dp[$i - 1][$w];
if ($weights[$i - 1] <= $w) { $includeValue = $values[$i - 1] + $dp[$i - 1][$w - $weights[$i - 1]]; $dp[$i][$w] = max($dp[$i][$w], $includeValue); } } }
// Reconstruct solution $items = []; $w = $capacity; for ($i = $n; $i > 0; $i--) { if ($dp[$i][$w] !== $dp[$i - 1][$w]) { $items[] = $i - 1; // Item index $w -= $weights[$i - 1]; } }
return [ 'maxValue' => $dp[$n][$capacity], 'items' => array_reverse($items) ]; }
// Space-optimized (1D array) public function maxValueOptimized(array $weights, array $values, int $capacity): int { $dp = array_fill(0, $capacity + 1, 0);
for ($i = 0; $i < count($weights); $i++) { // Traverse backwards to avoid using same item multiple times for ($w = $capacity; $w >= $weights[$i]; $w--) { $dp[$w] = max($dp[$w], $values[$i] + $dp[$w - $weights[$i]]); } }
return $dp[$capacity]; }}
// Example$knapsack = new Knapsack01();
$weights = [1, 3, 4, 5];$values = [1, 4, 5, 7];$capacity = 7;
echo "Max value: " . $knapsack->maxValue($weights, $values, $capacity) . "\n"; // 9
$result = $knapsack->maxValueWithItems($weights, $values, $capacity);echo "Max value: {$result['maxValue']}\n";echo "Items: " . implode(', ', $result['items']) . "\n"; // Items 1, 2 (weights 3+4=7, values 4+5=9)5b. Unbounded Knapsack & Rod Cutting
Section titled “5b. Unbounded Knapsack & Rod Cutting”In the 0/1 Knapsack problem, each item can be used at most once. The Unbounded Knapsack problem allows items to be used multiple times. This fundamental difference changes how we iterate in space-optimized solutions.
Key Difference:
- 0/1 Knapsack: Traverse backwards to prevent using same item twice
- Unbounded Knapsack: Traverse forwards to allow reusing items
Rod Cutting Problem
Section titled “Rod Cutting Problem”A classic unbounded knapsack problem: Given a rod of length n and prices for pieces of different lengths, maximize revenue by cutting the rod optimally.
<?php
declare(strict_types=1);
class RodCutting{ // Maximum revenue from cutting rod of length n public function maxRevenue(array $prices, int $length): int { // prices[i] = price for piece of length i+1 // dp[i] = max revenue for rod of length i $dp = array_fill(0, $length + 1, 0);
for ($i = 1; $i <= $length; $i++) { for ($j = 0; $j < $i && $j < count($prices); $j++) { // Try cutting piece of length j+1 $dp[$i] = max($dp[$i], $prices[$j] + $dp[$i - ($j + 1)]); } }
return $dp[$length]; }
// Get actual cuts that maximize revenue public function findCuts(array $prices, int $length): array { $dp = array_fill(0, $length + 1, 0); $cuts = array_fill(0, $length + 1, []);
for ($i = 1; $i <= $length; $i++) { for ($j = 0; $j < $i && $j < count($prices); $j++) { $pieceLength = $j + 1; $revenue = $prices[$j] + $dp[$i - $pieceLength];
if ($revenue > $dp[$i]) { $dp[$i] = $revenue; $cuts[$i] = array_merge([$pieceLength], $cuts[$i - $pieceLength]); } } }
return $cuts[$length]; }}
// Example$rod = new RodCutting();
$prices = [1, 5, 8, 9, 10, 17, 17, 20]; // Prices for lengths 1-8echo "Max revenue for length 8: " . $rod->maxRevenue($prices, 8) . "\n"; // 22print_r($rod->findCuts($prices, 8)); // [2, 6] or [1, 2, 5] etc.
// Time: O(n²), Space: O(n)Why It Works: For each rod length i, we try all possible first cuts j+1. Since pieces can be reused (unbounded), we can use dp[i - (j+1)] which may already include the same piece. This is why forward iteration works - we’re building up solutions that allow reuse.
Unbounded Knapsack
Section titled “Unbounded Knapsack”Similar to 0/1 knapsack, but items can be used multiple times.
<?php
declare(strict_types=1);
class UnboundedKnapsack{ // Maximum value with unlimited items public function maxValue(array $weights, array $values, int $capacity): int { // dp[w] = max value with capacity w $dp = array_fill(0, $capacity + 1, 0);
// Forward iteration allows reusing items for ($w = 0; $w <= $capacity; $w++) { for ($i = 0; $i < count($weights); $i++) { if ($weights[$i] <= $w) { $dp[$w] = max($dp[$w], $values[$i] + $dp[$w - $weights[$i]]); } } }
return $dp[$capacity]; }
// Get items used (may include duplicates) public function maxValueWithItems(array $weights, array $values, int $capacity): array { $dp = array_fill(0, $capacity + 1, 0); $items = array_fill(0, $capacity + 1, []);
for ($w = 0; $w <= $capacity; $w++) { for ($i = 0; $i < count($weights); $i++) { if ($weights[$i] <= $w) { $newValue = $values[$i] + $dp[$w - $weights[$i]]; if ($newValue > $dp[$w]) { $dp[$w] = $newValue; $items[$w] = array_merge([$i], $items[$w - $weights[$i]]); } } } }
return [ 'maxValue' => $dp[$capacity], 'items' => $items[$capacity] ]; }}
// Example$unbounded = new UnboundedKnapsack();
$weights = [1, 3, 4];$values = [15, 20, 30];$capacity = 7;
echo "Max value (unbounded): " . $unbounded->maxValue($weights, $values, $capacity) . "\n"; // 105// Uses item 0 (weight 1, value 15) seven times: 7 × 15 = 105
$result = $unbounded->maxValueWithItems($weights, $values, $capacity);echo "Max value: {$result['maxValue']}\n";echo "Items used: " . implode(', ', $result['items']) . "\n"; // [0, 0, 0, 0, 0, 0, 0]
// Time: O(n × W), Space: O(W)Bounded vs Unbounded: Key Differences
Section titled “Bounded vs Unbounded: Key Differences”Understanding when to use forward vs backward iteration is crucial:
| Aspect | 0/1 Knapsack (Bounded) | Unbounded Knapsack |
|---|---|---|
| Item Usage | At most once | Unlimited times |
| Iteration Direction | Backward (w--) | Forward (w++) |
| Why | Prevents using same item twice | Allows reusing items |
| DP State | dp[w] before including item | dp[w] may already include item |
| Example | Each item is unique | Coins, rod pieces |
Critical Insight:
- Backward iteration ensures when computing
dp[w], we usedp[w - weight]which was computed before considering the current item - Forward iteration allows
dp[w - weight]to already include the current item, enabling reuse
// 0/1 Knapsack: Backward iterationfor ($w = $capacity; $w >= $weights[$i]; $w--) { $dp[$w] = max($dp[$w], $values[$i] + $dp[$w - $weights[$i]]); // dp[w - weights[i]] computed BEFORE considering item i}
// Unbounded Knapsack: Forward iterationfor ($w = $weights[$i]; $w <= $capacity; $w++) { $dp[$w] = max($dp[$w], $values[$i] + $dp[$w - $weights[$i]]); // dp[w - weights[i]] may already include item i (reuse allowed)}6. Longest Increasing Subsequence (LIS)
Section titled “6. Longest Increasing Subsequence (LIS)”Find longest subsequence where elements are in increasing order.
<?php
declare(strict_types=1);
class LongestIncreasingSubsequence{ // O(n²) DP solution public function lisLength(array $nums): int { $n = count($nums); if ($n === 0) return 0;
// dp[i] = length of LIS ending at index i $dp = array_fill(0, $n, 1);
for ($i = 1; $i < $n; $i++) { for ($j = 0; $j < $i; $j++) { if ($nums[$j] < $nums[$i]) { $dp[$i] = max($dp[$i], $dp[$j] + 1); } } }
return max($dp); }
// Get actual LIS public function lis(array $nums): array { $n = count($nums); if ($n === 0) return [];
$dp = array_fill(0, $n, 1); $prev = array_fill(0, $n, -1);
for ($i = 1; $i < $n; $i++) { for ($j = 0; $j < $i; $j++) { if ($nums[$j] < $nums[$i] && $dp[$j] + 1 > $dp[$i]) { $dp[$i] = $dp[$j] + 1; $prev[$i] = $j; } } }
// Find index with max LIS length $maxLength = max($dp); $maxIndex = array_search($maxLength, $dp);
// Reconstruct LIS $lis = []; $current = $maxIndex; while ($current !== -1) { array_unshift($lis, $nums[$current]); $current = $prev[$current]; }
return $lis; }
// O(n log n) solution using binary search public function lisLengthOptimized(array $nums): int { $tails = []; // tails[i] = smallest tail of all LIS of length i+1
foreach ($nums as $num) { // Binary search for position $left = 0; $right = count($tails);
while ($left < $right) { $mid = (int)(($left + $right) / 2); if ($tails[$mid] < $num) { $left = $mid + 1; } else { $right = $mid; } }
// Update or append if ($left === count($tails)) { $tails[] = $num; } else { $tails[$left] = $num; } }
return count($tails); }}
// Example$lis = new LongestIncreasingSubsequence();
$nums = [10, 9, 2, 5, 3, 7, 101, 18];echo "LIS length: " . $lis->lisLength($nums) . "\n"; // 4print_r($lis->lis($nums)); // [2, 3, 7, 18] or [2, 5, 7, 18]echo "LIS length (optimized): " . $lis->lisLengthOptimized($nums) . "\n"; // 47. Minimum Path Sum (Grid DP)
Section titled “7. Minimum Path Sum (Grid DP)”Find the minimum cost path from top-left to bottom-right in a grid, moving only right or down. This demonstrates grid-based DP with optimization (not just counting).
<?php
declare(strict_types=1);
class MinimumPathSum{ // Minimum cost path from (0,0) to (m-1, n-1) public function minPathSum(array $grid): int { $m = count($grid); $n = count($grid[0]);
// dp[i][j] = min cost to reach (i, j) $dp = array_fill(0, $m, array_fill(0, $n, 0)); $dp[0][0] = $grid[0][0];
// Fill first row (can only come from left) for ($j = 1; $j < $n; $j++) { $dp[0][$j] = $dp[0][$j - 1] + $grid[0][$j]; }
// Fill first column (can only come from top) for ($i = 1; $i < $m; $i++) { $dp[$i][0] = $dp[$i - 1][0] + $grid[$i][0]; }
// Fill rest: min of coming from top or left for ($i = 1; $i < $m; $i++) { for ($j = 1; $j < $n; $j++) { $dp[$i][$j] = min($dp[$i - 1][$j], $dp[$i][$j - 1]) + $grid[$i][$j]; } }
return $dp[$m - 1][$n - 1]; }
// Space-optimized (only need previous row) public function minPathSumOptimized(array $grid): int { $m = count($grid); $n = count($grid[0]);
$prev = array_fill(0, $n, 0); $prev[0] = $grid[0][0];
// First row for ($j = 1; $j < $n; $j++) { $prev[$j] = $prev[$j - 1] + $grid[0][$j]; }
// Process remaining rows for ($i = 1; $i < $m; $i++) { $curr = []; $curr[0] = $prev[0] + $grid[$i][0]; // First column
for ($j = 1; $j < $n; $j++) { $curr[$j] = min($prev[$j], $curr[$j - 1]) + $grid[$i][$j]; }
$prev = $curr; }
return $prev[$n - 1]; }
// Get actual path public function minPathSumWithPath(array $grid): array { $m = count($grid); $n = count($grid[0]); $dp = array_fill(0, $m, array_fill(0, $n, 0)); $dp[0][0] = $grid[0][0];
// Fill DP table for ($j = 1; $j < $n; $j++) { $dp[0][$j] = $dp[0][$j - 1] + $grid[0][$j]; } for ($i = 1; $i < $m; $i++) { $dp[$i][0] = $dp[$i - 1][0] + $grid[$i][0]; } for ($i = 1; $i < $m; $i++) { for ($j = 1; $j < $n; $j++) { $dp[$i][$j] = min($dp[$i - 1][$j], $dp[$i][$j - 1]) + $grid[$i][$j]; } }
// Reconstruct path $path = []; $i = $m - 1; $j = $n - 1;
while ($i > 0 || $j > 0) { $path[] = [$i, $j];
if ($i === 0) { $j--; } elseif ($j === 0) { $i--; } elseif ($dp[$i - 1][$j] < $dp[$i][$j - 1]) { $i--; } else { $j--; } } $path[] = [0, 0];
return [ 'minCost' => $dp[$m - 1][$n - 1], 'path' => array_reverse($path) ]; }}
// Example$mps = new MinimumPathSum();
$grid = [ [1, 3, 1], [1, 5, 1], [4, 2, 1]];
echo "Min path sum: " . $mps->minPathSum($grid) . "\n"; // 7// Path: (0,0) → (0,1) → (0,2) → (1,2) → (2,2)// Cost: 1 + 3 + 1 + 1 + 1 = 7
$result = $mps->minPathSumWithPath($grid);echo "Min cost: {$result['minCost']}\n";echo "Path: " . json_encode($result['path']) . "\n";
// Time: O(m × n), Space: O(m × n) or O(n) optimizedWhy It Works: For each cell (i, j), the minimum cost to reach it is the minimum of:
- Coming from top:
dp[i-1][j] + grid[i][j] - Coming from left:
dp[i][j-1] + grid[i][j]
This is a classic grid DP pattern where we build solutions cell by cell, always choosing the optimal previous state.
8. Unique Paths
Section titled “8. Unique Paths”Count the number of unique paths from top-left to bottom-right in a grid, moving only right or down.
<?php
declare(strict_types=1);
class UniquePaths{ // Count paths in m×n grid (2D DP) public function uniquePaths(int $m, int $n): int { // dp[i][j] = number of paths to reach cell (i, j) $dp = array_fill(0, $m, array_fill(0, $n, 0));
// Base case: first row and column have only 1 path for ($i = 0; $i < $m; $i++) { $dp[$i][0] = 1; } for ($j = 0; $j < $n; $j++) { $dp[0][$j] = 1; }
// Fill DP table for ($i = 1; $i < $m; $i++) { for ($j = 1; $j < $n; $j++) { // Can reach (i,j) from above or left $dp[$i][$j] = $dp[$i - 1][$j] + $dp[$i][$j - 1]; } }
return $dp[$m - 1][$n - 1]; }
// Space-optimized version (1D array) public function uniquePathsOptimized(int $m, int $n): int { // Only need previous row, so use 1D array $prev = array_fill(0, $n, 1);
for ($i = 1; $i < $m; $i++) { $curr = array_fill(0, $n, 0); $curr[0] = 1; // First column always 1
for ($j = 1; $j < $n; $j++) { $curr[$j] = $prev[$j] + $curr[$j - 1]; }
$prev = $curr; }
return $prev[$n - 1]; }
// Unique paths with obstacles (0 = obstacle, 1 = free) public function uniquePathsWithObstacles(array $obstacleGrid): int { $m = count($obstacleGrid); $n = count($obstacleGrid[0]);
// If start or end is blocked, no paths if ($obstacleGrid[0][0] === 1 || $obstacleGrid[$m - 1][$n - 1] === 1) { return 0; }
$dp = array_fill(0, $m, array_fill(0, $n, 0)); $dp[0][0] = 1;
// Fill first row for ($j = 1; $j < $n; $j++) { $dp[0][$j] = ($obstacleGrid[0][$j] === 0 && $dp[0][$j - 1] > 0) ? 1 : 0; }
// Fill first column for ($i = 1; $i < $m; $i++) { $dp[$i][0] = ($obstacleGrid[$i][0] === 0 && $dp[$i - 1][0] > 0) ? 1 : 0; }
// Fill rest of grid for ($i = 1; $i < $m; $i++) { for ($j = 1; $j < $n; $j++) { if ($obstacleGrid[$i][$j] === 0) { $dp[$i][$j] = $dp[$i - 1][$j] + $dp[$i][$j - 1]; } else { $dp[$i][$j] = 0; // Obstacle blocks path } } }
return $dp[$m - 1][$n - 1]; }
// Get actual paths (for small grids) public function getAllPaths(int $m, int $n): array { $paths = []; $currentPath = [];
$this->dfs(0, 0, $m, $n, $currentPath, $paths);
return $paths; }
private function dfs(int $i, int $j, int $m, int $n, array &$path, array &$paths): void { // Reached destination if ($i === $m - 1 && $j === $n - 1) { $paths[] = $path; return; }
// Move right if ($j < $n - 1) { $path[] = 'R'; $this->dfs($i, $j + 1, $m, $n, $path, $paths); array_pop($path); }
// Move down if ($i < $m - 1) { $path[] = 'D'; $this->dfs($i + 1, $j, $m, $n, $path, $paths); array_pop($path); } }}
// Example$paths = new UniquePaths();
echo "Paths in 3×7 grid: " . $paths->uniquePaths(3, 7) . "\n"; // 28echo "Paths in 3×2 grid: " . $paths->uniquePaths(3, 2) . "\n"; // 3echo "Paths (optimized): " . $paths->uniquePathsOptimized(3, 7) . "\n"; // 28
// With obstacles$obstacles = [ [0, 0, 0], [0, 1, 0], [0, 0, 0]];echo "Paths with obstacles: " . $paths->uniquePathsWithObstacles($obstacles) . "\n"; // 2
// Get all paths for small gridprint_r($paths->getAllPaths(2, 2)); // [['R', 'D'], ['D', 'R']]
// Time: O(m × n), Space: O(min(m, n)) optimizedWhy It Works: This is a classic 2D DP problem. dp[i][j] represents the number of ways to reach cell (i, j). Since we can only move right or down, we can reach (i, j) from (i-1, j) (above) or (i, j-1) (left). The base case is that the first row and column have only 1 path each. The space-optimized version uses only one row at a time since we only need the previous row to compute the current row.
DP Pattern Recognition
Section titled “DP Pattern Recognition”<?php
declare(strict_types=1);
class DPPatterns{ public function identifyPattern(string $problemType): array { $patterns = [ 'Optimization' => [ 'Questions' => 'Maximum/minimum, best way, optimal', 'Examples' => 'Knapsack, coin change, LIS', 'Approach' => 'Find max/min at each step' ], 'Counting' => [ 'Questions' => 'How many ways, number of solutions', 'Examples' => 'Climbing stairs, coin combinations', 'Approach' => 'Sum up ways to reach current state' ], 'Decision' => [ 'Questions' => 'Is it possible, can we achieve', 'Examples' => 'Subset sum, partition equal, word break', 'Approach' => 'Boolean DP, track possibility' ], 'Sequence' => [ 'Questions' => 'Longest/shortest sequence', 'Examples' => 'LCS, LIS, edit distance', 'Approach' => 'Build sequence incrementally' ] ];
return $patterns[$problemType] ?? []; }
public function dpSteps(): array { return [ '1. Identify' => 'Does problem have optimal substructure and overlapping subproblems?', '2. Define State' => 'What does dp[i] or dp[i][j] represent?', '3. Find Recurrence' => 'How to compute dp[i] from smaller subproblems?', '4. Base Cases' => 'What are the initial/trivial cases?', '5. Order' => 'What order to fill the DP table?', '6. Answer' => 'Where is the final answer in the table?', '7. Optimize' => 'Can we reduce space complexity?' ]; }}Complexity Analysis
Section titled “Complexity Analysis”| Problem | Naive | DP Time | DP Space | Optimized Space |
|---|---|---|---|---|
| Fibonacci | O(2ⁿ) | O(n) | O(n) | O(1) |
| Climbing Stairs | O(2ⁿ) | O(n) | O(n) | O(1) |
| Coin Change | O(2ⁿ) | O(n×m) | O(n) | O(n) |
| Subset Sum | O(2ⁿ) | O(n×S) | O(n×S) | O(S) |
| LCS | O(2^(m+n)) | O(m×n) | O(m×n) | O(min(m,n)) |
| Knapsack 0/1 | O(2ⁿ) | O(n×W) | O(n×W) | O(W) |
| Unbounded Knapsack | O(2ⁿ) | O(n×W) | O(W) | O(W) |
| Rod Cutting | O(2ⁿ) | O(n²) | O(n) | O(n) |
| Minimum Path Sum | O(2^(m+n)) | O(m×n) | O(m×n) | O(n) |
| LIS | O(2ⁿ) | O(n²) | O(n) | O(n) with O(n log n) time |
| Unique Paths | O(2^(m+n)) | O(m×n) | O(m×n) | O(min(m,n)) |
Where:
- n = problem size
- m = secondary dimension (rows for grids, length for strings)
- W = knapsack capacity
- S = target sum for subset sum
Best Practices
Section titled “Best Practices”-
Start with Recursion
- Write recursive solution first
- Identify overlapping subproblems
- Add memoization
-
Choose Memoization vs Tabulation
- Memoization: Easier to code, only computes needed subproblems
- Tabulation: Better space optimization, avoids recursion overhead
-
Define DP State Carefully
- State should capture all necessary information
- Keep state simple for efficiency
-
Optimize Space
- Often can reduce from 2D to 1D
- Only keep necessary rows/columns
-
Trace Small Examples
- Work through small examples by hand
- Helps identify recurrence relation
Exercises
Section titled “Exercises”Exercise 1: House Robber Problem
Section titled “Exercise 1: House Robber Problem”Goal: Implement a solution to maximize money robbed without robbing adjacent houses.
Create a file called house-robber.php and implement:
HouseRobberclass withrob(array $houses): intmethod- Use DP to find maximum money that can be robbed
- Constraint: Cannot rob two adjacent houses
- Return the maximum amount possible
- Implement both memoization and tabulation approaches
Validation: Test your implementation:
<?php
declare(strict_types=1);
require_once 'house-robber.php';
$robber = new HouseRobber();
// Test case 1: [2, 7, 9, 3, 1]// Optimal: rob houses 0, 2, 4 = 2 + 9 + 1 = 12echo $robber->rob([2, 7, 9, 3, 1]) . "\n"; // Expected: 12
// Test case 2: [1, 2, 3, 1]// Optimal: rob houses 0, 2 = 1 + 3 = 4echo $robber->rob([1, 2, 3, 1]) . "\n"; // Expected: 4
// Test case 3: [2, 1, 1, 2]// Optimal: rob houses 0, 3 = 2 + 2 = 4echo $robber->rob([2, 1, 1, 2]) . "\n"; // Expected: 4Hint: dp[i] = max(dp[i-1], dp[i-2] + houses[i]) - either skip current house or rob it.
Exercise 2: Edit Distance (Levenshtein Distance)
Section titled “Exercise 2: Edit Distance (Levenshtein Distance)”Goal: Find minimum operations to convert one string into another.
Create a file called edit-distance.php and implement:
EditDistanceclass withminDistance(string $word1, string $word2): intmethod- Calculate minimum operations (insert, delete, replace) needed
- Use 2D DP table:
dp[i][j]= min operations to convertword1[0..i-1]toword2[0..j-1] - Return the minimum edit distance
- Optionally implement path reconstruction to show actual operations
Validation: Test your implementation:
<?php
declare(strict_types=1);
require_once 'edit-distance.php';
$edit = new EditDistance();
echo $edit->minDistance("horse", "ros") . "\n"; // Expected: 3// Operations: horse -> rorse (replace h with r)// rorse -> rose (remove r)// rose -> ros (remove e)
echo $edit->minDistance("intention", "execution") . "\n"; // Expected: 5echo $edit->minDistance("", "abc") . "\n"; // Expected: 3 (insert a, b, c)Hint: dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + cost) where cost is 0 if characters match, 1 otherwise.
Exercise 3: Maximum Subarray Sum (Kadane’s Algorithm)
Section titled “Exercise 3: Maximum Subarray Sum (Kadane’s Algorithm)”Goal: Find contiguous subarray with the largest sum using DP.
Create a file called maximum-subarray.php and implement:
MaximumSubarrayclass withmaxSubArray(array $nums): intmethod- Use DP to track maximum sum ending at each position
- Return the maximum sum
- Optionally return the subarray indices that produce maximum sum
Validation: Test your implementation:
<?php
declare(strict_types=1);
require_once 'maximum-subarray.php';
$maxSub = new MaximumSubarray();
echo $maxSub->maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) . "\n"; // Expected: 6// Subarray: [4, -1, 2, 1]
echo $maxSub->maxSubArray([1]) . "\n"; // Expected: 1echo $maxSub->maxSubArray([5, 4, -1, 7, 8]) . "\n"; // Expected: 23Hint: dp[i] = max(nums[i], dp[i-1] + nums[i]) - either start new subarray or extend previous.
Exercise 4: Word Break Problem
Section titled “Exercise 4: Word Break Problem”Goal: Determine if a string can be segmented into dictionary words.
Create a file called word-break.php and implement:
WordBreakclass withwordBreak(string $s, array $wordDict): boolmethod- Use boolean DP:
dp[i]= true ifs[0..i-1]can be segmented - Return true if entire string can be segmented, false otherwise
- Optionally return the actual segmentation
Validation: Test your implementation:
<?php
declare(strict_types=1);
require_once 'word-break.php';
$wb = new WordBreak();
echo $wb->wordBreak("leetcode", ["leet", "code"]) ? "true" : "false"; // Expected: trueecho "\n";
echo $wb->wordBreak("applepenapple", ["apple", "pen"]) ? "true" : "false"; // Expected: trueecho "\n";
echo $wb->wordBreak("catsandog", ["cats", "dog", "sand", "and", "cat"]) ? "true" : "false"; // Expected: falseecho "\n";Hint: dp[i] = true if there exists j < i such that dp[j] = true and s[j..i-1] is in dictionary.
Troubleshooting
Section titled “Troubleshooting”Error: “Maximum recursion depth exceeded”
Section titled “Error: “Maximum recursion depth exceeded””Symptom: Stack overflow when using memoization with large inputs
Cause: Recursive memoization can still hit stack limits for very large problems
Solution: Convert to tabulation (iterative) approach:
// ❌ Memoization (can overflow)public function fibMemo(int $n): int{ if ($n <= 1) return $n; if (isset($this->memo[$n])) return $this->memo[$n]; return $this->memo[$n] = $this->fibMemo($n - 1) + $this->fibMemo($n - 2);}
// ✅ Tabulation (no recursion)public function fibTab(int $n): int{ if ($n <= 1) return $n; $dp = [0, 1]; for ($i = 2; $i <= $n; $i++) { $dp[$i] = $dp[$i - 1] + $dp[$i - 2]; } return $dp[$n];}Problem: Wrong answer for coin change
Section titled “Problem: Wrong answer for coin change”Symptom: Getting incorrect minimum coins or -1 when solution exists
Cause: Common mistakes:
- Not initializing
dp[0] = 0correctly - Using wrong comparison in min calculation
- Not handling impossible cases properly
Solution: Verify base case and recurrence:
// ✅ Correct initialization$dp = array_fill(0, $amount + 1, PHP_INT_MAX);$dp[0] = 0; // Critical: 0 coins for amount 0
// ✅ Correct recurrencefor ($i = 1; $i <= $amount; $i++) { foreach ($coins as $coin) { if ($coin <= $i && $dp[$i - $coin] !== PHP_INT_MAX) { $dp[$i] = min($dp[$i], $dp[$i - $coin] + 1); } }}
// ✅ Check for impossible casereturn $dp[$amount] === PHP_INT_MAX ? -1 : $dp[$amount];Problem: LCS returns wrong length
Section titled “Problem: LCS returns wrong length”Symptom: LCS length doesn’t match expected value
Cause: Common issues:
- Off-by-one errors in string indexing
- Wrong base case initialization
- Incorrect recurrence relation
Solution: Double-check indexing and recurrence:
// ✅ Correct: dp[i][j] represents LCS of text1[0..i-1] and text2[0..j-1]$m = strlen($text1);$n = strlen($text2);$dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
for ($i = 1; $i <= $m; $i++) { for ($j = 1; $j <= $n; $j++) { // Note: text1[$i-1] because dp[i][j] uses 1-indexed, string uses 0-indexed if ($text1[$i - 1] === $text2[$j - 1]) { $dp[$i][$j] = $dp[$i - 1][$j - 1] + 1; } else { $dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]); } }}
return $dp[$m][$n]; // Answer is at dp[m][n]Problem: Knapsack space optimization gives wrong result
Section titled “Problem: Knapsack space optimization gives wrong result”Symptom: 1D array version produces incorrect maximum value
Cause: Iterating forward instead of backward, causing items to be used multiple times
Solution: Always iterate backwards in space-optimized knapsack:
// ❌ Wrong: Forward iteration (allows using same item multiple times)for ($w = $weights[$i]; $w <= $capacity; $w++) { $dp[$w] = max($dp[$w], $values[$i] + $dp[$w - $weights[$i]]);}
// ✅ Correct: Backward iteration (ensures each item used at most once)for ($w = $capacity; $w >= $weights[$i]; $w--) { $dp[$w] = max($dp[$w], $values[$i] + $dp[$w - $weights[$i]]);}Problem: DP state not capturing all necessary information
Section titled “Problem: DP state not capturing all necessary information”Symptom: Solution works for some cases but fails for others
Cause: DP state definition is incomplete - missing important constraints or dimensions
Solution: Review problem constraints and ensure state captures all relevant information:
// Example: If problem has multiple constraints, state needs multiple dimensions// ❌ Insufficient: dp[i] = max value using first i items// ✅ Complete: dp[i][w] = max value using first i items with capacity w
// For problems with additional constraints (like "at most k items"):// ✅ Complete: dp[i][w][k] = max value using first i items, capacity w, at most k itemsProblem: Time limit exceeded in competitive programming
Section titled “Problem: Time limit exceeded in competitive programming”Symptom: Solution is correct but too slow
Cause: Not optimizing space or using inefficient state transitions
Solution:
- Optimize space complexity (2D → 1D when possible)
- Reduce state dimensions if some are redundant
- Use tabulation instead of memoization (faster in practice)
- Consider if problem can be solved with greedy instead of DP
Wrap-up
Section titled “Wrap-up”Congratulations! You’ve mastered the fundamentals of Dynamic Programming. Here’s what you’ve accomplished:
- ✓ Understood the two key properties: overlapping subproblems and optimal substructure
- ✓ Implemented both memoization (top-down) and tabulation (bottom-up) approaches
- ✓ Solved classic DP problems: Fibonacci, climbing stairs, coin change, subset sum, LCS, knapsack (0/1 and unbounded), rod cutting, minimum path sum, and LIS
- ✓ Learned the critical difference between bounded (0/1) and unbounded problems, and when to use forward vs backward iteration
- ✓ Recognized DP patterns and when to apply this technique
- ✓ Optimized space complexity by keeping only necessary previous states
- ✓ Built visualization tools to understand DP state transitions
Dynamic Programming is a powerful optimization technique that transforms exponential-time solutions into polynomial or linear-time algorithms. The key is recognizing when a problem has overlapping subproblems and optimal substructure, then choosing the right approach (memoization or tabulation) based on your needs.
Remember the progression: start with recursion → add memoization → convert to tabulation → optimize space. This systematic approach will help you solve increasingly complex DP problems.
Key Takeaways
Section titled “Key Takeaways”- Dynamic Programming optimizes by storing solutions to subproblems
- Two key properties: overlapping subproblems and optimal substructure
- Two approaches: memoization (top-down) and tabulation (bottom-up)
- Memoization: Add caching to recursive solution
- Tabulation: Build table iteratively from base cases
- Often can optimize space by keeping only necessary previous states
- Pattern recognition helps identify DP problems
- Start simple, optimize later (recursion → memoization → tabulation → space optimization)
Further Reading
Section titled “Further Reading”- Dynamic Programming on GeeksforGeeks — Comprehensive DP tutorials and problems
- LeetCode Dynamic Programming Problems — Practice problems with solutions
- Introduction to Algorithms (CLRS) — Chapter 15 covers dynamic programming in depth
- Chapter 26: Advanced Dynamic Programming — Next chapter covering advanced DP techniques
💻 Code Samples
Section titled “💻 Code Samples”All code examples from this chapter are available in the GitHub repository:
Clone the repository to run examples:
git clone https://github.com/dalehurley/codewithphp.gitcd codewithphp/code-samples/php-algorithms/chapter-25php *.phpNext Steps
Section titled “Next Steps”In the next chapter, we’ll explore advanced dynamic programming techniques including state compression, bitmask DP, digit DP, and DP on trees and graphs.