0Pricing
DSA Interview Prep · درس

لص المنازل: علاقة تكرار الأخذ أو التخطي

صِغ قرار السرقة أو التخطي كعلاقة تكرار في DP، وقلّل المساحة إلى متغيرين، ومدّد الحل ليشمل المنازل الدائرية

لص المنازل: علاقة تكرار الأخذ أو التخطي درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

مشكلة سارق المنازل

تطرح مشكلة سارق المنازل السؤال التالي: given an array of non-negative integers representing the amount of money in each house, find the maximum amount you can rob without robbing two adjacent houses. على سبيل المثال، تعطي [2, 7, 9, 3, 1] القيمة 12، وذلك بسرقة المنازل 0 و2 و4. هذه مسألة كلاسيكية من مسائل البرمجة الديناميكية أحادية الأبعاد، حيث تتخذون قرارًا ثنائيًا في كل خطوة.

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]). هذا هو نمط الأخذ أو التخطي الأساسي، والذي يظهر في كثير من مسائل البرمجة الديناميكية.

# 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

تتبّع جدول البرمجة الديناميكية

بالنسبة إلى [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)

لا ينظر جدول البرمجة الديناميكية إلا إلى موضعين سابقين، لذا يمكننا استبدال المصفوفة بأكملها بمتغيرين: 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. نحتاج إلى البرمجة الديناميكية لأن الاختيارات المثلى محليًا لا تضمن الحل الأمثل عالميًا.

# 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

التعرّف على نمط الأخذ أو التخطي

يتجاوز نمط الأخذ أو التخطي مسألة سارق المنازل. ففي كل مرة تفحصون فيها مصفوفة وتختارون في كل موضع بين إدراج العنصر الحالي، مع تخطي السابق، أو استبعاده، مع الاحتفاظ بالنتيجة السابقة، فإنكم أمام برمجة ديناميكية من نمط الأخذ أو التخطي. ابحثوا عن قيود مثل عدم وجود عنصرين متجاورين أو عدم تداخل الفواصل، فهذه مؤشرات على تطبيق هذا النمط.

# 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

صيغة الحذف والكسب

تطرح مسألة Delete and Earn (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: شجرة ثنائية

في مسألة House Robber 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}')

اختبار سريع

اختبروا فهمكم للمفاهيم الواردة في هذا الدرس من Data Structures & Algorithms — Coding Interview Prep.

مراجعة الدرس

في هذا الدرس تعلمتم: علاقة العودية للاختيار أو التخطي dp[i] = max(dp[i-1], nums[i] + dp[i-2])، تقليل المساحة O(n) إلى O(1) باستخدام متغيرين متناوبين، وتعميم النمط على المصفوفات الدائرية والأشجار الثنائية. بعد ذلك سنستكشف مسألتي المصفوفة الفرعية ذات المجموع الأقصى والمصفوفة الفرعية ذات حاصل الضرب الأقصى باستخدام خوارزمية Kadane.

الأسئلة الشائعة

هل درس «لص المنازل: علاقة تكرار الأخذ أو التخطي» مجاني؟

نعم — نص درس «لص المنازل: علاقة تكرار الأخذ أو التخطي» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «لص المنازل: علاقة تكرار الأخذ أو التخطي»؟

صِغ قرار السرقة أو التخطي كعلاقة تكرار في DP، وقلّل المساحة إلى متغيرين، ومدّد الحل ليشمل المنازل الدائرية تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟

لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.

كم من الوقت يستغرق درس «لص المنازل: علاقة تكرار الأخذ أو التخطي»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟

نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. لص المنازل: علاقة تكرار الأخذ أو التخطي
  2. المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى
  3. تقسيم الكلمات وتقسيم السلسلة
  4. فك الترميز واحتساب المسارات
← العودة إلى DSA Interview Prep