Dynamic Programming (DP) Patterns

1. 0/1 Knapsack Problem (Space Optimization)

Approach Space Technique Trade-off
2D DP Table O(n*W) dp[i][w] = max value with first i items, capacity w Straightforward, can reconstruct solution
1D DP Array O(W) Process items one by one, update in reverse Space optimized, harder to reconstruct items
Reverse Iteration Required for 1D Prevents using updated values from same iteration Critical for correctness
State Transition Take or skip item dp[w] = max(dp[w], dp[w-weight] + value) Classic 0/1 knapsack formula

Example: 0/1 Knapsack 2D DP

function knapsack2D(weights, values, capacity) {
    const n = weights.length;
    // dp[i][w] = max value using first i items with capacity w
    const dp = Array(n + 1).fill(null)
        .map(() => Array(capacity + 1).fill(0));
    
    for (let i = 1; i <= n; i++) {
        for (let w = 0; w <= capacity; w++) {
            // Option 1: Don't take item i-1
            dp[i][w] = dp[i - 1][w];
            
            // Option 2: Take item i-1 (if fits)
            if (weights[i - 1] <= w) {
                dp[i][w] = Math.max(
                    dp[i][w],
                    dp[i - 1][w - weights[i - 1]] + values[i - 1]
                );
            }
        }
    }
    
    return dp[n][capacity];
}

// weights=[1,3,4,5], values=[1,4,5,7], capacity=7
// -> 9 (take items 1 and 3: values 4+5)
// Time: O(n*W), Space: O(n*W)

Example: 0/1 Knapsack 1D optimized

function knapsack1D(weights, values, capacity) {
    const dp = Array(capacity + 1).fill(0);
    
    for (let i = 0; i < weights.length; i++) {
        // MUST iterate backwards!
        // Prevents using updated values from current iteration
        for (let w = capacity; w >= weights[i]; w--) {
            dp[w] = Math.max(
                dp[w],  // Don't take item i
                dp[w - weights[i]] + values[i]  // Take item i
            );
        }
    }
    
    return dp[capacity];
}

// Same result: 9
// Time: O(n*W), Space: O(W)
// Key: Reverse iteration preserves previous row values

Example: Partition equal subset sum (0/1 knapsack variant)

function canPartition(nums) {
    const sum = nums.reduce((a, b) => a + b, 0);
    
    // If odd sum, can't partition equally
    if (sum % 2 !== 0) return false;
    
    const target = sum / 2;
    const dp = Array(target + 1).fill(false);
    dp[0] = true; // Base case: sum 0 is always achievable
    
    for (const num of nums) {
        // Iterate backwards (0/1 knapsack pattern)
        for (let j = target; j >= num; j--) {
            dp[j] = dp[j] || dp[j - num];
        }
    }
    
    return dp[target];
}

// [1,5,11,5] -> true (partition: [1,5,5] and [11])
// [1,2,3,5] -> false
// Time: O(n*sum), Space: O(sum)

2. Unbounded Knapsack (Bottom-Up DP)

Difference from 0/1 Implementation Reason Application
Unlimited Items Can use same item multiple times Forward iteration in inner loop Coin change, rod cutting
Forward Iteration Use updated values in same iteration Allows reusing same item Different from 0/1's reverse iteration
State Transition dp[w] = max(dp[w], dp[w-weight] + value) Can revisit dp[w-weight] after update Enables unlimited use
Space Optimization 1D array sufficient Only need current state O(W) space

Example: Unbounded knapsack

function unboundedKnapsack(weights, values, capacity) {
    const dp = Array(capacity + 1).fill(0);
    
    for (let i = 0; i < weights.length; i++) {
        // Forward iteration (key difference!)
        for (let w = weights[i]; w <= capacity; w++) {
            dp[w] = Math.max(
                dp[w],
                dp[w - weights[i]] + values[i]
            );
        }
    }
    
    return dp[capacity];
}

// weights=[1,3,4], values=[15,20,25], capacity=4
// Can use item multiple times
// Best: take item 0 four times (15*4=60) or
//       take item 2 once (25)
// Time: O(n*W), Space: O(W)

Example: Coin change (ways to make amount)

function change(amount, coins) {
    const dp = Array(amount + 1).fill(0);
    dp[0] = 1; // One way to make 0: use no coins
    
    for (const coin of coins) {
        // Forward iteration for unbounded
        for (let i = coin; i <= amount; i++) {
            dp[i] += dp[i - coin];
        }
    }
    
    return dp[amount];
}

// amount=5, coins=[1,2,5]
// Ways: [5], [2,2,1], [2,1,1,1], [1,1,1,1,1]
// -> 4 ways
// Time: O(n*amount), Space: O(amount)

Example: Rod cutting problem

function rodCutting(prices, length) {
    // prices[i] = price of rod of length i+1
    const dp = Array(length + 1).fill(0);
    
    for (let len = 1; len <= length; len++) {
        for (let cut = 1; cut <= len; cut++) {
            // Try cutting piece of length 'cut'
            dp[len] = Math.max(
                dp[len],
                prices[cut - 1] + dp[len - cut]
            );
        }
    }
    
    return dp[length];
}

// prices=[1,5,8,9,10,17,17,20], length=8
// Best: cut into 2+6 for price 5+17=22
// Time: O(n²), Space: O(n)

3. Longest Common Subsequence (LCS Algorithm)

Concept State Definition Transition Complexity
Subsequence Characters in order, not necessarily consecutive Different from substring (must be consecutive) More flexible matching
DP Table dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1] 2D table, bottom-up or top-down O(m*n) time and space
Match Case If s1[i-1] === s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 Extend previous LCS by 1 Characters match
Mismatch Case Else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) Take best by skipping one character Try both options

Example: Longest common subsequence

function longestCommonSubsequence(text1, text2) {
    const m = text1.length;
    const n = text2.length;
    
    // dp[i][j] = LCS length of text1[0..i-1] and text2[0..j-1]
    const dp = Array(m + 1).fill(null)
        .map(() => Array(n + 1).fill(0));
    
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (text1[i - 1] === text2[j - 1]) {
                // Characters match: extend LCS
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                // Characters don't match: take max
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    
    return dp[m][n];
}

// "abcde", "ace" -> 3 (LCS = "ace")
// "abc", "abc" -> 3 (LCS = "abc")
// "abc", "def" -> 0 (no common subsequence)
// Time: O(m*n), Space: O(m*n)

Example: LCS with path reconstruction

function lcsWithPath(text1, text2) {
    const m = text1.length;
    const n = text2.length;
    const dp = Array(m + 1).fill(null)
        .map(() => Array(n + 1).fill(0));
    
    // Build DP table
    for (let i = 1; i <= m; i++) {
        for (let 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] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    
    // Reconstruct LCS
    let i = m, j = n;
    const lcs = [];
    
    while (i > 0 && j > 0) {
        if (text1[i - 1] === text2[j - 1]) {
            lcs.unshift(text1[i - 1]);
            i--;
            j--;
        } else if (dp[i - 1][j] > dp[i][j - 1]) {
            i--;
        } else {
            j--;
        }
    }
    
    return {
        length: dp[m][n],
        sequence: lcs.join('')
    };
}

// "ABCDGH", "AEDFHR"
// -> {length: 3, sequence: "ADH"}

Example: Space-optimized LCS O(min(m,n))

function lcsSpaceOptimized(text1, text2) {
    // Ensure text1 is shorter for space efficiency
    if (text1.length > text2.length) {
        [text1, text2] = [text2, text1];
    }
    
    const m = text1.length;
    const n = text2.length;
    
    let prev = Array(m + 1).fill(0);
    let curr = Array(m + 1).fill(0);
    
    for (let j = 1; j <= n; j++) {
        for (let i = 1; i <= m; i++) {
            if (text1[i - 1] === text2[j - 1]) {
                curr[i] = prev[i - 1] + 1;
            } else {
                curr[i] = Math.max(curr[i - 1], prev[i]);
            }
        }
        [prev, curr] = [curr, prev];
    }
    
    return prev[m];
}

// Space: O(min(m,n)) instead of O(m*n)
// Can't reconstruct path with this approach

4. Longest Increasing Subsequence (LIS O(n log n))

Approach Method Time Space
DP O(n²) For each i, check all j < i O(n²) O(n)
Binary Search Maintain tails array, binary search for position O(n log n) O(n)
DP State dp[i] = length of LIS ending at index i Must check all previous elements Simple to understand
Tails Array tails[i] = smallest tail of LIS of length i+1 Optimal for finding length only Complex but efficient

Example: LIS with O(n²) DP

function lengthOfLIS(nums) {
    if (nums.length === 0) return 0;
    
    const dp = Array(nums.length).fill(1);
    
    for (let i = 1; i < nums.length; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    
    return Math.max(...dp);
}

// [10,9,2,5,3,7,101,18]
// dp = [1,1,1,2,2,3,4,4]
// LIS: [2,3,7,101] or [2,5,7,101], length = 4
// Time: O(n²), Space: O(n)

Example: LIS with binary search O(n log n)

function lengthOfLISOptimal(nums) {
    const tails = [];
    
    for (const num of nums) {
        // Binary search for position
        let left = 0, right = tails.length;
        
        while (left < right) {
            const mid = Math.floor((left + right) / 2);
            if (tails[mid] < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        // Replace or append
        if (left === tails.length) {
            tails.push(num);
        } else {
            tails[left] = num;
        }
    }
    
    return tails.length;
}

// tails[i] = smallest ending value of LIS of length i+1
// Time: O(n log n), Space: O(n)

Example: LIS with reconstruction

function lisWithPath(nums) {
    if (nums.length === 0) return [];
    
    const dp = Array(nums.length).fill(1);
    const prev = Array(nums.length).fill(-1);
    
    let maxLen = 1;
    let maxIndex = 0;
    
    for (let i = 1; i < nums.length; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) {
                dp[i] = dp[j] + 1;
                prev[i] = j; // Track predecessor
            }
        }
        
        if (dp[i] > maxLen) {
            maxLen = dp[i];
            maxIndex = i;
        }
    }
    
    // Reconstruct LIS
    const lis = [];
    let idx = maxIndex;
    while (idx !== -1) {
        lis.unshift(nums[idx]);
        idx = prev[idx];
    }
    
    return lis;
}

// [10,9,2,5,3,7,101,18]
// -> [2,3,7,101] (one possible LIS)
Note: Key DP patterns: 0/1 Knapsack uses reverse iteration for 1D optimization, Unbounded Knapsack uses forward iteration to allow item reuse. LCS is fundamental for diff algorithms and bioinformatics. LIS has O(n²) DP solution and O(n log n) binary search optimization.

5. Coin Change Minimum Coins Problem

Variant State Definition Transition Base Case
Min Coins dp[i] = min coins to make amount i dp[i] = min(dp[i], dp[i-coin] + 1) dp[0] = 0, others = Infinity
Count Ways dp[i] = ways to make amount i dp[i] += dp[i-coin] dp[0] = 1, others = 0
Unbounded Type Can use same coin multiple times Forward iteration Classic unbounded knapsack
Impossible Case If dp[amount] remains Infinity Return -1 No combination exists

Example: Coin change minimum coins

function coinChange(coins, amount) {
    const dp = Array(amount + 1).fill(Infinity);
    dp[0] = 0; // 0 coins needed for amount 0
    
    for (const coin of coins) {
        for (let i = coin; i <= amount; i++) {
            dp[i] = Math.min(dp[i], dp[i - coin] + 1);
        }
    }
    
    return dp[amount] === Infinity ? -1 : dp[amount];
}

// coins=[1,2,5], amount=11
// -> 3 (5+5+1)
// coins=[2], amount=3
// -> -1 (impossible)
// Time: O(n*amount), Space: O(amount)

Example: Coin change with path

function coinChangeWithPath(coins, amount) {
    const dp = Array(amount + 1).fill(Infinity);
    const parent = Array(amount + 1).fill(-1);
    dp[0] = 0;
    
    for (let i = 1; i <= amount; i++) {
        for (const coin of coins) {
            if (coin <= i && dp[i - coin] + 1 < dp[i]) {
                dp[i] = dp[i - coin] + 1;
                parent[i] = coin;
            }
        }
    }
    
    if (dp[amount] === Infinity) return null;
    
    // Reconstruct path
    const result = [];
    let curr = amount;
    while (curr > 0) {
        result.push(parent[curr]);
        curr -= parent[curr];
    }
    
    return result;
}

// coins=[1,2,5], amount=11
// -> [5,5,1]

Example: Coin change II (count ways)

function change(amount, coins) {
    const dp = Array(amount + 1).fill(0);
    dp[0] = 1; // One way to make 0
    
    // IMPORTANT: Iterate coins in outer loop
    // to avoid counting permutations
    for (const coin of coins) {
        for (let i = coin; i <= amount; i++) {
            dp[i] += dp[i - coin];
        }
    }
    
    return dp[amount];
}

// amount=5, coins=[1,2,5]
// Ways: [5], [2,2,1], [2,1,1,1], [1,1,1,1,1]
// -> 4 ways
// If we iterate amount in outer loop, we get permutations instead!
// Time: O(n*amount), Space: O(amount)

6. Palindromic Substrings (Expand Around Centers)

Approach Method Time Advantage
DP 2D Table dp[i][j] = is s[i..j] palindrome O(n²) Can query any substring
Expand Centers Try each position as center, expand outward O(n²) Better space O(1), simpler code
Odd Length Single character center Expand while s[i-k] === s[i+k] Center at index i
Even Length Two character center Expand while s[i] === s[i+1+k] Center between i and i+1

Example: Count palindromic substrings

function countSubstrings(s) {
    let count = 0;
    
    function expandAroundCenter(left, right) {
        while (left >= 0 && right < s.length && 
               s[left] === s[right]) {
            count++;
            left--;
            right++;
        }
    }
    
    for (let i = 0; i < s.length; i++) {
        // Odd length palindromes (center at i)
        expandAroundCenter(i, i);
        
        // Even length palindromes (center between i and i+1)
        expandAroundCenter(i, i + 1);
    }
    
    return count;
}

// "abc" -> 3 ("a", "b", "c")
// "aaa" -> 6 ("a", "a", "a", "aa", "aa", "aaa")
// Time: O(n²), Space: O(1)

Example: Longest palindromic substring

function longestPalindrome(s) {
    let start = 0;
    let maxLen = 0;
    
    function expandAroundCenter(left, right) {
        while (left >= 0 && right < s.length && 
               s[left] === s[right]) {
            const len = right - left + 1;
            if (len > maxLen) {
                start = left;
                maxLen = len;
            }
            left--;
            right++;
        }
    }
    
    for (let i = 0; i < s.length; i++) {
        expandAroundCenter(i, i);      // Odd
        expandAroundCenter(i, i + 1);  // Even
    }
    
    return s.substring(start, start + maxLen);
}

// "babad" -> "bab" or "aba"
// "cbbd" -> "bb"
// Time: O(n²), Space: O(1)

Example: Palindromic substrings DP approach

function countSubstringsDP(s) {
    const n = s.length;
    const dp = Array(n).fill(null).map(() => Array(n).fill(false));
    let count = 0;
    
    // Every single character is a palindrome
    for (let i = 0; i < n; i++) {
        dp[i][i] = true;
        count++;
    }
    
    // Check substrings of length 2
    for (let i = 0; i < n - 1; i++) {
        if (s[i] === s[i + 1]) {
            dp[i][i + 1] = true;
            count++;
        }
    }
    
    // Check substrings of length 3+
    for (let len = 3; len <= n; len++) {
        for (let i = 0; i <= n - len; i++) {
            const j = i + len - 1;
            if (s[i] === s[j] && dp[i + 1][j - 1]) {
                dp[i][j] = true;
                count++;
            }
        }
    }
    
    return count;
}

// Same results as expand approach
// Time: O(n²), Space: O(n²)

7. Edit Distance (Levenshtein Distance Algorithm)

Operation Description State Transition Example
Insert Add character to word1 dp[i][j] = dp[i][j-1] + 1 "abc" → "abdc" (insert 'd')
Delete Remove character from word1 dp[i][j] = dp[i-1][j] + 1 "abc" → "ac" (delete 'b')
Replace Change character in word1 dp[i][j] = dp[i-1][j-1] + 1 "abc" → "adc" (replace 'b' with 'd')
Match Characters already equal dp[i][j] = dp[i-1][j-1] No operation needed

Example: Edit distance (Levenshtein distance)

function minDistance(word1, word2) {
    const m = word1.length;
    const n = word2.length;
    
    // dp[i][j] = min operations to convert word1[0..i-1] to word2[0..j-1]
    const dp = Array(m + 1).fill(null)
        .map(() => Array(n + 1).fill(0));
    
    // Base cases: convert empty string
    for (let i = 0; i <= m; i++) dp[i][0] = i; // Delete all
    for (let j = 0; j <= n; j++) dp[0][j] = j; // Insert all
    
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (word1[i - 1] === word2[j - 1]) {
                // Characters match, no operation needed
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.min(
                    dp[i - 1][j] + 1,      // Delete
                    dp[i][j - 1] + 1,      // Insert
                    dp[i - 1][j - 1] + 1   // Replace
                );
            }
        }
    }
    
    return dp[m][n];
}

// "horse", "ros" -> 3
// horse -> rorse (replace 'h' with 'r')
// rorse -> rose (remove 'r')
// rose -> ros (remove 'e')
// Time: O(m*n), Space: O(m*n)

Example: Space-optimized edit distance

function minDistanceOptimized(word1, word2) {
    const m = word1.length;
    const n = word2.length;
    
    let prev = Array(n + 1).fill(0);
    let curr = Array(n + 1).fill(0);
    
    // Initialize base case
    for (let j = 0; j <= n; j++) prev[j] = j;
    
    for (let i = 1; i <= m; i++) {
        curr[0] = i; // Base case for current row
        
        for (let j = 1; j <= n; j++) {
            if (word1[i - 1] === word2[j - 1]) {
                curr[j] = prev[j - 1];
            } else {
                curr[j] = Math.min(
                    prev[j] + 1,        // Delete
                    curr[j - 1] + 1,    // Insert
                    prev[j - 1] + 1     // Replace
                );
            }
        }
        
        [prev, curr] = [curr, prev];
    }
    
    return prev[n];
}

// Same result, Space: O(n) instead of O(m*n)

Example: One edit distance

function isOneEditDistance(s, t) {
    const m = s.length;
    const n = t.length;
    
    // Length difference must be 0 or 1
    if (Math.abs(m - n) > 1) return false;
    if (s === t) return false; // Must be exactly one edit
    
    let i = 0;
    
    // Find first difference
    while (i < Math.min(m, n) && s[i] === t[i]) {
        i++;
    }
    
    if (m === n) {
        // Replace: rest must match
        return s.slice(i + 1) === t.slice(i + 1);
    } else if (m < n) {
        // Insert into s: skip one char in t
        return s.slice(i) === t.slice(i + 1);
    } else {
        // Delete from s: skip one char in s
        return s.slice(i + 1) === t.slice(i);
    }
}

// "ab", "acb" -> true (insert 'c')
// "cab", "ad" -> false (2 edits)
// Time: O(n), Space: O(1)

8. Maximum Subarray Sum (Kadane's Algorithm)

Concept Description Formula Complexity
Core Idea At each position, extend or start new subarray Either continue or restart Greedy choice at each step
State maxEndingHere = max sum ending at current position max(nums[i], maxEndingHere + nums[i]) Local maximum
Global Max Track best result seen so far max(maxSoFar, maxEndingHere) Answer
Performance Single pass through array O(n) time, O(1) space Optimal solution

Example: Kadane's algorithm

function maxSubArray(nums) {
    let maxSoFar = nums[0];
    let maxEndingHere = nums[0];
    
    for (let i = 1; i < nums.length; i++) {
        // Either extend current subarray or start new
        maxEndingHere = Math.max(
            nums[i],
            maxEndingHere + nums[i]
        );
        
        // Update global maximum
        maxSoFar = Math.max(maxSoFar, maxEndingHere);
    }
    
    return maxSoFar;
}

// [-2,1,-3,4,-1,2,1,-5,4]
// maxEndingHere: [-2,1,-2,4,3,5,6,1,5]
// -> 6 (subarray [4,-1,2,1])
// Time: O(n), Space: O(1)

Example: Kadane with indices

function maxSubArrayWithIndices(nums) {
    let maxSoFar = nums[0];
    let maxEndingHere = nums[0];
    let start = 0, end = 0, tempStart = 0;
    
    for (let i = 1; i < nums.length; i++) {
        if (nums[i] > maxEndingHere + nums[i]) {
            maxEndingHere = nums[i];
            tempStart = i; // New subarray starts
        } else {
            maxEndingHere = maxEndingHere + nums[i];
        }
        
        if (maxEndingHere > maxSoFar) {
            maxSoFar = maxEndingHere;
            start = tempStart;
            end = i;
        }
    }
    
    return {
        sum: maxSoFar,
        subarray: nums.slice(start, end + 1)
    };
}

// [-2,1,-3,4,-1,2,1,-5,4]
// -> {sum: 6, subarray: [4,-1,2,1]}

Example: Circular array maximum subarray

function maxSubarraySumCircular(nums) {
    function kadane(arr) {
        let maxSoFar = arr[0];
        let maxEndingHere = arr[0];
        
        for (let i = 1; i < arr.length; i++) {
            maxEndingHere = Math.max(arr[i], maxEndingHere + arr[i]);
            maxSoFar = Math.max(maxSoFar, maxEndingHere);
        }
        
        return maxSoFar;
    }
    
    // Case 1: Maximum subarray is in the middle (normal Kadane)
    const maxNormal = kadane(nums);
    
    // Case 2: Maximum subarray wraps around
    // Find minimum subarray, subtract from total
    const total = nums.reduce((a, b) => a + b, 0);
    const inverted = nums.map(x => -x);
    const maxInverted = kadane(inverted);
    const maxWrap = total + maxInverted; // total - minSubarray
    
    // Edge case: all negative numbers
    if (maxWrap === 0) return maxNormal;
    
    return Math.max(maxNormal, maxWrap);
}

// [5,-3,5] -> 10 (wrap: 5+5)
// [1,-2,3,-2] -> 3 (normal)
// Time: O(n), Space: O(n) for inverted array
Note: Coin Change is fundamental unbounded knapsack. Palindromes use expand-from-center for O(1) space or DP for substring queries. Edit Distance is essential for spell checkers and DNA sequencing. Kadane's Algorithm is the optimal O(n) solution for maximum subarray, can be adapted for 2D, circular arrays, and product variants.

9. House Robber Problem (DP State Transitions)

Variant Constraint State Transition
House Robber I Linear array, can't rob adjacent dp[i] = max money up to house i max(dp[i-1], dp[i-2] + nums[i])
House Robber II Circular array, first and last adjacent Two cases: rob first or rob last max(rob(0..n-2), rob(1..n-1))
House Robber III Binary tree, can't rob parent and child {rob: maxIfRob, skip: maxIfSkip} Tree DP with two states per node
Space Optimization Only need last 2 values Two variables instead of array O(1) space

Example: House robber I

function rob(nums) {
    if (nums.length === 0) return 0;
    if (nums.length === 1) return nums[0];
    
    let prev2 = nums[0];
    let prev1 = Math.max(nums[0], nums[1]);
    
    for (let i = 2; i < nums.length; i++) {
        const curr = Math.max(
            prev1,              // Skip current house
            prev2 + nums[i]     // Rob current house
        );
        prev2 = prev1;
        prev1 = curr;
    }
    
    return prev1;
}

// [1,2,3,1] -> 4 (rob houses 0 and 2: 1+3=4)
// [2,7,9,3,1] -> 12 (rob houses 0,2,4: 2+9+1=12)
// Time: O(n), Space: O(1)

Example: House robber II (circular)

function robCircular(nums) {
    if (nums.length === 1) return nums[0];
    if (nums.length === 2) return Math.max(nums[0], nums[1]);
    
    function robLinear(start, end) {
        let prev2 = 0, prev1 = 0;
        
        for (let i = start; i <= end; i++) {
            const curr = Math.max(prev1, prev2 + nums[i]);
            prev2 = prev1;
            prev1 = curr;
        }
        
        return prev1;
    }
    
    // Case 1: Rob houses 0 to n-2 (exclude last)
    // Case 2: Rob houses 1 to n-1 (exclude first)
    return Math.max(
        robLinear(0, nums.length - 2),
        robLinear(1, nums.length - 1)
    );
}

// [2,3,2] -> 3 (can't rob both 0 and 2)
// [1,2,3,1] -> 4 (rob 1 and 3)
// Time: O(n), Space: O(1)

Example: House robber III (binary tree)

function robTree(root) {
    // Returns {rob: maxIfRobRoot, skip: maxIfSkipRoot}
    function dfs(node) {
        if (!node) return { rob: 0, skip: 0 };
        
        const left = dfs(node.left);
        const right = dfs(node.right);
        
        // If we rob this node, can't rob children
        const rob = node.val + left.skip + right.skip;
        
        // If we skip this node, take max from children
        const skip = Math.max(left.rob, left.skip) + 
                     Math.max(right.rob, right.skip);
        
        return { rob, skip };
    }
    
    const result = dfs(root);
    return Math.max(result.rob, result.skip);
}

//       3
//      / \
//     2   3
//      \   \
//       3   1
// -> 7 (rob 3, 3, 1)
// Time: O(n), Space: O(h) for recursion

1. 0/1 Knapsack Problem (Space Optimization)

Pattern State Recurrence Use Case
Tree DP Compute value at node using children Post-order traversal (children first) Diameter, path sum, subtree properties
Multiple States Track different scenarios per node Return object/array with all states Include/exclude node, different paths
Parent Info Pass info down from parent Pre-order or two-pass approach Distance from root, parent values
Rerooting Answer for each node as root Two DFS: compute subtree, then reroot Sum of distances to all nodes

Example: Binary tree diameter

function diameterOfBinaryTree(root) {
    let diameter = 0;
    
    function height(node) {
        if (!node) return 0;
        
        const leftHeight = height(node.left);
        const rightHeight = height(node.right);
        
        // Diameter through this node
        diameter = Math.max(
            diameter,
            leftHeight + rightHeight
        );
        
        // Return height for parent
        return 1 + Math.max(leftHeight, rightHeight);
    }
    
    height(root);
    return diameter;
}

//       1
//      / \
//     2   3
//    / \
//   4   5
// -> 3 (path 4-2-1-3 or 5-2-1-3)
// Time: O(n), Space: O(h)

Example: Binary tree maximum path sum

function maxPathSum(root) {
    let maxSum = -Infinity;
    
    function maxGain(node) {
        if (!node) return 0;
        
        // Get max gain from children (ignore negative)
        const leftGain = Math.max(maxGain(node.left), 0);
        const rightGain = Math.max(maxGain(node.right), 0);
        
        // Path through current node
        const pathSum = node.val + leftGain + rightGain;
        maxSum = Math.max(maxSum, pathSum);
        
        // Return max gain through this node
        // (can only use one child)
        return node.val + Math.max(leftGain, rightGain);
    }
    
    maxGain(root);
    return maxSum;
}

//      -10
//      / \
//     9  20
//       /  \
//      15   7
// -> 42 (path 15-20-7)
// Time: O(n), Space: O(h)

Example: Count good nodes in tree

function goodNodes(root) {
    let count = 0;
    
    function dfs(node, maxSoFar) {
        if (!node) return;
        
        // Good node: value >= max on path from root
        if (node.val >= maxSoFar) {
            count++;
            maxSoFar = node.val;
        }
        
        dfs(node.left, maxSoFar);
        dfs(node.right, maxSoFar);
    }
    
    dfs(root, -Infinity);
    return count;
}

//       3
//      / \
//     1   4
//    /   / \
//   3   1   5
// -> 4 (nodes 3,3,4,5 are good)
// Time: O(n), Space: O(h)

1. 0/1 Knapsack Problem (Space Optimization)

Concept Representation Operations Use Case
State Compression Use bits to represent subset Each bit = element included/excluded Track visited states compactly
Check Bit (mask >> i) & 1 Test if i-th element included Query state
Set Bit mask | (1 << i) Include i-th element Transition to new state
Clear Bit mask & ~(1 << i) Exclude i-th element Remove from state

Example: Traveling salesman problem (TSP)

function tsp(dist) {
    const n = dist.length;
    const VISITED_ALL = (1 << n) - 1;
    
    // dp[mask][i] = min cost to visit cities in mask, ending at i
    const dp = Array(1 << n).fill(null)
        .map(() => Array(n).fill(Infinity));
    
    dp[1][0] = 0; // Start at city 0
    
    for (let mask = 0; mask <= VISITED_ALL; mask++) {
        for (let last = 0; last < n; last++) {
            if (!(mask & (1 << last))) continue;
            if (dp[mask][last] === Infinity) continue;
            
            for (let next = 0; next < n; next++) {
                if (mask & (1 << next)) continue;
                
                const newMask = mask | (1 << next);
                dp[newMask][next] = Math.min(
                    dp[newMask][next],
                    dp[mask][last] + dist[last][next]
                );
            }
        }
    }
    
    // Find min cost ending at any city
    let result = Infinity;
    for (let i = 0; i < n; i++) {
        result = Math.min(result, 
            dp[VISITED_ALL][i] + dist[i][0]);
    }
    
    return result;
}

// Time: O(2^n * n²), Space: O(2^n * n)

Example: Minimum cost to connect all points (Steiner tree)

function assignTasks(servers, tasks) {
    const n = servers.length;
    const m = tasks.length;
    
    // dp[mask] = min cost to assign tasks in mask
    const dp = Array(1 << m).fill(Infinity);
    dp[0] = 0;
    
    for (let mask = 0; mask < (1 << m); mask++) {
        if (dp[mask] === Infinity) continue;
        
        // Count assigned tasks
        const assigned = countBits(mask);
        if (assigned >= n) continue;
        
        for (let task = 0; task < m; task++) {
            if (mask & (1 << task)) continue;
            
            const newMask = mask | (1 << task);
            const cost = servers[assigned] * tasks[task];
            
            dp[newMask] = Math.min(
                dp[newMask],
                dp[mask] + cost
            );
        }
    }
    
    return dp[(1 << m) - 1];
}

function countBits(n) {
    let count = 0;
    while (n) {
        count += n & 1;
        n >>= 1;
    }
    return count;
}

// Time: O(2^m * m * n), Space: O(2^m)

Example: Partition into k equal sum subsets

function canPartitionKSubsets(nums, k) {
    const sum = nums.reduce((a, b) => a + b, 0);
    if (sum % k !== 0) return false;
    
    const target = sum / k;
    nums.sort((a, b) => b - a);
    
    if (nums[0] > target) return false;
    
    const n = nums.length;
    const dp = Array(1 << n).fill(-1);
    dp[0] = 0;
    
    for (let mask = 0; mask < (1 << n); mask++) {
        if (dp[mask] === -1) continue;
        
        for (let i = 0; i < n; i++) {
            if (mask & (1 << i)) continue;
            
            const newSum = dp[mask] + nums[i];
            if (newSum > target) continue;
            
            const newMask = mask | (1 << i);
            if (dp[newMask] !== -1) continue;
            
            dp[newMask] = newSum % target;
        }
    }
    
    return dp[(1 << n) - 1] === 0;
}

// [4,3,2,3,5,2,1], k=4 -> true
// Can partition into [5], [1,4], [2,3], [2,3]
// Time: O(2^n * n), Space: O(2^n)

1. 0/1 Knapsack Problem (Space Optimization)

Pattern State Iteration Order Example
Interval DP dp[i][j] = answer for subarray/substring [i..j] Increasing interval length Optimal way to merge/split interval
Length Loop Outer loop: interval length from 1 to n Build small intervals first Ensures subproblems solved
Split Point Try all ways to split [i..j] dp[i][j] = min/max over all k in [i..j] Matrix chain, burst balloons
Complexity Usually O(n³) n² states, O(n) transition Can optimize with monotonic queue

Example: Matrix chain multiplication

function matrixChainOrder(dims) {
    // dims[i] = dimensions of matrix i
    // Matrix i has dimensions dims[i-1] x dims[i]
    const n = dims.length - 1;
    
    // dp[i][j] = min multiplications for matrices i to j
    const dp = Array(n).fill(null)
        .map(() => Array(n).fill(0));
    
    // len = chain length
    for (let len = 2; len <= n; len++) {
        for (let i = 0; i < n - len + 1; i++) {
            const j = i + len - 1;
            dp[i][j] = Infinity;
            
            // Try splitting at k
            for (let k = i; k < j; k++) {
                const cost = dp[i][k] + dp[k + 1][j] + 
                    dims[i] * dims[k + 1] * dims[j + 1];
                
                dp[i][j] = Math.min(dp[i][j], cost);
            }
        }
    }
    
    return dp[0][n - 1];
}

// dims = [10, 20, 30, 40, 30]
// Matrices: A(10x20), B(20x30), C(30x40), D(40x30)
// Optimal: ((A*B)*C)*D = 30,000 multiplications
// Time: O(n³), Space: O(n²)

Example: Burst balloons

function maxCoins(nums) {
    // Add virtual balloons with value 1
    const arr = [1, ...nums, 1];
    const n = arr.length;
    
    // dp[i][j] = max coins from bursting balloons (i+1..j-1)
    const dp = Array(n).fill(null)
        .map(() => Array(n).fill(0));
    
    for (let len = 2; len < n; len++) {
        for (let left = 0; left < n - len; left++) {
            const right = left + len;
            
            // Try bursting k last in range (left+1..right-1)
            for (let k = left + 1; k < right; k++) {
                const coins = arr[left] * arr[k] * arr[right] +
                    dp[left][k] + dp[k][right];
                
                dp[left][right] = Math.max(
                    dp[left][right],
                    coins
                );
            }
        }
    }
    
    return dp[0][n - 1];
}

// [3,1,5,8] -> 167
// Burst order: 1,5,3,8
// 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
// Time: O(n³), Space: O(n²)

Example: Palindrome partitioning II

function minCut(s) {
    const n = s.length;
    
    // isPalin[i][j] = is s[i..j] palindrome
    const isPalin = Array(n).fill(null)
        .map(() => Array(n).fill(false));
    
    // Build palindrome table
    for (let len = 1; len <= n; len++) {
        for (let i = 0; i <= n - len; i++) {
            const j = i + len - 1;
            if (len === 1) {
                isPalin[i][j] = true;
            } else if (len === 2) {
                isPalin[i][j] = s[i] === s[j];
            } else {
                isPalin[i][j] = s[i] === s[j] && 
                    isPalin[i + 1][j - 1];
            }
        }
    }
    
    // dp[i] = min cuts for s[0..i]
    const dp = Array(n).fill(Infinity);
    
    for (let i = 0; i < n; i++) {
        if (isPalin[0][i]) {
            dp[i] = 0;
        } else {
            for (let j = 0; j < i; j++) {
                if (isPalin[j + 1][i]) {
                    dp[i] = Math.min(dp[i], dp[j] + 1);
                }
            }
        }
    }
    
    return dp[n - 1];
}

// "aab" -> 1 (cut: "aa|b")
// "a" -> 0 (already palindrome)
// Time: O(n²), Space: O(n²)
Warning: Dynamic programming requires identifying optimal substructure and overlapping subproblems. Space optimization (2D→1D) is common but may prevent path reconstruction. For bitmask DP, limit to n≤20 due to exponential states. Interval DP typically needs O(n³) time - ensure n is manageable. Always verify base cases and state transitions carefully.
Summary: Dynamic Programming is solving complex problems by breaking them into overlapping subproblems. Key techniques: Knapsack (0/1 and Unbounded) for subset selection, LCS/LIS for sequence problems, Edit Distance for string transformations, Kadane for optimal subarrays, Tree DP for hierarchical structures, Bitmask DP for subset enumeration, and Interval DP for range optimization. Master state definition, transition formulas, and space optimization techniques.