SwadhyAI Notes

All DSA notes

DSA Dynamic Programming

Dynamic Programming

The query hits at n = 40 catalog SKUs and a budget of 10⁴. Brute-force subsets are correct; 2⁴⁰ is not a one-second answer. You need work proportional to a , not to the full recursion tree.

DP is not “use a table.” It requires , , and a precise before any cache or loop. Then choose or .

Algorithm journey

1

What inputs uniquely determine a subproblem answer?

2

Can an optimum be built from optima of those states?

3

Will the same state recur enough to pay for caching?

4

Memoize or tabulate

Compute each reachable state once; extract the answer.

What actually happens

Naive idea: recurse on every choice and recompute shared prefixes. Correct on tiny n. On grid paths or knapsack-style budgets, the call tree explodes while the distinct states stay polynomial (or pseudo-polynomial).

Better idea: name the , prove and , then store each answer once. Time becomes (reachable states) × (transitions per state), not the size of the naive tree.

The improved design still fails when the state misses a field (wrong cache key), when subproblems do not overlap, when the state space is exponential and you needed approximation, or when you optimize without optimal substructure.

Invariant

After a state is fully solved, its stored value equals the true answer for that exact subproblem. Every transition may read only already-correct predecessor states (or recurse into them under memoization).

How to choose DP

1. Can you define the state? If you cannot name the inputs that fix the answer, do not open a DP table yet.

2. Optimal substructure + overlap? Both required for classic DP. Overlap alone is caching; optimum alone may still need search.

3. Top-down or bottom-up? Natural recursion and sparse reachability favor memoization. Tight loops, space rolling, and no stack risk favor tabulation.

Quantitative check

Naive Fibonacci recursion is Θ(φⁿ) calls. Memoized Fib is Θ(n) states. A 100×100 grid path DP is about 10⁴ cells; unrestricted path enumeration is binomial and huge. At n = 40 subset DP without structure, 2⁴⁰ states are impossible: compress the state or change the model.

Grid Paths Visualizer (2D DP)

Counting unique paths from top-left to bottom-right.

Step 0 of -1

Current Cell
Source (Top/Left)
Filled

Core principles

1. Overlapping subproblems

The problem breaks into smaller subproblems that are solved multiple times. Solve each once and store the result.

Example: Computing Fibonacci(5) requires Fibonacci(3) multiple times

2. Optimal substructure

The optimal solution can be constructed from optimal solutions of its subproblems.

Example: Shortest path from A to C through B = shortest(A to B) + shortest(B to C) when that decomposition is valid

DP vs divide and conquer

Both break problems into subproblems. Divide and conquer solves independent subproblems (merge sort halves). DP solves overlapping subproblems and remembers answers.

Two approaches

🔝 Top-Down (Memoization)

  • •Start with original problem, break down recursively
  • •Store results in cache (memo) as you solve
  • •Check cache before solving subproblem
  • •Natural recursive thinking
  • •Only solves needed subproblems

⬇️ Bottom-Up (Tabulation)

  • •Start with smallest subproblems, build up
  • •Fill table/array iteratively
  • •No recursion overhead
  • •Better space optimization possible
  • •Solves all subproblems

🏆 Memoization vs. Tabulation: Which to choose?

FeatureTop-Down (Memoization)Bottom-Up (Tabulation)
ImplementationRecursive (Natural for many)Iterative (Requires clear ordering)
Subproblems solvedOnly those needed for the solutionAll subproblems in the table
OverheadRecursive calls (Call stack overhead)No recursion (Faster execution)
Space OptimizationDifficult to optimize away the cacheEasier (e.g., use only last 2 rows/elements)
RiskStack Overflow for large 'N'No recursion-stack risk; table size and numeric overflow still matter

State design & DAG of subproblems

A DP formulation is a directed acyclic graph: each state points only to strictly smaller states in a well-founded measure (smaller index, remaining budget, bitmask subset, etc.). If you cannot define such an order, you may have a cycle, pure DP will not terminate without extra structure.

Dimension checklist: What uniquely identifies a subproblem? (index i, end j, sum s, mask of used items, last digit …) The product of domain sizes is your table size, watch for exponential blow-up before coding.

The six-line DP contract

1 · State

A complete, minimal description of one subproblem. If two calls can have different answers, their keys must differ.

2 · Meaning

Write one sentence: “dp[…] equals …”. This sentence determines dimensions, base cases, and the final lookup.

3 · Transition

Enumerate every legal first/last choice exactly once, then combine already-correct smaller states.

4 · Base cases

Smallest valid states plus explicit impossible states. Distinguish impossible from a legitimate zero or negative answer.

5 · Order

A topological order of the implicit state DAG: recursion postorder, increasing index, interval length, or popcount.

6 · Answer

Name the exact state or aggregate returned, and store parent decisions if the problem asks for a witness.

Complexity: count the state graph

time = reachable states × transitions examined per state

space = stored states + recursion depth + reconstruction metadata

Do not say “there are two loops, so O(n²)” until dimensions are named. A state (index, remainingCapacity) gives O(nW) pseudo-polynomial time: W is a numeric value, not its bit length. Sparse memoization may visit fewer states; hash and tuple-key costs still count.

Common archetypes and the hidden choice

ArchetypeState clueTransition choice
Prefix / sequenceBest answer using first i itemsSkip/take, or choose previous compatible index
GridPosition plus limited resource/stateWhich predecessor cell or movement arrives here
Two stringsPrefix lengths (i,j)Match, delete, insert, replace, or skip one side
KnapsackItem index + budgetExclude/include; loop direction distinguishes 0/1 from unbounded
IntervalContiguous [left,right]Last operation or split point k
Tree / DAGNode + decision inherited from parentCombine independent child states after dependencies

Optimization ladder

  1. 1. Prune the state: remove dimensions derivable from others; canonicalize symmetric states.
  2. 2. Skip unreachable states: top-down memoization or sparse maps help when the nominal table is mostly empty.
  3. 3. Compress memory: rolling rows are valid only when transitions no longer read overwritten history. Keep parent choices if reconstruction is required.
  4. 4. Accelerate transitions: prefix minima/sums, monotone queues, bitsets, coordinate compression, or specialized data structures.
  5. 5. Use advanced optimization only with proof: divide-and-conquer, Knuth, and convex-hull optimization require monotonicity/convexity conditions; they are not syntax templates.

When DP is not the answer

  • Greedy suffices: if an exchange argument proves a local choice is globally safe (activity selection, Huffman merging, some scheduling).
  • No overlap: divide-and-conquer like merge sort, subproblems are disjoint, so memo tables buy nothing.
  • State space too large: meet-in-the-middle, branch-and-bound, or approximation algorithms may be required instead of exact DP.

🎯 Key Insight: The DP Pattern

To solve a problem with DP, follow these steps:

  1. Identify if the problem has overlapping subproblems and optimal substructure
  2. Define the state (what parameters uniquely identify a subproblem?)
  3. Write the recurrence relation (how to compute state from smaller states?)
  4. Determine base cases (smallest subproblems with known answers)
  5. Choose top-down (memoization) or bottom-up (tabulation)
  6. Optimize space if possible (often can reduce from 2D to 1D)

💻 Classic Example: Fibonacci Numbers

❌ Naive Recursion (Exponential Time)

Python

python
def fibonacci_naive(n):
    """
    Naive recursive approach - VERY SLOW!
    Time: O(2^n) - exponential
    Space: O(n) - recursion stack
    
    Problem: Recalculates same values many times
    fib(5) calls fib(3) twice, fib(2) three times, etc.
    """
    if n < 0:
        raise ValueError("n must be non-negative")
    if n <= 1:
        return n
    return fibonacci_naive(n - 1) + fibonacci_naive(n - 2)

# For n=40, this takes several seconds!
print(fibonacci_naive(10))  # 55

Java

java
import java.math.BigInteger;

public class FibonacciNaive {
    /**
     * Naive recursive approach - VERY SLOW!
     * Time: O(2^n) - exponential
     * Space: O(n) - recursion stack
     * 
     * Problem: Recalculates same values many times
     * fib(5) calls fib(3) twice, fib(2) three times, etc.
     */
    public static BigInteger fibonacciNaive(int n) {
        if (n < 0) throw new IllegalArgumentException("n must be non-negative");
        if (n <= 1) {
            return BigInteger.valueOf(n);
        }
        return fibonacciNaive(n - 1).add(fibonacciNaive(n - 2));
    }
    
    public static void main(String[] args) {
        // For n=40, this takes several seconds!
        System.out.println(fibonacciNaive(10));  // 55
    }
}

✅ Top-Down DP (Memoization)

Python

python
def fibonacci_memo(n, memo=None):
    """
    Top-down DP with memoization
    Time: O(n) - each subproblem solved once
    Space: O(n) - memo dictionary + recursion stack
    
    Stores results to avoid recalculation
    """
    if n < 0:
        raise ValueError("n must be non-negative")
    if memo is None:
        memo = {}
    
    # Check if already computed
    if n in memo:
        return memo[n]
    
    # Base cases
    if n <= 1:
        return n
    
    # Compute and store
    memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
    return memo[n]

# For n=40, this is instant!
print(fibonacci_memo(100))  # 354224848179261915075

Java

java
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

public class FibonacciMemo {
    /**
     * Top-down DP with memoization
     * Time: O(n) - each subproblem solved once
     * Space: O(n) - memo map + recursion stack
     * 
     * Stores results to avoid recalculation
     */
    public static BigInteger fibonacciMemo(int n, Map<Integer, BigInteger> memo) {
        if (n < 0) throw new IllegalArgumentException("n must be non-negative");
        // Check if already computed
        if (memo.containsKey(n)) {
            return memo.get(n);
        }
        
        // Base cases
        if (n <= 1) {
            return BigInteger.valueOf(n);
        }
        
        // Compute and store
        BigInteger result = fibonacciMemo(n - 1, memo).add(fibonacciMemo(n - 2, memo));
        memo.put(n, result);
        return result;
    }
    
    public static void main(String[] args) {
        Map<Integer, BigInteger> memo = new HashMap<>();
        BigInteger expected = new BigInteger("354224848179261915075");
        if (!fibonacciMemo(100, memo).equals(expected)) throw new AssertionError();
    }
}

✅ Bottom-Up DP (Tabulation)

Python

python
def fibonacci_tabulation(n):
    """
    Bottom-up DP with tabulation
    Time: O(n) - single loop
    Space: O(n) - dp array
    
    Builds solution iteratively from base cases
    """
    if n < 0:
        raise ValueError("n must be non-negative")
    if n <= 1:
        return n
    
    # Create table
    dp = [0] * (n + 1)
    dp[0] = 0
    dp[1] = 1
    
    # Fill table bottom-up
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    
    return dp[n]

print(fibonacci_tabulation(100))  # 354224848179261915075

Java

java
import java.math.BigInteger;

public class FibonacciTabulation {
    /**
     * Bottom-up DP with tabulation
     * Time: O(n) - single loop
     * Space: O(n) - dp array
     * 
     * Builds solution iteratively from base cases
     */
    public static BigInteger fibonacciTabulation(int n) {
        if (n < 0) throw new IllegalArgumentException("n must be non-negative");
        if (n <= 1) {
            return BigInteger.valueOf(n);
        }
        
        // Create table
        BigInteger[] dp = new BigInteger[n + 1];
        dp[0] = BigInteger.ZERO;
        dp[1] = BigInteger.ONE;
        
        // Fill table bottom-up
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1].add(dp[i - 2]);
        }
        
        return dp[n];
    }
    
    public static void main(String[] args) {
        BigInteger expected = new BigInteger("354224848179261915075");
        if (!fibonacciTabulation(100).equals(expected)) throw new AssertionError();
    }
}

🚀 Space-Optimized DP

Python

python
def fibonacci_optimized(n):
    """
    Space-optimized bottom-up DP
    Time: O(n)
    Space: O(1) - only two variables!
    
    We only need last two values, not entire array
    """
    if n < 0:
        raise ValueError("n must be non-negative")
    if n <= 1:
        return n
    
    prev2 = 0  # fib(0)
    prev1 = 1  # fib(1)
    
    for i in range(2, n + 1):
        current = prev1 + prev2
        prev2 = prev1
        prev1 = current
    
    return prev1

print(fibonacci_optimized(100))  # 354224848179261915075

Java

java
import java.math.BigInteger;

public class FibonacciOptimized {
    /**
     * Space-optimized bottom-up DP
     * Time: O(n)
     * Space: O(1) - only two variables!
     * 
     * We only need last two values, not entire array
     */
    public static BigInteger fibonacciOptimized(int n) {
        if (n < 0) throw new IllegalArgumentException("n must be non-negative");
        if (n <= 1) {
            return BigInteger.valueOf(n);
        }
        
        BigInteger prev2 = BigInteger.ZERO;  // fib(0)
        BigInteger prev1 = BigInteger.ONE;   // fib(1)
        
        for (int i = 2; i <= n; i++) {
            BigInteger current = prev1.add(prev2);
            prev2 = prev1;
            prev1 = current;
        }
        
        return prev1;
    }
    
    public static void main(String[] args) {
        BigInteger expected = new BigInteger("354224848179261915075");
        if (!fibonacciOptimized(100).equals(expected)) throw new AssertionError();
    }
}

📊 Performance Comparison

ApproachTimeSpacen=40 Runtime
Naive RecursionO(2ⁿ)O(n)~30 seconds
MemoizationO(n)O(n)~0.001 seconds
TabulationO(n)O(n)~0.001 seconds
OptimizedO(n)O(1)~0.001 seconds

💡 Pro Tip: When to Use DP

Ask yourself these questions to identify DP problems:

  • • Can the problem be broken into overlapping subproblems?
  • • Does it have optimal substructure (optimal solution from optimal subproblems)?
  • • Are you finding maximum/minimum or counting ways?
  • • Does naive recursion have repeated calculations?

Common DP keywords: "maximum", "minimum", "longest", "shortest", "count ways", "optimize"

🧩 Advanced DP Patterns

1. Longest Common Subsequence (LCS)String DP

Finding the longest subsequence present in both strings in the same relative order. Unlike substrings, subsequences are not required to occupy consecutive positions.

Recurrence Relation:

if s1[i-1] == s2[j-1]:

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

else:

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

  • ✅ Base Case: dp[i][0] = 0, dp[0][j] = 0
  • 🚀 Time: O(N * M)
  • 💾 Space: O(N * M) or O(min(N, M))

Python

python
def lcs(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = 1 + dp[i-1][j-1]
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
                
    return dp[m][n]

assert lcs("AGGTAB", "GXTXAYB") == 4
assert lcs("🙂A", "🙂B") == 1

Java

java
public class LcsDP {
    static int lcs(String first, String second) {
        int[] firstPoints = first.codePoints().toArray();
        int[] secondPoints = second.codePoints().toArray();
        int[][] dp = new int[firstPoints.length + 1][secondPoints.length + 1];
        for (int i = 1; i <= firstPoints.length; i++) {
            for (int j = 1; j <= secondPoints.length; j++) {
                if (firstPoints[i - 1] == secondPoints[j - 1]) {
                    dp[i][j] = 1 + dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[firstPoints.length][secondPoints.length];
    }

    public static void main(String[] args) {
        if (lcs("AGGTAB", "GXTXAYB") != 4 || lcs("🙂A", "🙂B") != 1) {
            throw new AssertionError();
        }
    }
}

2. Longest Increasing Subsequence (LIS)Array DP

Finding the length of the longest subsequence in an array such that all elements of the subsequence are sorted in increasing order.

State Definition:

dp[i] = length of LIS ending exactly at index i.

Recurrence:

dp[i] = 1 + max(dp[j]) for all j < i such that arr[j] < arr[i]

  • 🚀 Time: O(N²) (Basic), O(N log N) (Binary Search)
  • 💾 Space: O(N)

Python

python
def length_of_lis(nums):
    if not nums: return 0
    n = len(nums)
    dp = [1] * n
    
    for i in range(1, n):
        for j in range(i):
            if nums[i] > nums[j]:
                dp[i] = max(dp[i], dp[j] + 1)
                
    return max(dp)

assert length_of_lis([10, 9, 2, 5, 3, 7, 101, 18]) == 4

Java

java
import java.util.Arrays;

public class LisDP {
    static int lengthOfLis(int[] values) {
        if (values.length == 0) return 0;
        int[] dp = new int[values.length];
        Arrays.fill(dp, 1);
        int answer = 1;
        for (int i = 1; i < values.length; i++) {
            for (int j = 0; j < i; j++) {
                if (values[j] < values[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            answer = Math.max(answer, dp[i]);
        }
        return answer;
    }

    public static void main(String[] args) {
        int[] input = {10, 9, 2, 5, 3, 7, 101, 18};
        if (lengthOfLis(input) != 4) throw new AssertionError();
    }
}

✅ Pros & ❌ Cons

✅ Advantages

  • ✓Evaluates each reachable state once instead of repeating the same subproblem
  • ✓Computes the exact recurrence when the state, transitions, base cases, and order are correct
  • ✓Avoids redundant calculations
  • ✓Systematic approach to optimization problems
  • ✓Can be space-optimized

❌ Disadvantages

  • ✗Can be difficult to identify DP problems
  • ✗Requires extra space for memoization/table
  • ✗Finding recurrence relation can be tricky
  • ✗Not all optimization problems have DP solution
  • ✗Can have high space complexity

💼 Real World Applications

🗺️ GPS & Route Planning

GPS systems use DP (Bellman-Ford, Floyd-Warshall) to find shortest paths between locations, storing intermediate results to quickly recalculate when routes change.

Example: Google Maps, Waze navigation

📱 Autocomplete & Spell Check

Edit distance (Levenshtein distance) uses DP to find the minimum number of edits to transform one word to another, powering spell checkers and autocorrect.

Example: Smartphone keyboards, search suggestions

💰 Finance & Trading

Portfolio optimization, option pricing (binomial model), and algorithmic trading use DP to maximize returns while managing risk.

Example: Stock trading algorithms, risk management

🧬 Bioinformatics

DNA sequence alignment uses DP (Needleman-Wunsch algorithm) to find similarities between genetic sequences, crucial for understanding evolution and disease.

Example: Genome sequencing, protein folding

Exercises

  1. For grid paths from (0,0) to (n,n) moving only right/down, write the and prove with a tiny counterexample recursion tree (n = 3).
  2. n = 10⁵ stairs, climb 1 or 2. Compare naive recursion, memoization, and O(1)-space tabulation. Where does the call stack still fail?
  3. Give a problem with optimal substructure but no useful overlap (or the reverse). Would you still call it DP?

Primary sources

  • Bellman, R. (1957): Dynamic Programming (principle of optimality).
  • Cormen et al., Introduction to Algorithms (CLRS): dynamic programming chapter (optimal substructure, overlapping subproblems).
  • Kleinberg & Tardos, Algorithm Design: DP design patterns and weighted interval scheduling.

Takeaway

State first, then optimal substructure and overlapping subproblems. Memoization and tabulation are delivery mechanisms for the same recurrence.