라이브 코딩 면접에서 동적 계획법(이하 DP)이 나왔을 때 떨어지는 사람과 통과하는 사람의 차이는 알고리즘 지식이 아니라 접근 방식의 명료함에 있다. DP는 BFS/DFS처럼 템플릿이 떨어지지 않고, 매 문제마다 “상태를 어떻게 정의할 것인가”를 새로 정해야 한다. 그래서 면접관 앞에서 가장 많이 무너지는 유형이기도 하다. 특히 HackerRank 스타일의...
라이브 코딩 면접에서 동적 계획법(이하 DP)이 나왔을 때 떨어지는 사람과 통과하는 사람의 차이는 알고리즘 지식이 아니라 접근 방식의 명료함에 있다. DP는 BFS/DFS처럼 템플릿이 떨어지지 않고, 매 문제마다 “상태를 어떻게 정의할 것인가”를 새로 정해야 한다. 그래서 면접관 앞에서 가장 많이 무너지는 유형이기도 하다.
특히 HackerRank 스타일의 라이브 코딩에서는 다음과 같은 흐름이 거의 정형화되어 있다.
이 흐름에서 “DP인 것 같다”까지는 누구나 가지만, 상태 정의를 말로 또렷하게 설명하지 못해서 거기서부터 발이 꼬이는 경우가 많다. 이 문서는 그 구간을 메우는 데 초점을 맞춘다.
DP를 한 문장으로 정의하면 이렇다.
작은 부분 문제의 답을 한 번만 계산해서 저장해두고, 큰 문제의 답을 그 부분 문제들의 답으로 조합해서 만드는 기법.
이 정의에서 핵심 두 가지가 등장한다.
라이브 코딩에서 면접관에게 “이 문제가 DP라고 판단한 근거가 뭔가요?”라는 질문을 받으면, 위 두 조건을 자기 문제의 언어로 풀어 설명할 수 있어야 한다. 예를 들어 계단 오르기 문제라면 이렇게 말한다.
n번째 계단에 도달하는 경우의 수는 (n-1)번째에서 한 칸, (n-2)번째에서 두 칸 올라온 경우의 합으로 만들어집니다(최적 부분 구조). 그리고 재귀로 풀면 같은 f(k)가 여러 번 호출되니까 메모이제이션 이득이 있어서 DP로 풉니다(중복 부분 문제).
이 두 문장만 면접관 앞에서 또렷하게 말할 수 있어도 시작이 절반 이상 끝난다.
점화식을 “감”으로 세우려고 하면 무너진다. 항상 다음 5단계로 진행한다.
문제를 풀기 시작하기 전에 답이 무엇인지 한 줄로 적는다.
문제: 정수 배열 nums가 주어질 때, 인접하지 않은 원소만 골라 만들 수 있는 최대 합을 구하라.
답: 인접하지 않은 원소의 부분집합 중 합이 최대인 값답을 적지 않은 채로 점화식을 만지면, 자기가 지금 무엇을 구하고 있는지 잊어버린다. 이건 5분만 헤매도 면접관이 인지한다.
상태는 “DP 테이블의 한 칸이 의미하는 것”이다. 보통 이렇게 적는다.
dp[i] = nums[0..i]까지만 봤을 때, 인접하지 않은 원소를 골라 만들 수 있는 최대 합상태 정의는 반드시 다음 세 가지를 포함해야 한다.
상태 정의가 모호하면 점화식도 모호해진다. 거꾸로 말하면, 점화식이 잘 안 떠오를 때는 상태 정의가 잘못된 경우가 90%다.
상태 i를 작은 상태들로부터 만드는 식을 적는다.
dp[i] = max(dp[i-1], dp[i-2] + nums[i])이 식의 의미를 면접관에게 말로 풀 수 있어야 한다.
인덱스 i를 안 쓰면 답은 dp[i-1] 그대로고, 인덱스 i를 쓰면 i-1은 못 쓰니까 dp[i-2] + nums[i]가 됩니다. 둘 중 큰 쪽이 dp[i]가 됩니다.
가장 자주 실수하는 구간이다. 보통 0번째, 1번째 인덱스를 직접 손으로 계산해 적어둔다.
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])초기값을 0으로 그냥 두는 실수가 많다. 점화식이 max를 쓰는 경우 0이 정답이 되어버려 모든 답이 0으로 망가질 수 있다.
정답: dp[n-1]이 단계를 빠뜨리면 “계산은 다 했는데 답을 어디서 꺼내지?” 하면서 마지막 줄에서 멈춘다.
| 구분 | Top-down (재귀 + 메모이제이션) | Bottom-up (반복 + 테이블) |
|---|---|---|
| 사고 흐름 | 문제 정의 그대로 | 작은 것부터 쌓아 올림 |
| 코드 길이 | 보통 짧음 | 보통 약간 더 김 |
| 스택 위험 | 입력 크면 StackOverflow | 안전 |
| 디버깅 | 호출 그래프 따라가기 어려움 | 표로 출력해서 보기 쉬움 |
| 부분 계산 | 필요한 부분만 계산 | 전부 계산 |
라이브 코딩에서의 권장은 Bottom-up 우선이다. 이유는 세 가지다.
다만 트리/그래프 위 DP, 또는 “모든 부분 문제가 다 필요한 게 아닐 때”는 Top-down이 더 자연스럽다. 면접관에게는 이렇게 말하면 좋다.
기본은 bottom-up으로 가겠습니다. 다만 상태 공간이 큰데 실제 방문하는 상태가 적다면 top-down + 메모이제이션이 더 효율적이라 그 경우엔 바꾸겠습니다.
배낭(knapsack)류 문제에서 “지금 원소를 골랐는지 여부”가 다음 결정에 영향을 주는데, 상태에 그게 없으면 점화식이 깨진다. House Robber 같은 문제를 처음 풀 때 흔히 겪는다. 해결책은 보통 두 가지다.
dp[i][0](안 고름), dp[i][1](고름)으로 분리상태에 차원을 자꾸 늘리다 보면 O(n^3)이 쉽게 나오고, 그러다 메모리 한계도 넘는다. 라이브 코딩에서 “상태가 4차원이 되었어요”라고 말하는 순간 면접관은 다른 정의를 시도해 보라는 신호로 받아들인다. 줄이는 방법은:
“최솟값 DP”에서 가장 흔하다. 초기값을 0으로 두면 점화식의 min이 항상 0이 되어 답이 무너진다. 이 경우엔 Integer.MAX_VALUE로 초기화하고, 더하기 연산에서 오버플로 안 나도록 조건 분기를 둬야 한다.
if (dp[i - coin] != Integer.MAX_VALUE) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}Top-down으로 풀 때 다음과 같은 코드를 쓰는 사람이 매우 많다.
int solve(int i) {
if (i == 0) return nums[0];
if (i == 1) return Math.max(nums[0], nums[1]);
return Math.max(solve(i - 1), solve(i - 2) + nums[i]);
}이 코드는 DP가 아니다. 단순한 재귀이고, 시간 복잡도는 O(2^n)이다. 메모이제이션을 빼먹은 순간 “저는 DP를 풀었습니다”라고 말할 수 없다. 올바른 형태는 다음과 같다.
Integer[] memo;
int solve(int i) {
if (i == 0) return nums[0];
if (i == 1) return Math.max(nums[0], nums[1]);
if (memo[i] != null) return memo[i];
return memo[i] = Math.max(solve(i - 1), solve(i - 2) + nums[i]);
}라이브에서는 메모를 적는 줄을 의도적으로 강조해서 말한다.
여기서 memo에 저장하지 않으면 같은 i가 지수적으로 호출되니, 반드시 저장 먼저 합니다.
라이브 코딩 도중 자주 터지는 버그를 미리 알면 손이 멈추지 않는다.
dp 배열 길이를 n으로 잡았는데 dp[n]을 참조 → ArrayIndexOutOfBoundsException. 보통 길이를 n+1로 잡고 1-indexed로 푸는 게 안전하다.n == 0, n == 1을 처리 안 해 NPE 또는 음수 인덱스 접근.dp[i] = dp[i] + dp[i - coin] 순서가 잘못되면 같은 동전을 한 번만 쓰는 0/1 배낭이 되어 버린다. 1차원으로 0/1 배낭을 풀 때는 거꾸로 순회해야 한다는 것도 같은 맥락이다.long이 필요할 때가 많다. int로 받았다가 음수로 뒤집힘.dp[n-1]이 정답인데 dp[n]을 반환하거나 그 반대.Math.max / Math.min 인자 순서 의식 부족: 한 변수에 자기 자신을 비교 안 하고 덮어써 이전 후보가 사라짐.이 목록을 머리 속에 두고 코드를 짜면, 화면 위에서 면접관이 “여기 인덱스 한번 보세요”라고 했을 때 즉답이 가능하다.
말하는 순서를 외워두면, 머리가 하얘져도 입이 먼저 움직인다.
이 흐름은 “문제 → 코드”로 점프하지 않게 막아주는 안전망이다. 면접관도 이 순서를 들으면 채점 항목을 하나씩 체크해 나가게 된다.
라이브 코딩에 대비하려면 IDE의 자동완성에 의존하지 않는 환경에서 한 번씩 풀어보는 게 좋다. HackerRank 면접은 보통 웹 기반 단일 파일 환경이다.
준비물:
Main.java 파일에 푸는 습관Scanner로 입력 받는 패턴 1개 머리에 저장System.out.println 출력만으로 디버깅하는 습관기본 골격:
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) nums[i] = sc.nextInt();
System.out.println(solve(nums));
}
static int solve(int[] nums) {
// 여기에 DP
return 0;
}
}연습할 때는 다음 규칙을 지킨다.
다음 두 문제를 풀이는 가린 채 직접 풀어본 뒤 details 블록을 펼친다. 펼치기 전에 반드시 점화식부터 종이/주석에 적어 두는 것이 학습 효과가 크다.
한 번에 1칸 또는 2칸 오를 수 있을 때, n칸짜리 계단의 꼭대기까지 올라가는 서로 다른 방법의 수를 구하라.
먼저 직접 점화식을 적어볼 것.
상태 정의: dp[i] = i칸짜리 계단을 오르는 방법의 수
마지막 한 발자국이 1칸이었거나 2칸이었거나, 두 경우다. 따라서
dp[i] = dp[i-1] + dp[i-2]피보나치와 같은 점화식이지만, 의미를 “마지막 한 걸음을 어디서 디뎠는가”로 설명할 수 있어야 면접관 앞에서 자신 있게 말할 수 있다.
초기값:
dp[1] = 1
dp[2] = 2dp[0]은 정의가 모호하므로 굳이 만들지 말고 1, 2부터 베이스로 깔아둔다. 만약 dp[0] = 1로 정의한다면 “0칸 오르는 방법은 ‘아무것도 안 한다’ 1가지” 라는 해석을 명시해야 한다.
복잡도: O(N) 시간, O(N) 공간 → 롤링 변수로 O(1)까지 가능.
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
System.out.println(climb(n));
}
static int climb(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
int prev2 = 1; // dp[1]
int prev1 = 2; // dp[2]
int curr = 0;
for (int i = 3; i <= n; i++) {
curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
}라이브에서 말할 포인트:
서로 다른 액면가의 동전 배열 coins와 정수 amount가 주어질 때, amount를 만들기 위한 동전 최소 개수를 구하라. 만들 수 없으면 -1을 반환한다. 같은 동전을 여러 번 사용할 수 있다.
coins[] (1 ≤ 길이 ≤ 12, 1 ≤ 액면 ≤ 2^31-1), amount (0 ≤ amount ≤ 10^4)먼저 직접 풀어볼 것. 그리디로 풀면 안 된다는 것을 먼저 반례로 보여주는 것이 중요하다. (예: coins = [1, 3, 4], amount = 6 → 그리디는 4+1+1=3개, 최적은 3+3=2개)
상태 정의: dp[v] = 액수 v를 만들기 위해 필요한 동전의 최소 개수
점화식: 마지막에 어떤 동전 c를 썼느냐로 분기한다.
dp[v] = min over all c in coins where v - c >= 0 of (dp[v - c] + 1)초기값: dp[0] = 0 (액수 0을 만드는 데는 0개 필요). 나머지는 Integer.MAX_VALUE로 초기화.
정답 위치: dp[amount]. 만약 그 값이 여전히 MAX_VALUE라면 -1.
복잡도: O(amount * coins.length) 시간, O(amount) 공간.
흔한 버그 두 가지를 미리 막는다.
dp[v - c] + 1을 계산하기 전에 dp[v - c] != Integer.MAX_VALUE 체크. 안 그러면 오버플로로 음수가 되어 min이 더 작은 값을 골라버린다.MAX_VALUE를 그대로 반환하지 말고 -1로 바꿔서 반환.import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] coins = new int[n];
for (int i = 0; i < n; i++) coins[i] = sc.nextInt();
int amount = sc.nextInt();
System.out.println(coinChange(coins, amount));
}
static int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int v = 1; v <= amount; v++) {
for (int c : coins) {
if (c <= v && dp[v - c] != Integer.MAX_VALUE) {
dp[v] = Math.min(dp[v], dp[v - c] + 1);
}
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}라이브에서 말할 포인트:
Integer.MAX_VALUE 가드 → 오버플로 방지 이유까지 설명한다.for v 안쪽에 for coin을 두는 순서를 선택했음을 짚는다. (만약 “조합의 수”를 셀 때는 for coin 바깥, for v 안쪽으로 순서가 바뀐다는 점도 함께 말하면 가산점.)DP 자체는 알고리즘 영역이지만, 시니어 백엔드 후보로서 답변할 때는 다음 관점을 한 줄씩 곁들이면 단순 코딩 테스트 통과자가 아니라 “설계 감각이 있는 사람”으로 보인다.
면접관이 “DP는 실무에서 자주 쓰나요?”라고 물으면 이렇게 답하면 자연스럽다.
매일 짠다기보다는, 결과 재사용 + 부분 문제 분해라는 사고 자체가 캐시 설계, 배치 집계, 비용 산정 같은 데서 거의 같은 모양으로 등장합니다. 그래서 DP를 익혀두는 것은 알고리즘 자체보다도 “재사용 가능한 부분 답을 어떻게 설계할 것인가”라는 백엔드 감각을 다지는 것에 가깝다고 생각합니다.
라이브가 끝난 뒤 받기 쉬운 후속 질문들을 미리 준비해 둔다.
면접 직전 1주일 동안 매일 30분만 다음 항목을 점검하면, DP 라이브에서 무너지지 않는다.
이 체크리스트를 완성한 상태에서 라이브에 들어가면, 알고리즘 실력이 아니라 사고의 명료함으로 통과하는 사람이 된다. DP 라이브 코딩의 합격 기준은 거의 항상 거기에 있다.