본문으로 건너뛰기
Previous
Next
DP Practice Problems — Complete Guide

DP Practice Problems — Complete Guide

DP Practice Problems — Complete Guide

이 글의 핵심

Master DP practice problems: coding interview DP problem-solving strategies. Learn Make One, Edit Distance, Coin Change, LIS, and Knapsack with principles and code.

Introduction

DP problems require practice in defining states and finding recurrence relations. The problems below are solved using both Bottom-Up (fill table from front) and Top-Down (recursion + memo) to master transition relationships.

1. Problem 1: Make One

Problem Description

Make integer N into 1. You can use three operations:

  1. Divide by 3 if divisible by 3
  2. Divide by 2 if divisible by 2
  3. Subtract 1 Find minimum number of operations.

Bottom-Up Solution

If dp[i] is “minimum operations to make integer i into 1”, there are three ways to reach i: subtract 1 from i-1, or divide from i/2 or i/3 when divisible. Filling from small numbers allows updating each dp[i] in O(1).

# 함수 정의 및 구현
def make_one(n):
    """
    dp[i] = minimum operations to make i into 1
    """
    dp = [0] * (n + 1)
    
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + 1
        
        if i % 2 == 0:
            dp[i] = min(dp[i], dp[i // 2] + 1)
        
        if i % 3 == 0:
            dp[i] = min(dp[i], dp[i // 3] + 1)
    
    return dp[n]
print(make_one(10))  # 3 (10 → 9 → 3 → 1)

Step-by-Step Trace:

n=10
dp[0]=0, dp[1]=0
dp[2] = dp[1]+1 = 1
      = min(1, dp[1]+1) = 1 (divide by 2)
dp[3] = dp[2]+1 = 2
      = min(2, dp[1]+1) = 1 (divide by 3)
dp[4] = dp[3]+1 = 2
      = min(2, dp[2]+1) = 2 (divide by 2)
...
dp[9] = dp[8]+1 = 4
      = min(4, dp[3]+1) = 2 (divide by 3)
dp[10] = dp[9]+1 = 3
       = min(3, dp[5]+1) = 3 (divide by 2)
Result: 3

Top-Down Solution

def make_one_recursive(n, memo=None):
    if memo is None:
        memo = {}
    
    if n == 1:
        return 0
    
    if n in memo:
        return memo[n]
    
    result = make_one_recursive(n - 1, memo) + 1
    
    if n % 2 == 0:
        result = min(result, make_one_recursive(n // 2, memo) + 1)
    
    if n % 3 == 0:
        result = min(result, make_one_recursive(n // 3, memo) + 1)
    
    memo[n] = result
    return result
print(make_one_recursive(10))  # 3

2. Problem 2: Edit Distance

Problem Description

Find minimum operations to convert string s1 to s2.

  • Operations: insert, delete, replace

Solution

def edit_distance(s1, s2):
    """
    dp[i][j] = minimum operations to convert s1[:i] to s2[:j]
    """
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # Initial values
    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 s1[i-1] == s2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = min(
                    dp[i-1][j] + 1,    # delete
                    dp[i][j-1] + 1,    # insert
                    dp[i-1][j-1] + 1   # replace
                )
    
    return dp[m][n]
print(edit_distance("horse", "ros"))  # 3
print(edit_distance("intention", "execution"))  # 5

DP Table Trace:

s1="horse", s2="ros"
    ""  r  o  s
""   0  1  2  3
h    1  1  2  3
o    2  2  1  2
r    3  2  2  2
s    4  3  3  2
e    5  4  4  3
Operations:
1. replace h→r
2. delete o
3. delete e
Result: 3

3. Problem 3: Coin Change

Problem Description

Find minimum number of coins to make amount with given coins.

Solution

def coin_change(coins, amount):
    """
    dp[i] = minimum coins to make amount i
    """
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    
    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i:
                dp[i] = min(dp[i], dp[i - coin] + 1)
    
    return dp[amount] if dp[amount] != float('inf') else -1
coins = [1, 2, 5]
print(coin_change(coins, 11))  # 3 (5+5+1)
print(coin_change(coins, 3))   # 2 (2+1)

DP Table Trace:

coins=[1,2,5], amount=11
dp[0]=0
dp[1]=1 (1)
dp[2]=1 (2)
dp[3]=2 (2+1)
dp[4]=2 (2+2)
dp[5]=1 (5)
dp[6]=2 (5+1)
dp[7]=2 (5+2)
dp[8]=3 (5+2+1)
dp[9]=3 (5+2+2)
dp[10]=2 (5+5)
dp[11]=3 (5+5+1)
Result: 3

Path Tracking

def coin_change_with_path(coins, amount):
    dp = [float('inf')] * (amount + 1)
    parent = [-1] * (amount + 1)
    dp[0] = 0
    
    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i and dp[i - coin] + 1 < dp[i]:
                dp[i] = dp[i - coin] + 1
                parent[i] = coin
    
    if dp[amount] == float('inf'):
        return -1, []
    
    path = []
    current = amount
    while current > 0:
        path.append(parent[current])
        current -= parent[current]
    
    return dp[amount], path
count, path = coin_change_with_path([1, 2, 5], 11)
print(f"Min count: {count}, Coins: {path}")  # 3, [5, 5, 1]

4. Problem 4: Longest Increasing Subsequence (LIS)

Problem Description

Find maximum length of increasing subsequence in array.

O(n²) Solution

def lis(arr):
    """
    dp[i] = LIS length ending at index i
    """
    n = len(arr)
    dp = [1] * n
    
    for i in range(1, n):
        for j in range(i):
            if arr[j] < arr[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    
    return max(dp)
print(lis([10, 9, 2, 5, 3, 7, 101, 18]))  # 4 ([2, 3, 7, 101])

O(n log n) Solution

from bisect import bisect_left
def lis_optimized(arr):
    """
    Optimized with binary search
    """
    tails = []
    
    for num in arr:
        pos = bisect_left(tails, num)
        if pos == len(tails):
            tails.append(num)
        else:
            tails[pos] = num
    
    return len(tails)
print(lis_optimized([10, 9, 2, 5, 3, 7, 101, 18]))  # 4

How O(n log n) Works:

arr = [10, 9, 2, 5, 3, 7, 101, 18]
tails maintains smallest tail for each length
num=10: tails=[10]
num=9:  tails=[9]     (replace 10)
num=2:  tails=[2]     (replace 9)
num=5:  tails=[2,5]   (append)
num=3:  tails=[2,3]   (replace 5)
num=7:  tails=[2,3,7] (append)
num=101:tails=[2,3,7,101] (append)
num=18: tails=[2,3,7,18]  (replace 101)
Length: 4

5. Problem 5: Knapsack

Problem Description

Maximize value in knapsack with weight limit W.

Solution

def knapsack(weights, values, W):
    """
    dp[i][w] = max value considering first i items with weight w
    """
    n = len(weights)
    dp = [[0] * (W + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for w in range(1, W + 1):
            if weights[i-1] <= w:
                dp[i][w] = max(
                    dp[i-1][w],
                    dp[i-1][w - weights[i-1]] + values[i-1]
                )
            else:
                dp[i][w] = dp[i-1][w]
    
    return dp[n][W]
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
W = 7
print(knapsack(weights, values, W))  # 9 (weights 3+4, values 4+5)

DP Table Trace:

weights=[1,3,4,5], values=[1,4,5,7], W=7
     w: 0  1  2  3  4  5  6  7
i=0:    0  0  0  0  0  0  0  0
i=1(1): 0  1  1  1  1  1  1  1
i=2(3): 0  1  1  4  5  5  5  5
i=3(4): 0  1  1  4  5  6  6  9
i=4(5): 0  1  1  4  5  7  7  9
Result: dp[4][7]=9
Items: weight 3 (value 4) + weight 4 (value 5)

Space Optimization

def knapsack_optimized(weights, values, W):
    """
    Space optimization with 1D array
    """
    dp = [0] * (W + 1)
    
    for i in range(len(weights)):
        for w in range(W, weights[i] - 1, -1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    
    return dp[W]
print(knapsack_optimized(weights, values, W))  # 9

6. Problem 6: Longest Common Subsequence (LCS)

Problem Description

Find longest common subsequence of two strings.

Solution

def lcs(s1, s2):
    """
    dp[i][j] = LCS length of s1[:i] and s2[:j]
    """
    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] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    
    return dp[m][n]
print(lcs("ABCDGH", "AEDFHR"))  # 3 ("ADH")

LCS with Path Reconstruction

def lcs_with_string(s1, s2):
    """
    Return LCS string, not just length
    """
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # Build DP table
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    
    # Backtrack to find string
    result = []
    i, j = m, n
    
    while i > 0 and j > 0:
        if s1[i-1] == s2[j-1]:
            result.append(s1[i-1])
            i -= 1
            j -= 1
        elif dp[i-1][j] > dp[i][j-1]:
            i -= 1
        else:
            j -= 1
    
    return '.join(reversed(result))
print(lcs_with_string("ABCDGH", "AEDFHR"))  # "ADH"

7. Problem 7: House Robber

Problem Description

Rob houses for maximum money, but cannot rob adjacent houses.

Solution

def rob(houses):
    """
    dp[i] = max money robbing up to house i
    """
    if not houses:
        return 0
    if len(houses) == 1:
        return houses[0]
    
    dp = [0] * len(houses)
    dp[0] = houses[0]
    dp[1] = max(houses[0], houses[1])
    
    for i in range(2, len(houses)):
        dp[i] = max(dp[i-1], dp[i-2] + houses[i])
    
    return dp[-1]
# Test
print(rob([2, 7, 9, 3, 1]))  # 12 (2+9+1)

Space Optimized:

def rob_optimized(houses):
    """
    O(1) space
    """
    if not houses:
        return 0
    if len(houses) == 1:
        return houses[0]
    
    prev2 = houses[0]
    prev1 = max(houses[0], houses[1])
    
    for i in range(2, len(houses)):
        current = max(prev1, prev2 + houses[i])
        prev2, prev1 = prev1, current
    
    return prev1

Practical Tips

DP Problem Approach (5 Steps)

# 1. Define subproblems
# dp[i] = "optimal solution up to i"
# 2. Find recurrence relation
# dp[i] = f(dp[i-1], dp[i-2], ...)
# 3. Set initial values
# dp[0] = ..., dp[1] = ...
# 4. Choose implementation
# Top-Down (recursion) vs Bottom-Up (iteration)
# 5. Optimize
# Space optimization, skip unnecessary calculations

Debugging Tips

def debug_dp(dp):
    """Print DP table"""
    for i, row in enumerate(dp):
        print(f"dp[{i}]: {row}")
# Usage
dp = [[0] * 5 for _ in range(3)]
debug_dp(dp)
# Print 1D DP
def debug_dp_1d(dp):
    print("dp:", dp)
# Verify recurrence
def verify_recurrence(dp, i):
    """Check if dp[i] follows recurrence relation"""
    expected = calculate_expected(dp, i)
    actual = dp[i]
    assert expected == actual, f"dp[{i}]: expected {expected}, got {actual}"

Common Mistakes

# ❌ Wrong: Forgot initial values
dp = [0] * n
for i in range(n):
    dp[i] = dp[i-1] + dp[i-2]  # dp[0], dp[1] not set!
# ✅ Correct: Set initial values
dp = [0] * n
dp[0] = 1
dp[1] = 1
for i in range(2, n):
    dp[i] = dp[i-1] + dp[i-2]
# ❌ Wrong: Index out of bounds
for i in range(n):
    dp[i] = dp[i-1] + dp[i-2]  # i=0, i=1 error!
# ✅ Correct: Start from valid index
for i in range(2, n):
    dp[i] = dp[i-1] + dp[i-2]

Summary

Key Points

  1. Approach: Subproblems → Recurrence → Initial values → Implementation
  2. Patterns: 1D DP, 2D DP, Knapsack, LCS, LIS
  3. Optimization: Space (1D array), Time (binary search)
  4. Practice: Solve many problems

Problem Type Checklist

TypeRecurrenceExample
1D DPdp[i] = f(dp[i-1])Make One, Climbing Stairs
2D DPdp[i][j] = f(dp[i-1][j], dp[i][j-1])Edit Distance, LCS
Knapsackdp[i][w] = max(take, don't take)0-1 Knapsack
LISdp[i] = max(dp[j]+1) for j<iLIS

Next Steps


Baekjoon

LeetCode

  • LeetCode 70: Climbing Stairs
  • LeetCode 198: House Robber
  • LeetCode 322: Coin Change
  • LeetCode 300: Longest Increasing Subsequence
  • LeetCode 72: Edit Distance
  • LeetCode 1143: Longest Common Subsequence
  • LeetCode 416: Partition Equal Subset Sum

Programmers



Keywords

Dynamic Programming, DP, Problem Solving, Make One, Edit Distance, Coin Change, LIS, Knapsack, LCS, Coding Interview, Algorithm, Optimization


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Master DP practice problems: coding interview DP problem-solving strategies. Learn Make One, Edit Distance, Coin Change,… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Algorithm, DP, Problem Solving, Coding Interview, Dynamic Programming 등으로 검색하시면 이 글이 도움이 됩니다.