โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม
จำลองการตัดสินใจปล้นหรือข้ามเป็นความสัมพันธ์เวียนเกิดของ 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม
- ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด
- การแบ่งคำและการแบ่งสตริงเป็นส่วน
- ถอดรหัสวิธีและการนับเส้นทาง