DP من الأعلى إلى الأسفل مع التخزين المؤقت
أضف قاموس memo إلى حل ذاتي لتقليص الاستدعاءات المكررة، واستخدم @lru_cache للتخزين المؤقت بأقل قدر من الشيفرة
DP من الأعلى إلى الأسفل مع التخزين المؤقت درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
DP من أعلى إلى أسفل: فكرة التخزين المؤقت
تبدأ DP من أعلى إلى أسفل بالحل التكراري الأصلي، ثم تضيف التخزين المؤقت: ذاكرة تخزين مؤقت تحفظ نتيجة كل مسألة فرعية عند حسابها للمرة الأولى. عند إجراء استدعاءات لاحقة بالوسائط نفسها، تُعاد النتيجة المخزنة مؤقتاً فوراً من دون تكرار الاستدعاء. يحوّل ذلك التكرار الساذج ذي التعقيد O(2^n) إلى O(n) مع تغييرات طفيفة في الشيفرة — غالباً بإضافة سطرين أو ثلاثة فقط إلى حل تكراري موجود.
# Top-down approach:
# 1. Write the recursive solution (natural but slow)
# 2. Add a memo dict to cache results
# 3. Before recursing, check if the result is cached
# 4. Before returning, store the result in the cache
# This is also called 'memoization' (US spelling)
# 'memoize' means 'to remember', not 'memorize'
# The cache key is the function arguments
# For fib: key is n
# For 2D DP: key is (i, j)
# For 3D DP: key is (i, j, k)
print('Top-down = recursion + memo cache')فيبوناتشي مع التخزين المؤقت
تؤدي إضافة قاموس تخزين مؤقت إلى تكرار فيبوناتشي الساذج إلى تقليل الزمن من O(2^n) إلى O(n). يحسب الاستدعاء الأول لـ fib(k) النتيجة ويخزنها. وتعيد جميع الاستدعاءات اللاحقة للقيمة نفسها k القيمة المخزنة مؤقتاً فوراً. التعقيد المكاني هو O(n) لقاموس التخزين المؤقت، بالإضافة إلى O(n) لمكدس الاستدعاءات. قارن عدد الاستدعاءات: من دون تخزين مؤقت، يُجري fib(30) نحو مليوني استدعاء؛ ومع التخزين المؤقت، يجري 30 استدعاءً بالضبط.
def fib_memo(n, memo=None):
if memo is None:
memo = {}
if n in memo:
return memo[n] # return cached result
if n <= 1:
return n
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]
# Verify speed improvement:
print(fib_memo(30)) # fast!
print(fib_memo(50)) # still fast
print(fib_memo(100)) # no problem
# Without memo, fib_naive(50) would take minutes
# With memo: each of the 50 sub-problems computed onceاستخدام @functools.lru_cache
يعمل المزيّن @functools.lru_cache(maxsize=None) في Python (أو الاسم المستعار @cache في Python 3.9 والإصدارات الأحدث) على تطبيق التخزين المؤقت لدالة تلقائياً بناءً على وسائطها. هذه أنظف طريقة لإضافة DP من أعلى إلى أسفل في المقابلات — اكتب الحل التكراري، وأضف المزيّن، وبذلك تنتهي المهمة. يخزن المزيّن جميع النتائج في قاموس، ويستخدم وسائط الدالة كمفاتيح، ولذلك يجب أن تكون هذه الوسائط قابلة للتجزئة (لا تستخدم القوائم — استخدم tuples بدلاً منها).
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print(fib(50)) # 12586269025
print(fib(100)) # works instantly
# Clear cache between tests if needed:
fib.cache_clear()
# Python 3.9+ shorthand:
# from functools import cache
# @cache
# def fib(n): ...
print(fib.cache_info()) # shows hits, misses, maxsize, currsizeCoin Change من أعلى إلى أسفل
تطلب مسألة Coin Change (LeetCode #322)، عند إعطائك فئات العملات ومبلغاً مستهدفاً، إيجاد الحد الأدنى لعدد العملات اللازمة. الصياغة التكرارية هي: لكل عملة، اخترها وحل المسألة للمبلغ المتبقي، ثم خذ الحد الأدنى. استخدم التخزين المؤقت للمبلغ لتجنب إعادة الحساب. الحالة الأساسية: يحتاج amount=0 إلى 0 من العملات؛ أما المبلغ المستحيل فيعيد ما لا نهاية (أو -1 بعد انتهاء التكرار).
import functools
def coin_change_top_down(coins, amount):
@functools.lru_cache(maxsize=None)
def dp(remaining):
if remaining == 0:
return 0 # no coins needed
if remaining < 0:
return float('inf') # impossible
# Try each coin and take the minimum
return 1 + min(dp(remaining - c) for c in coins)
result = dp(amount)
return result if result != float('inf') else -1
print(coin_change_top_down([1, 5, 6, 9], 11)) # 2: (5+6) or (2*5+1?no: 9+2?no) 5+6=11 YES
print(coin_change_top_down([2], 3)) # -1: impossible
print(coin_change_top_down([1, 2, 5], 11)) # 3: 5+5+1صعود السلالم من أعلى إلى أسفل مع K خطوات
عمّم مسألة صعود السلالم للسماح بخطوات من 1 إلى k. الحالة هي الدرجة الحالية، ومن الدرجة i يمكنك الوصول إلى الدرجات i+1 وi+2 و... وi+k. العلاقة التكرارية هي: dp(i) = sum of dp(i-j) for j in 1..k if i-j >= 0. يجعل التخزين المؤقت التعقيد O(n*k) بدلاً من O(k^n). يظهر هذا التعميم في مسائل مثل «الحد الأدنى للتكلفة للوصول إلى الدرجة الأخيرة» و«عدد طرق ملء شبكة».
import functools
def climb_k_steps(n, k):
@functools.lru_cache(maxsize=None)
def dp(i):
if i == 0:
return 1 # base: one way to stay at ground
if i < 0:
return 0 # impossible
# From stair i, you could have come from i-1, i-2, ..., i-k
return sum(dp(i - j) for j in range(1, k+1) if i - j >= 0)
return dp(n)
# k=2 (original): should match fib-like sequence
print([climb_k_steps(n, 2) for n in range(7)]) # [1,1,2,3,5,8,13]
# k=3: more options
print([climb_k_steps(n, 3) for n in range(7)]) # [1,1,2,4,7,13,24]LCS من أعلى إلى أسفل: التخزين المؤقت ثنائي الأبعاد
تتطلب المتتالية الفرعية المشتركة الأطول (LCS) حالة ثنائية الأبعاد: dp(i, j) = طول LCS في s1[:i] وs2[:j]. إذا كان s1[i-1] == s2[j-1]، فإن المحرفين متطابقان: dp(i,j) = 1 + dp(i-1, j-1). وإلا: dp(i,j) = max(dp(i-1,j), dp(i,j-1)) — تخطَّ محرفاً واحداً من إحدى السلسلتين. يؤدي استخدام التخزين المؤقت على (i, j) إلى O(mn) بدلاً من O(2^(m+n)).
import functools
def lcs_top_down(s1, s2):
m, n = len(s1), len(s2)
@functools.lru_cache(maxsize=None)
def dp(i, j):
if i == 0 or j == 0:
return 0 # empty prefix has LCS of 0
if s1[i-1] == s2[j-1]:
return 1 + dp(i-1, j-1) # characters match
return max(dp(i-1, j), dp(i, j-1)) # skip one
return dp(m, n)
print(lcs_top_down('abcde', 'ace')) # 3: 'ace'
print(lcs_top_down('abc', 'abc')) # 3: 'abc'
print(lcs_top_down('abc', 'def')) # 0: no common charsقاموس التخزين المؤقت مقابل lru_cache: متى تختار؟
استخدم @lru_cache عندما تكون وسائط دالتك من الأنواع البدائية القابلة للتجزئة (int، str، tuple). استخدم قاموس تخزين مؤقت يدوياً عندما: تحتاج إلى تمرير حالة قابلة للتغيير (مثل lists وdicts) بعد تحويلها إلى tuples، أو تحتاج إلى تتبع المفاتيح التي حُسبت، أو تعمل داخل أسلوب في فئة ولا ينبغي تخزين self مؤقتاً. يكون قاموس التخزين المؤقت اليدوي أكثر وضوحاً، كما يتجنب مشكلات الإغلاق الدقيقة في الدوال المساعدة التكرارية.
# @lru_cache: clean, automatic, O(1) overhead
# Use when: arguments are simple (int, str, tuple)
import functools
@functools.lru_cache(maxsize=None)
def simple_dp(n):
if n <= 1: return n
return simple_dp(n-1) + simple_dp(n-2)
# Manual memo dict: explicit, flexible
# Use when: complex state, need to inspect memo, class methods
def manual_memo_dp(s1, s2):
memo = {}
def dp(i, j):
if (i,j) in memo: return memo[(i,j)]
if i == 0 or j == 0:
return 0
if s1[i-1] == s2[j-1]:
memo[(i,j)] = 1 + dp(i-1, j-1)
else:
memo[(i,j)] = max(dp(i-1,j), dp(i,j-1))
return memo[(i,j)]
return dp(len(s1), len(s2))
print(manual_memo_dp('abcde', 'ace')) # 3Target Sum من أعلى إلى أسفل
تطلب مسألة Target Sum (LeetCode #494) إسناد إشارة + أو - إلى كل عدد، ثم عدّ الإسنادات التي تنتج مجموعاً مستهدفاً. الحالة هي: dp(index, current_sum). عند كل فهرس، جرّب جمع العدد الحالي (+) وطرحه (-). يحوّل التخزين المؤقت على (index, current_sum) الحل بالقوة الغاشمة ذي التعقيد O(2^n) إلى O(n * sum_range). ويحدّ مجموع جميع الأعداد نطاق المجموع، مما يعطي O(n * S) من الحالات إجمالاً.
import functools
def find_target_sum_ways(nums, target):
@functools.lru_cache(maxsize=None)
def dp(index, current_sum):
if index == len(nums):
return 1 if current_sum == target else 0
# Try adding the number
add = dp(index + 1, current_sum + nums[index])
# Try subtracting the number
subtract = dp(index + 1, current_sum - nums[index])
return add + subtract
return dp(0, 0)
print(find_target_sum_ways([1,1,1,1,1], 3)) # 5
print(find_target_sum_ways([1], 1)) # 1
print(find_target_sum_ways([1], -1)) # 1DP من أعلى إلى أسفل مقابل DP من أسفل إلى أعلى: المزايا والعيوب
من أعلى إلى أسفل (التخزين المؤقت) — المزايا: سهولة الكتابة بصورة طبيعية (إذ يبدأ بالحل التكراري)، وحساب المسائل الفرعية المطلوبة فعلياً فقط (بشكل كسول)، وسهولة إضافة ذاكرة التخزين المؤقت تدريجياً. من أسفل إلى أعلى (إنشاء الجدول) — المزايا: عدم وجود تكلفة لمكدس الاستدعاءات (ولا حد لتكرار Python)، ووصول إلى الذاكرة أكثر ملاءمة لذاكرة التخزين المؤقت، وسهولة أكبر في تحسين المساحة. يمتلك النهجان التعقيد التقاربي نفسه. في المقابلات، ابدأ من أعلى إلى أسفل للتحقق من الصحة، ثم حوّله إلى أسفل إلى أعلى إذا طُلبت مساحة أفضل.
# Top-down advantages:
# + Natural: write recursive, add @cache
# + Lazy: only computes needed sub-problems
# + Easy to reason about correctness
# - Uses call stack (recursion limit in Python)
# - Higher constant factor (function call overhead)
# Bottom-up advantages:
# + No recursion limit
# + Better cache performance (sequential memory)
# + Easier to space-optimise (rolling array)
# - Must compute all sub-problems in order
# - Less intuitive for complex 2D/3D problems
# Interview strategy:
# Start with top-down to verify recurrence,
# convert to bottom-up only if asked.
print('Top-down: easy to write | Bottom-up: efficient for large n')Word Break مع DP من أعلى إلى أسفل
تسأل مسألة Word Break (LeetCode #139) عما إذا كان يمكن تقسيم السلسلة s إلى كلمات من قاموس. الحالة هي: dp(i) = ما إذا كان من الممكن تقسيم s[i:]. بدءاً من الفهرس i، جرّب جميع الكلمات: إذا كان s[i:i+len(w)] == w، فاستدعِ الحل تكرارياً على اللاحقة المتبقية. يحوّل التخزين المؤقت على فهرس البداية الحل بالقوة الغاشمة ذي التعقيد O(2^n) إلى O(n^2) (أو O(n * max_word_len)) مع فحص الانتماء إلى المجموعة.
import functools
def word_break(s, word_dict):
word_set = set(word_dict)
@functools.lru_cache(maxsize=None)
def dp(start):
if start == len(s):
return True # successfully segmented entire string
for end in range(start + 1, len(s) + 1):
if s[start:end] in word_set and dp(end):
return True
return False
return dp(0)
print(word_break('leetcode', ['leet', 'code'])) # True
print(word_break('applepenapple', ['apple', 'pen'])) # True
print(word_break('catsandog', ['cats', 'dog', 'and', 'cat', 'san', 'andog'])) # Falseحد التكرار وItertools
حد التكرار الافتراضي في Python هو 1000 (تحدده sys.getrecursionlimit()). في مسائل DP ذات المُدخلات الكبيرة (n = 10,000+)، سيصطدم التخزين المؤقت من أعلى إلى أسفل بهذا الحد. تتوفر خيارات عدة: زيادة الحد باستخدام sys.setrecursionlimit(100000)، أو التحويل إلى DP من أسفل إلى أعلى. في البرمجة التنافسية، تُعد زيادة الحد أمراً شائعاً؛ أما في شيفرة الإنتاج، فاحرص دائماً على تفضيل الحلول من أسفل إلى أعلى أو الحلول التكرارية لتحقيق الموثوقية.
import sys
print('Default recursion limit:', sys.getrecursionlimit()) # 1000
# For large DP problems, increase if needed:
# sys.setrecursionlimit(100000)
# Better: convert to bottom-up DP for large n
def fib_bottom_up(n):
if n <= 1: return n
a, b = 0, 1
for _ in range(2, n+1):
a, b = b, a + b
return b
# No recursion limit issue:
print(fib_bottom_up(10000)) # works fine, no recursionاختبار سريع
اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
تعلمت في هذا الدرس: DP من أعلى إلى أسفل باستخدام قاموس تخزين مؤقت والمزيّن @lru_cache، وحلولاً تستخدم التخزين المؤقت لمسائل فيبوناتشي، وCoin Change، وLCS، وTarget Sum، وWord Break، ومتى تختار النهج من أعلى إلى أسفل بدلاً من النهج من أسفل إلى أعلى. ننتقل بعد ذلك إلى تنفيذ DP من أسفل إلى أعلى باستخدام إنشاء الجدول وتحسين المساحة.
الأسئلة الشائعة
هل درس «DP من الأعلى إلى الأسفل مع التخزين المؤقت» مجاني؟
نعم — نص درس «DP من الأعلى إلى الأسفل مع التخزين المؤقت» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «DP من الأعلى إلى الأسفل مع التخزين المؤقت»؟
أضف قاموس memo إلى حل ذاتي لتقليص الاستدعاءات المكررة، واستخدم @lru_cache للتخزين المؤقت بأقل قدر من الشيفرة تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «DP من الأعلى إلى الأسفل مع التخزين المؤقت»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- التعرّف إلى DP: المسائل الفرعية المتداخلة
- DP من الأعلى إلى الأسفل مع التخزين المؤقت
- DP من الأسفل إلى الأعلى باستخدام الجدولة
- تغيير العملات والسلم ذي التكلفة الدنيا