0% found this document useful (0 votes)
13 views21 pages

Dynamic Programming - Complete DSA Guide

The document is a comprehensive guide on Dynamic Programming (DP), covering its fundamentals, approaches, classic problems, and optimization techniques. It explains key concepts like optimal substructure, overlapping subproblems, and methods such as memoization and tabulation, along with various DP patterns. Additionally, it provides examples of classic DP problems and their solutions in Python, making it a valuable resource for understanding and applying DP in algorithmic challenges.

Uploaded by

pahohi5743
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
13 views21 pages

Dynamic Programming - Complete DSA Guide

The document is a comprehensive guide on Dynamic Programming (DP), covering its fundamentals, approaches, classic problems, and optimization techniques. It explains key concepts like optimal substructure, overlapping subproblems, and methods such as memoization and tabulation, along with various DP patterns. Additionally, it provides examples of classic DP problems and their solutions in Python, making it a valuable resource for understanding and applying DP in algorithmic challenges.

Uploaded by

pahohi5743
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 21

Dynamic Programming - Complete DSA Guide

Table of Contents
1. Introduction

2. Dynamic Programming Fundamentals

3. DP Approaches

4. Classic DP Problems

5. DP Patterns

6. String DP Problems

7. Tree and Graph DP

8. Optimization Techniques

9. Time & Space Complexity

10. Practice Problems

Introduction
Dynamic Programming (DP) is a powerful algorithmic paradigm that solves complex problems by
breaking them down into simpler subproblems. It combines the efficiency of greedy algorithms with the
correctness of exhaustive search.

Why Dynamic Programming Matters


• Optimization: Significantly reduces time complexity

• Problem Solving: Essential for many algorithmic challenges

• Real Applications: Used in economics, bioinformatics, AI, and more

• Interview Frequency: Very common in technical interviews

• Mathematical Foundation: Builds strong analytical thinking

Dynamic Programming Fundamentals

Core Concepts

1. Optimal Substructure

A problem has optimal substructure if an optimal solution contains optimal solutions to subproblems.
python

# Example: Fibonacci sequence


# F(n) = F(n-1) + F(n-2)
# Optimal solution for F(n) uses optimal solutions for F(n-1) and F(n-2)

2. Overlapping Subproblems

The same subproblems are solved multiple times in a naive recursive approach.

python

# Fibonacci without DP - exponential time


def fib_naive(n):
if n <= 1:
return n
return fib_naive(n-1) + fib_naive(n-2)

# Many subproblems calculated multiple times:


# fib(5) calls fib(4) and fib(3)
# fib(4) calls fib(3) and fib(2)
# fib(3) is calculated twice!

3. Memoization

Store results of expensive function calls to avoid recomputation.

python

def fib_memo(n, memo={}):


if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]

4. Tabulation

Build up solutions from smaller subproblems systematically.


python

def fib_tabulation(n):
if n <= 1:
return n

dp = [0] * (n + 1)
dp[1] = 1

for i in range(2, n + 1):


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

return dp[n]

DP Approaches

1. Top-Down (Memoization)
• Start with the original problem

• Recursively break down into subproblems

• Store results to avoid recomputation

Template:

python

def dp_top_down(params, memo={}):


# Base case
if base_condition:
return base_value

# Check if already computed


if params in memo:
return memo[params]

# Recursive computation
result = compute_from_subproblems()
memo[params] = result

return result

2. Bottom-Up (Tabulation)
• Start with smallest subproblems

• Build up to larger problems

• Usually more space efficient

Template:

python

def dp_bottom_up(n):
# Initialize DP table
dp = [0] * (n + 1)

# Base cases
dp[0] = base_value

# Fill table
for i in range(1, n + 1):
dp[i] = compute_from_previous(dp, i)

return dp[n]

Classic DP Problems

1. Climbing Stairs
Problem: How many ways to reach the top of n stairs if you can climb 1 or 2 steps at a time?
python

def climb_stairs(n):
if n <= 2:
return n

dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2

for i in range(3, n + 1):


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

return dp[n]

# Space optimized version


def climb_stairs_optimized(n):
if n <= 2:
return n

prev2 = 1 # dp[i-2]
prev1 = 2 # dp[i-1]

for i in range(3, n + 1):


current = prev1 + prev2
prev2 = prev1
prev1 = current

return prev1

2. House Robber
Problem: Rob houses to maximize money without robbing adjacent houses.
python

def rob(nums):
if not nums:
return 0
if len(nums) == 1:
return nums[0]

n = len(nums)
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])

for i in range(2, n):


dp[i] = max(dp[i-1], dp[i-2] + nums[i])

return dp[n-1]

# Space optimized
def rob_optimized(nums):
if not nums:
return 0

prev2 = 0 # dp[i-2]
prev1 = 0 # dp[i-1]

for num in nums:


current = max(prev1, prev2 + num)
prev2 = prev1
prev1 = current

return prev1

3. 0/1 Knapsack Problem


Problem: Maximize value in knapsack with weight constraint.
python

def knapsack(weights, values, capacity):


n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]

for i in range(1, n + 1):


for w in range(1, capacity + 1):
# Don't take item i-1
dp[i][w] = dp[i-1][w]

# Take item i-1 if it fits


if weights[i-1] <= w:
dp[i][w] = max(dp[i][w],
dp[i-1][w - weights[i-1]] + values[i-1])

return dp[n][capacity]

# Space optimized (1D array)


def knapsack_1d(weights, values, capacity):
dp = [0] * (capacity + 1)

for i in range(len(weights)):
# Traverse backwards to avoid using updated values
for w in range(capacity, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])

return dp[capacity]

4. Coin Change
Problem: Find minimum coins to make target amount.
python

def coin_change(coins, amount):


dp = [float('inf')] * (amount + 1)
dp[0] = 0

for coin in coins:


for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)

return dp[amount] if dp[amount] != float('inf') else -1

# Count number of ways to make amount


def coin_change_ways(coins, amount):
dp = [0] * (amount + 1)
dp[0] = 1

for coin in coins:


for i in range(coin, amount + 1):
dp[i] += dp[i - coin]

return dp[amount]

5. Longest Increasing Subsequence (LIS)


Problem: Find length of longest increasing subsequence.
python

def lis_dp(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)

# Binary search approach - O(n log n)


def lis_binary_search(nums):
import bisect

tails = []

for num in nums:


pos = bisect.bisect_left(tails, num)
if pos == len(tails):
tails.append(num)
else:
tails[pos] = num

return len(tails)

DP Patterns

1. Linear DP
Problems where state depends on previous states in a linear fashion.

Examples: Fibonacci, Climbing Stairs, House Robber

Template:
python

def linear_dp(arr):
n = len(arr)
dp = [0] * n

# Base cases
dp[0] = base_value

for i in range(1, n):


dp[i] = function_of(dp[i-1], arr[i])

return dp[n-1]

2. Grid DP
Problems involving 2D grids or matrices.

Example: Minimum Path Sum

python

def min_path_sum(grid):
if not grid:
return 0

m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)]

# Base case
dp[0][0] = grid[0][0]

# Fill first row


for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j]

# Fill first column


for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]

# Fill rest of the table


for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]

return dp[m-1][n-1]
3. Interval DP
Problems involving intervals or ranges.

Example: Matrix Chain Multiplication

python

def matrix_chain_multiplication(dimensions):
n = len(dimensions) - 1
dp = [[0] * n for _ in range(n)]

# Length of chain
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')

for k in range(i, j):


cost = (dp[i][k] + dp[k+1][j] +
dimensions[i] * dimensions[k+1] * dimensions[j+1])
dp[i][j] = min(dp[i][j], cost)

return dp[0][n-1]

4. Subset DP
Problems involving subsets or partitions.

Example: Partition Equal Subset Sum


python

def can_partition(nums):
total_sum = sum(nums)

if total_sum % 2 != 0:
return False

target = total_sum // 2
dp = [False] * (target + 1)
dp[0] = True

for num in nums:


for j in range(target, num - 1, -1):
dp[j] = dp[j] or dp[j - num]

return dp[target]

5. State Machine DP
Problems with different states and transitions.

Example: Best Time to Buy and Sell Stock with Transaction Fee

python

def max_profit_with_fee(prices, fee):


hold = -prices[0] # Max profit when holding stock
sold = 0 # Max profit when not holding stock

for i in range(1, len(prices)):


hold = max(hold, sold - prices[i])
sold = max(sold, hold + prices[i] - fee)

return sold

String DP Problems

1. Longest Common Subsequence (LCS)


python

def lcs(text1, text2):


m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]

for i in range(1, m + 1):


for j in range(1, n + 1):
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]

2. Edit Distance (Levenshtein Distance)

python

def edit_distance(word1, word2):


m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]

# Base cases
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j

for i in range(1, m + 1):


for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], # Delete
dp[i][j-1], # Insert
dp[i-1][j-1] # Replace
)

return dp[m][n]

3. Palindromic Subsequences
python

def longest_palindromic_subsequence(s):
n = len(s)
dp = [[0] * n for _ in range(n)]

# Every single character is a palindrome


for i in range(n):
dp[i][i] = 1

# Fill for substrings of length 2 to n


for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1

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

return dp[0][n-1]

Tree and Graph DP

1. Tree DP - Maximum Path Sum


python

def max_path_sum(root):
max_sum = float('-inf')

def max_gain(node):
nonlocal max_sum

if not node:
return 0

# Max gain from left and right subtrees


left_gain = max(max_gain(node.left), 0)
right_gain = max(max_gain(node.right), 0)

# Price of new path including current node


price_new_path = node.val + left_gain + right_gain

# Update global maximum


max_sum = max(max_sum, price_new_path)

# Return max gain if continue with this node


return node.val + max(left_gain, right_gain)

max_gain(root)
return max_sum

2. Graph DP - Longest Path in DAG


python

def longest_path_dag(graph):
in_degree = {node: 0 for node in graph}

# Calculate in-degrees
for node in graph:
for neighbor in graph[node]:
in_degree[neighbor] += 1

# Topological sort using Kahn's algorithm


from collections import deque
queue = deque([node for node in in_degree if in_degree[node] == 0])

dp = {node: 0 for node in graph}


max_path = 0

while queue:
node = queue.popleft()

for neighbor, weight in graph[node]:


dp[neighbor] = max(dp[neighbor], dp[node] + weight)
max_path = max(max_path, dp[neighbor])

in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)

return max_path

Optimization Techniques

1. Space Optimization
Reduce space complexity by keeping only necessary previous states.
python

# From O(n) to O(1) space for Fibonacci


def fib_optimized(n):
if n <= 1:
return n

a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b

return b

2. Memoization with Decorators

python

from functools import lru_cache

@lru_cache(maxsize=None)
def fib_memo_decorator(n):
if n <= 1:
return n
return fib_memo_decorator(n-1) + fib_memo_decorator(n-2)

3. Rolling Array Technique

python

def unique_paths_optimized(m, n):


dp = [1] * n

for i in range(1, m):


for j in range(1, n):
dp[j] += dp[j-1]

return dp[n-1]

4. State Compression
For problems with multiple states, use bit manipulation to compress states.
python

def travel_salesman_dp(dist, n):


# dp[mask][i] = min cost to visit cities in mask ending at city i
dp = {}

def solve(mask, pos):


if mask == (1 << n) - 1:
return dist[pos][0] # Return to start

if (mask, pos) in dp:


return dp[(mask, pos)]

result = float('inf')
for city in range(n):
if not (mask & (1 << city)):
new_mask = mask | (1 << city)
result = min(result, dist[pos][city] + solve(new_mask, city))

dp[(mask, pos)] = result


return result

return solve(1, 0) # Start from city 0

Time & Space Complexity

Common DP Complexities
Problem Type Time Space Space Optimized

Linear DP O(n) O(n) O(1)

2D Grid DP O(mn) O(mn) O(min(m,n))

LCS O(mn) O(mn) O(min(m,n))

Knapsack O(nW) O(nW) O(W)

LIS O(n²) O(n) O(n)

LIS (Binary Search) O(n log n) O(n) O(n)

Matrix Chain O(n³) O(n²) O(n²)

Factors Affecting Complexity


• Number of states: How many unique subproblems

• State transitions: Cost to compute each state

• Overlapping: Degree of subproblem overlap

• Space optimization: Whether rolling arrays can be used


Practice Problems

Beginner Level
1. Min Cost Climbing Stairs

2. Maximum Subarray (Kadane's Algorithm)

3. Range Sum Query - Immutable

4. Pascal's Triangle

5. Unique Paths

Intermediate Level
1. Decode Ways

2. Word Break

3. Triangle (Minimum Path Sum)

4. Maximum Product Subarray

5. Coin Change 2

Advanced Level
1. Regular Expression Matching

2. Interleaving String

3. Distinct Subsequences

4. Maximum Rectangle

5. Burst Balloons

Expert Level
1. Shortest Superstring

2. Optimal Binary Search Tree

3. Palindrome Partitioning II

4. Count of Range Sum

5. Strange Printer

Tips for Success

Problem-Solving Strategy
1. Identify DP characteristics: Optimal substructure and overlapping subproblems

2. Define state: What does dp[i] or dp[i][j] represent?

3. Find recurrence relation: How to compute current state from previous states

4. Determine base cases: What are the simplest subproblems?

5. Choose approach: Top-down (memoization) or bottom-up (tabulation)

6. Optimize space: Can you reduce space complexity?

Common Patterns Recognition


• Optimization problems: Usually ask for minimum/maximum

• Counting problems: How many ways to do something

• Decision problems: Is it possible to achieve something

• Sequence problems: Often 1D DP

• Grid problems: Usually 2D DP

• Interval problems: Consider all possible splits

Common Pitfalls
• Not identifying the optimal substructure

• Incorrect base cases

• Wrong order of computation in tabulation

• Forgetting to handle edge cases

• Not optimizing space when possible

• Recursive solution without memoization leading to TLE

Best Practices
• Start with recursive solution, then add memoization

• Always handle edge cases first

• Use meaningful variable names for DP states

• Test with small examples first

• Consider both time and space complexity

• Practice state definition - it's often the hardest part

Advanced Tips
• Matrix exponentiation: For problems with linear recurrence

• Digit DP: For problems involving digit constraints

• Probability DP: For problems involving probabilities

• Bitmask DP: For problems involving subsets

• Tree DP: For problems on trees

• Profile DP: For problems on grids with constraints

Conclusion
Dynamic Programming is a powerful technique that transforms exponential-time recursive solutions into
polynomial-time algorithms. Success in DP requires:

1. Pattern Recognition: Identifying when DP applies

2. State Definition: Clearly defining what each DP state represents

3. Recurrence Relations: Finding the mathematical relationship between states

4. Implementation: Choosing between top-down and bottom-up approaches

5. Optimization: Reducing space complexity when possible

The key to mastering DP is practice and pattern recognition. Start with basic problems like Fibonacci and
Climbing Stairs, then gradually work your way up to more complex problems. Focus on understanding
the underlying principles rather than memorizing solutions.

Remember: DP is not just about the algorithm - it's about the way of thinking. Once you develop DP
intuition, you'll find that many seemingly complex problems have elegant DP solutions.

You might also like