0Pricing
Coding Interview Prep · 강의

House Robber: 선택 또는 건너뛰기 점화식

훔칠지 건너뛸지의 결정을 DP 점화식으로 모델링하고 공간을 두 변수로 줄인 뒤, 원형으로 배치된 집으로 해법을 확장합니다.

House Robber: 선택 또는 건너뛰기 점화식은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Coding Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

집 도둑질 문제

집 도둑질 문제는 각 집에 있는 금액을 나타내는 음이 아닌 정수 배열이 주어졌을 때, 인접한 두 집을 털지 않고 훔칠 수 있는 최대 금액을 구하는 문제입니다. 예를 들어 [2, 7, 9, 3, 1]에서는 12를 얻을 수 있습니다(0번, 2번, 4번 집을 털기). 각 단계에서 두 가지 중 하나를 선택하는 전형적인 1차원 DP 문제입니다.

nums = [2, 7, 9, 3, 1]
# Can't rob adjacent houses
# Options: rob index 0 and 2 and 4 → 2+9+1=12
# or rob index 1 and 3 → 7+3=10
print('Max profit:', 12)  # answer is 12

점화식 정의

dp[i]를 처음 i+1개의 집에서 훔칠 수 있는 최대 금액이라고 하겠습니다. 각 i번 집에서 두 가지 선택이 있습니다. 건너뛰기(dp[i-1]을 취함) 또는 털기(nums[i] + dp[i-2]를 취함)입니다. 점화식은 dp[i] = max(dp[i-1], nums[i] + dp[i-2])입니다. 이는 여러 DP 문제에서 나타나는 기본적인 선택 또는 건너뛰기 패턴입니다.

# Recurrence: dp[i] = max(dp[i-1], nums[i] + dp[i-2])
# Base cases:
# dp[0] = nums[0]  (only one house, rob it)
# dp[1] = max(nums[0], nums[1])  (take the richer of the two)
def rob(nums):
    n = len(nums)
    if n == 1: return nums[0]
    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], nums[i] + dp[i-2])
    return dp[-1]

print(rob([2, 7, 9, 3, 1]))  # 12

DP 표 따라가기

[2, 7, 9, 3, 1]에 대해 표를 따라 계산해 보겠습니다. dp[0] = 2, dp[1] = max(2, 7) = 7, dp[2] = max(7, 9+2) = 11, dp[3] = max(11, 3+7) = 11, dp[4] = max(11, 1+11) = 12입니다. 최종 정답은 dp[4] = 12입니다. 표를 직접 따라 계산하면 각 위치에서 점화식이 선택과 건너뛰기를 모두 올바르게 처리하는지 확인할 수 있습니다.

nums = [2, 7, 9, 3, 1]
dp = [0] * len(nums)
dp[0] = 2
dp[1] = max(2, 7)  # 7
for i in range(2, len(nums)):
    skip = dp[i-1]
    take = nums[i] + dp[i-2]
    dp[i] = max(skip, take)
    print(f'dp[{i}] = max({skip}, {nums[i]}+{dp[i-2]}) = {dp[i]}')
print('Answer:', dp[-1])

O(1) 공간으로 줄이기

DP 표는 항상 두 위치 전까지만 참조하므로 전체 배열을 두 변수로 바꿀 수 있습니다. prev2는 두 단계 전의 값이고 prev1은 한 단계 전의 값입니다. 각 반복이 끝나면 prev2 = prev1과 prev1 = current로 값을 이동합니다. 이렇게 하면 시간 복잡도는 O(n)으로 유지하면서 메모리를 O(n)에서 O(1)로 줄일 수 있습니다.

def rob_optimised(nums):
    if not nums: return 0
    if len(nums) == 1: return nums[0]
    prev2 = nums[0]
    prev1 = max(nums[0], nums[1])
    for i in range(2, len(nums)):
        curr = max(prev1, nums[i] + prev2)
        prev2 = prev1
        prev1 = curr
    return prev1

print(rob_optimised([2, 7, 9, 3, 1]))   # 12
print(rob_optimised([1, 2, 3, 1]))       # 4

처리해야 할 경계 사례

항상 경계 사례로 해법을 테스트하세요. 빈 배열이면 0을 반환하고, 원소 하나인 배열이면 해당 원소를 반환하며, 원소 두 개인 배열이면 둘 중 큰 값을 반환해야 합니다. 면접에서 이러한 사례를 언급하고 처리하면 철저함을 보여 줄 수 있습니다. if n == 1 조건은 dp[1]을 위해 nums[1]에 접근할 때 인덱스 범위를 벗어나는 것을 방지합니다.

def rob(nums):
    if not nums: return 0
    if len(nums) == 1: return nums[0]
    prev2 = nums[0]
    prev1 = max(nums[0], nums[1])
    for i in range(2, len(nums)):
        curr = max(prev1, nums[i] + prev2)
        prev2, prev1 = prev1, curr
    return prev1

print(rob([]))         # 0
print(rob([5]))        # 5
print(rob([3, 10]))    # 10
print(rob([10, 3]))    # 10

집 도둑질 II: 원형으로 배치된 집

원형 변형 문제(LeetCode 213)에서는 집들이 원형으로 배치되어 첫 번째 집과 마지막 집이 인접합니다. 선형 점화식을 그대로 적용할 수 없습니다. 핵심은 첫 번째 집을 털고 마지막 집을 제외하거나, 첫 번째 집을 제외하고 마지막 집을 포함하는 둘 중 하나라는 점입니다. 두 부분 배열에 선형 집 도둑질 해법을 실행하고 최댓값을 취합니다.

def rob_linear(nums):
    prev2, prev1 = 0, 0
    for n in nums:
        prev2, prev1 = prev1, max(prev1, n + prev2)
    return prev1

def rob_circular(nums):
    if len(nums) == 1: return nums[0]
    # Either include first (exclude last) or include last (exclude first)
    return max(rob_linear(nums[:-1]), rob_linear(nums[1:]))

print(rob_circular([2, 3, 2]))   # 3
print(rob_circular([1, 2, 3, 1]))  # 4

여기서 탐욕적 방법이 실패하는 이유

순진한 탐욕적 접근은 항상 남아 있는 집 중 금액이 가장 큰 집을 털려고 할 수 있습니다. 그러나 [2, 1, 1, 2]와 같은 입력에서는 실패합니다. 탐욕적 방법은 0번 집(금액 2)을 고른 다음 3번 집(금액 2)을 골라 총 4를 얻지만, 0번과 2번 집을 털면 3을 얻습니다. 잠깐, 이 경우에는 탐욕적 방법이 작동합니다! 하지만 [1, 3, 1, 3, 100]을 보세요. 탐욕적 방법은 위치 1과 3의 금액 3을 골라 6을 얻으므로, 최적인 1+1+100=102를 놓칩니다. 국소적으로 최적인 선택이 전체 최적해를 보장하지 않기 때문에 DP가 필요합니다.

# Greedy failure example
nums = [1, 3, 1, 3, 100]
# Greedy: pick max each step
# picks 3 (index 1), then 3 (index 3) → total 6
# DP optimal: pick 1 (index 0) + 1 (index 2) + 100 (index 4) → 102

def rob(nums):
    prev2, prev1 = 0, 0
    for n in nums:
        prev2, prev1 = prev1, max(prev1, n + prev2)
    return prev1

print(rob(nums))  # 102

선택 또는 건너뛰기 패턴 알아보기

선택 또는 건너뛰기 패턴은 집 도둑질을 넘어 일반화됩니다. 배열을 순회하면서 각 위치에서 현재 원소를 포함하고(이전 원소는 건너뜀) 또는 현재 원소를 제외하고(이전 결과를 유지함) 둘 중 하나를 선택한다면 선택 또는 건너뛰기 DP를 사용하게 됩니다. 인접한 두 원소를 선택할 수 없음 또는 겹치는 구간이 없음과 같은 제약을 보면 이 패턴을 적용할 신호로 생각하세요.

# General take-or-skip template
def take_or_skip(values, gap=1):
    '''Max sum where selected elements must be at least gap+1 apart.'''
    n = len(values)
    if n == 0: return 0
    # dp[i] = best up to index i
    dp = [0] * (n + gap)
    for i in range(n):
        take = values[i] + (dp[i - 1] if i >= 1 else 0)
        skip = dp[i + gap - 1] if i + gap - 1 < len(dp) else 0
        dp[i + gap] = max(skip, take)
    return dp[-1]

print(take_or_skip([2, 7, 9, 3, 1]))  # house robber-like

삭제하고 얻기 변형

삭제하고 얻기(LeetCode 740)는 선택한 각 숫자에 대해 num × count(num)만큼 얻지만, num-1과 num+1의 모든 출현을 삭제해야 하는 문제입니다. 이는 집 도둑질 문제로 바로 바꿀 수 있습니다. 모든 값에 대해 earn[v] = v × count(v)인 배열을 만든 다음 이 배열에 집 도둑질 해법을 실행합니다. 문제를 다른 문제로 환원하는 능력은 면접에서 중요한 기술입니다.

from collections import Counter

def delete_and_earn(nums):
    if not nums: return 0
    count = Counter(nums)
    max_val = max(nums)
    # earn[v] = total points from taking all v's
    earn = [v * count[v] for v in range(max_val + 1)]
    # Now run house robber on earn
    prev2, prev1 = 0, 0
    for e in earn:
        prev2, prev1 = prev1, max(prev1, e + prev2)
    return prev1

print(delete_and_earn([3, 4, 2]))    # 6 (take 3+3=no, take 4+2=6)
print(delete_and_earn([2, 2, 3, 3, 3, 4]))  # 9 (take all 3s)

집 도둑질 III: 이진 트리

집 도둑질 III에서는 집들이 이진 트리로 배치되어 있습니다. 노드와 그 직접 부모 노드를 동시에 털 수 없습니다. 두 값을 반환하는 보조 함수를 정의합니다. rob(node) → (rob_root, skip_root)입니다. 루트를 털면 두 자식의 건너뛰기 값을 더합니다. 루트를 건너뛰면 각 자식에서 얻을 수 있는 최선의 값을 더합니다. 이는 각 노드에서 선택 또는 건너뛰기를 결정하는 후위 순회 DFS입니다.

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def rob_tree(root):
    def dfs(node):
        if not node: return (0, 0)  # (rob, skip)
        l_rob, l_skip = dfs(node.left)
        r_rob, r_skip = dfs(node.right)
        rob = node.val + l_skip + r_skip
        skip = max(l_rob, l_skip) + max(r_rob, r_skip)
        return (rob, skip)
    return max(dfs(root))

# Tree: 3 -> 2,3 -> None,3,None,1
root = TreeNode(3, TreeNode(2, None, TreeNode(3)), TreeNode(3, None, TreeNode(1)))
print(rob_tree(root))  # 7

복잡도와 면접에서의 논의

선형 집 도둑질은 두 변수 최적화를 사용하면 O(n) 시간과 O(1) 공간에 실행됩니다. 원형 변형도 선형 버전을 두 번 호출하므로 O(n) 시간에 실행됩니다. 트리 변형은 트리의 높이를 h라고 할 때 O(n) 시간과 O(h) 공간에 실행됩니다. 면접에서는 구현 후 항상 복잡도를 말하고 공간 최적화도 언급하세요. 이는 처음 작동하는 해법을 넘어 생각한다는 것을 보여 줍니다.

# Summary of complexities
# Linear House Robber:
#   Time: O(n), Space: O(1) with two-variable trick
# Circular House Robber:
#   Time: O(n), Space: O(1) (two passes)
# Tree House Robber:
#   Time: O(n), Space: O(h) call stack

# Quick benchmark
import time
import random
nums = [random.randint(0, 100) for _ in range(10**6)]
start = time.time()
prev2 = prev1 = 0
for n in nums:
    prev2, prev1 = prev1, max(prev1, n + prev2)
print(f'1M elements in {time.time()-start:.3f}s, result={prev1}')

빠른 확인

이 단원에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인해 보세요.

단원 요약

이 단원에서는 가져가거나 건너뛰는 점화식 dp[i] = max(dp[i-1], nums[i] + dp[i-2]), 두 개의 순환 변수를 사용해 O(n) 공간을 O(1)로 줄이는 방법, 이 패턴을 원형 배열과 이진 트리로 확장하는 방법을 배웠습니다. 다음으로 카데인 알고리즘을 사용한 최대 부분 배열 및 최대 곱 부분 배열 문제를 살펴봅니다.

자주 묻는 질문

“House Robber: 선택 또는 건너뛰기 점화식” 강의는 무료인가요?

네 — “House Robber: 선택 또는 건너뛰기 점화식” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“House Robber: 선택 또는 건너뛰기 점화식”에서 뭘 배우나요?

훔칠지 건너뛸지의 결정을 DP 점화식으로 모델링하고 공간을 두 변수로 줄인 뒤, 원형으로 배치된 집으로 해법을 확장합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“House Robber: 선택 또는 건너뛰기 점화식” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. House Robber: 선택 또는 건너뛰기 점화식
  2. 최대 부분 배열과 최대 곱 부분 배열
  3. 단어 분할과 문자열 분할
  4. 경우의 수 디코딩과 경로 세기
← Coding Interview Prep(으)로 돌아가기