Algorithms

Module 6· 9 min read· 5 Questions·completed

6. Dynamic Programming (DP)

Overlapping subproblems, optimal substructure, Memoization vs Tabulation, space optimization, 0/1 Knapsack, Longest Common Subsequence (LCS), Matrix Chain Multiplication (MCM), and Coin Change across C, C++, and Python

NPTEL / GATE CS / UGC NET Core Subject Key Exam Questions: 0/1 Knapsack recurrence and table filling, LCS length recurrence (O(mn)O(mn)), Matrix Chain Multiplication parenthesization (O(n3)O(n^3)), Coin Change variations, and Space Optimization techniques.


1. Prerequisites & What You Should Know#

Before mastering Dynamic Programming, ensure you understand:

  • Recursion Trees & Redundant Work: Being able to trace a recursive call stack and spot repeated subproblem evaluations.
  • DAG (Directed Acyclic Graph) Topological Ordering: Every DP problem represents a topological sort over a DAG of dependent states!
  • State Representation: Defining what parameters uniquely identify a subproblem (e.g., dp[i][w] = max value using first ii items with capacity ww).
  • Mathematical Induction: Showing that if smaller states are optimal, the transition function preserves optimality for larger states.

2. Conceptual Intuition: The Fibonacci Tragedy & The Sticky Note#

Diagram / Text
Naive Recursive Fibonacci: fib(5)
                           fib(5)
                       /           \
                  fib(4)            fib(3)
                 /      \          /      \
             fib(3)    fib(2)    fib(2)   fib(1)
            /     \     /   \     /   \
        fib(2)  fib(1) f(1) f(0) f(1) f(0)
        /   \
      f(1)  f(0)

Notice: fib(3) is recomputed 2 times!
        fib(2) is recomputed 3 times!
Total calls grow exponentially as O(2^n) = O(1.618^n)!
For n = 50, naive recursion takes ~1.12 * 10^15 operations (years of compute).

The Sticky Note (Memoization) Principle#

"Those who cannot remember the past are condemned to repeat it." — George Santayana (Adapted by Richard Bellman)

Whenever you compute an answer to a subproblem for the first time, write it down on a sticky note (lookup table / array). Before performing any recursive calculation, first check if you already have a sticky note for that exact input. If so, return it in O(1)O(1) constant time!

This collapses the entire exponential tree down into a linear chain of nn unique states: O(n)O(n) time!


3. The Two Golden Rules of Dynamic Programming#

A problem can be solved using Dynamic Programming if and only if it satisfies:

  1. Overlapping Subproblems: A recursive algorithm visits the exact same subproblems over and over, rather than generating brand-new subproblems at every step (which occurs in Divide & Conquer).
  2. Optimal Substructure: An optimal solution to the global problem can be constructed by combining the optimal solutions of its subproblems.

4. Memoization (Top-Down) vs Tabulation (Bottom-Up)#

Diagram / Text
Top-Down (Memoization):
[Main Problem] -> Needs Subproblem A & B -> Recurse downwards -> Base cases reached -> Store in cache & return up

Bottom-Up (Tabulation):
[Base Cases: dp[0], dp[1]] -> Iteratively compute dp[2] -> dp[3] -> ... -> [dp[N]: Final Answer]
FeatureTop-Down (Memoization)Bottom-Up (Tabulation)
FormulationRecursive with cache dictionary/arrayIterative table filling via nested loops
Call Stack OverheadIncurs function call stack depth O(N)O(N) (risk of stack overflow)Zero call-stack overhead (O(1)O(1) auxiliary stack)
State ExplorationComputes only states strictly reachable from rootSystematically computes all states in topological order
Space OptimizationDifficult to discard old statesEasy to optimize (e.g. keep only previous row or previous 2 numbers)

5. Classic DP Formulations Deconstructed#

5.1 0/1 Knapsack Recurrence#

Given nn items with weights wt[i]wt[i] and values val[i]val[i], and knapsack capacity WW:

DP[i][w]={DP[i1][w]if wt[i1]>wmax(DP[i1][w],val[i1]+DP[i1][wwt[i1]])if wt[i1]wDP[i][w] = \begin{cases} DP[i-1][w] & \text{if } wt[i-1] > w \\ \max(DP[i-1][w], \, val[i-1] + DP[i-1][w - wt[i-1]]) & \text{if } wt[i-1] \le w \end{cases}
  • Time Complexity: Θ(nW)\Theta(n W)
  • Why is it Pseudo-Polynomial? WW is an integer value, not the number of inputs. The input size of WW in bits is log2W\log_2 W. Hence runtime is exponential in terms of input bit length (O(n2b)O(n \cdot 2^b))!
  • 1D Space Optimization: By looping capacity ww in reverse order (from WW down to wt[i]wt[i]), we ensure each item is used at most once while reducing memory from O(nW)O(nW) to O(W)O(W)!

5.2 Longest Common Subsequence (LCS)#

For strings X[0m1]X[0 \dots m-1] and Y[0n1]Y[0 \dots n-1]:

LCS[i][j]={0if i=0 or j=01+LCS[i1][j1]if X[i1]==Y[j1]max(LCS[i1][j],LCS[i][j1])if X[i1]Y[j1]LCS[i][j] = \begin{cases} 0 & \text{if } i=0 \text{ or } j=0 \\ 1 + LCS[i-1][j-1] & \text{if } X[i-1] == Y[j-1] \\ \max(LCS[i-1][j], \, LCS[i][j-1]) & \text{if } X[i-1] \ne Y[j-1] \end{cases}
  • Computes the longest sequence of characters that appear in both strings in the same relative order (not necessarily contiguous).
  • Backtracking from DP[m][n]DP[m][n] reconstructs the exact subsequence string in O(m+n)O(m + n) time.

5.3 Matrix Chain Multiplication (MCM)#

Given matrices A1,A2,,AnA_1, A_2, \dots, A_n where AiA_i has dimension pi1×pip_{i-1} \times p_i:

  • Matrix multiplication is associative: (A1A2)A3=A1(A2A3)(A_1 A_2) A_3 = A_1 (A_2 A_3).
  • But the number of scalar multiplications depends drastically on the order:
    • Multiplying A10×100×B100×5×C5×50A_{10 \times 100} \times B_{100 \times 5} \times C_{5 \times 50}:
      • Order (AB)C(AB)C: (10×100×5)+(10×5×50)=5,000+2,500=7,500(10 \times 100 \times 5) + (10 \times 5 \times 50) = 5{,}000 + 2{,}500 = 7{,}500 operations.
      • Order A(BC)A(BC): (100×5×50)+(10×100×50)=25,000+50,000=75,000(100 \times 5 \times 50) + (10 \times 100 \times 50) = 25{,}000 + 50{,}000 = 75{,}000 operations! (10× slower!)
  • Recurrence:
m[i,j]={0if i=jminik<j{m[i,k]+m[k+1,j]+pi1pkpj}if i<jm[i, j] = \begin{cases} 0 & \text{if } i = j \\ \min_{i \le k < j} \{ m[i, k] + m[k+1, j] + p_{i-1} p_k p_j \} & \text{if } i < j \end{cases}
  • Time Complexity: Θ(n3)\Theta(n^3), Space Complexity: Θ(n2)\Theta(n^2).

6. Real-World Applications#

  1. Bioinformatics & Genomics: Needleman-Wunsch (global alignment) and Smith-Waterman (local alignment) algorithms align DNA and amino-acid sequences using 2D DP matrices.
  2. Version Control (git diff): Myers diff algorithm finds the shortest edit script (LCS) between two file revisions.
  3. Speech Recognition & Telecom: The Viterbi Algorithm decodes hidden states in Hidden Markov Models (HMMs) for natural speech and CDMA mobile phone error-correcting codes.
  4. Natural Language Processing: Spell checkers, autocomplete suggestions, and search engines compute Levenshtein Minimum Edit Distance via DP.

7. Implementation in C, C++, and Python with Syntax Logic#

0/1 Knapsack: 1D Space Optimized O(W
#include <stdio.h>
#include <string.h>

/**
 * C Syntax Logic Note:
 * 1. 1D Array Optimization: Notice dp[w] depends only on previous row dp[w - wt[i]].
 *    Traversing w BACKWARDS from W down to wt[i] prevents overwriting values from
 *    the current item, shrinking space from O(nW) to O(W)!
 * 2. memset(): Rapidly zeroes the buffer in contiguous memory.
 */

int knapsack01(int W, int wt[], int val[], int n) {
    int dp[W + 1];
    memset(dp, 0, sizeof(dp));

    for (int i = 0; i < n; i++) {
        // Iterate backwards to ensure 0/1 (single item usage) constraint!
        for (int w = W; w >= wt[i]; w--) {
            int take = val[i] + dp[w - wt[i]];
            if (take > dp[w]) {
                dp[w] = take;
            }
        }
    }
    return dp[W];
}

Practice Quiz

Test your understanding with step-by-step solutions

Practice Quiz

5 questions · 90s per question

Each question has a 90-second time limit. Unanswered questions will be auto-submitted when time runs out.