0Pricing
DSA Interview Prep · บทเรียน

โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม

จำลองการตัดสินใจปล้นหรือข้ามเป็นความสัมพันธ์เวียนเกิดของ DP ลดพื้นที่ให้เหลือตัวแปรสองตัว และขยายคำตอบไปยังบ้านที่จัดเป็นวงกลม

โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

ปัญหาโจรปล้นบ้าน

ปัญหา โจรปล้นบ้าน ถามว่า เมื่อกำหนดอาร์เรย์ของจำนวนเต็มที่ไม่ติดลบ ซึ่งแทนจำนวนเงินในบ้านแต่ละหลัง จงหาจำนวนเงินสูงสุดที่สามารถปล้นได้โดยไม่ปล้นบ้านที่อยู่ติดกันสองหลัง ตัวอย่างเช่น [2, 7, 9, 3, 1] ให้คำตอบเป็น 12 โดยปล้นบ้าน 0, 2 และ 4 นี่เป็นปัญหา 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(1) โดยยังคงมีความซับซ้อนด้านเวลาเป็น O(n)

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 ช่วยป้องกันการเข้าถึงดัชนีเกินขอบเขตเมื่อต้องเข้าถึง nums[1] สำหรับ dp[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]: วิธีเลือกแบบโลภเลือกค่า 3 และ 3 (ดัชนี 1 และ 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) เช่นกัน เนื่องจากเรียกใช้วิธีแบบเส้นตรงสองครั้ง รูปแบบต้นไม้ใช้เวลา O(n) และใช้พื้นที่ O(h) โดย 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) ด้วยตัวแปรเลื่อนสองตัว และ การขยายรูปแบบไปยังอาร์เรย์วงกลมและต้นไม้ทวิภาค ต่อไปเราจะศึกษาโจทย์อาร์เรย์ย่อยผลรวมสูงสุดและอาร์เรย์ย่อยผลคูณสูงสุดโดยใช้อัลกอริทึมของ Kadane

คำถามที่พบบ่อย

บทเรียน “โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม”

จำลองการตัดสินใจปล้นหรือข้ามเป็นความสัมพันธ์เวียนเกิดของ DP ลดพื้นที่ให้เหลือตัวแปรสองตัว และขยายคำตอบไปยังบ้านที่จัดเป็นวงกลม คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน

บทเรียน “โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม

ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม
  2. ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด
  3. การแบ่งคำและการแบ่งสตริงเป็นส่วน
  4. ถอดรหัสวิธีและการนับเส้นทาง
← กลับไปที่ DSA Interview Prep