الخوارزميات الجشعة مقابل البرمجة الديناميكية: متى تستخدم كلًّا منهما
حدّد سمات المسائل التي يمكن حلّها بخوارزمية جشعة، وتلك التي تتطلب البرمجة الديناميكية، باستخدام خاصية الاختيار الجشع وحجة الاستبدال.
الخوارزميات الجشعة مقابل البرمجة الديناميكية: متى تستخدم كلًّا منهما درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
نظرة عامة على الجشع والبرمجة الديناميكية
يحل كل من الجشع والبرمجة الديناميكية مسائل التحسين، مثل إيجاد قيمة عظمى أو صغرى أو ترتيب أمثل. يتخذ الأسلوب الجشع الخيار الأمثل محليًا في كل خطوة من دون إعادة النظر في القرارات السابقة. أما DP فيستكشف جميع الاحتمالات، لكنه يستخدم التخزين المؤقت لتجنب إعادة الحساب. ويمكن أن يوفر معرفة الأسلوب المناسب ساعات من تصحيح أخطاء خوارزمية جشعة غير صحيحة أو جدول DP معقد بلا داعٍ.
# Greedy: always take the locally best option
# Example: coin change with coins [1, 5, 10, 25]
# Greedy: take as many 25s as possible, then 10s, etc.
# This works for standard denominations but NOT all coin sets!
# DP: explore all possibilities via memoisation
# Example: coin change with coins [1, 3, 4] and target 6
# Greedy would pick 4, then 1, 1 → 3 coins
# DP finds: 3 + 3 → 2 coins (optimal!)
print('Greedy can fail when local optimum != global optimum')خاصية الاختيار الجشع
تتمتع المسألة بخاصية الاختيار الجشع عندما يمكن دائمًا إنشاء حل أمثل عالميًا من خلال اتخاذ خيارات محلية مثلى، أي جشعة. وبصورة رسمية، يوجد حل أمثل يبدأ بالاختيار الجشع، ولذلك لا نحتاج إلى التراجع. ويُثبت ذلك عادةً باستخدام حجة الاستبدال: افترض أن حلًا أمثلًا لا يتضمن الاختيار الجشع، ثم أثبت أنه يمكنك استبدال أحد اختياراته به من دون جعل النتيجة أسوأ.
# Exchange argument example: Activity Selection
# Greedy: always pick the activity that ends earliest
# Proof: suppose optimal solution starts with activity A (not earliest-ending)
# Let G be the earliest-ending activity.
# Replace A with G in the solution:
# - G ends no later than A, so G does not conflict with any activity A allowed
# - The solution remains valid with at least as many activities
# Therefore greedy choice (earliest end) is always safe.
activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14)]
activities.sort(key=lambda x: x[1]) # sort by end time
print('Sorted by end:', activities[:4], '...')البنية المثلى
يتطلب كل من الجشع وDP بنية مثلى: إذ يحتوي الحل الأمثل للمسألة الكاملة على حلول مثلى لمسائلها الفرعية. والفرق هو ما إذا كان يمكن تحديد حلول المسائل الفرعية المثلى بطريقة جشعة، من دون استكشاف جميع الخيارات، أم يتطلب الأمر مقارنة خيارات متعددة. إذا اتخذت خيارًا وكانت المسألة الفرعية المتبقية مماثلة في بنيتها، فقد ينجح الأسلوب الجشع. أما إذا وجب عليك مقارنة عدة خيارات، فاستخدم DP.
# Greedy works: activity selection
# Making the greedy choice (earliest-ending) leaves a sub-problem
# that is structurally identical (activity selection on remaining activities)
# and the greedy choice for the sub-problem is still valid.
# DP needed: 0/1 knapsack
# After choosing to include/exclude item i, the remaining sub-problem
# depends on WHICH item we chose — different choices yield different sub-problems.
# No single greedy rule works for all inputs.
print('Greedy: sub-problem is unique after each choice')
print('DP: sub-problem depends on which choice was made')تداخل المسائل الفرعية: مؤشر على DP
إذا حُلّت المسألة الفرعية نفسها عدة مرات أثناء التفكيك التكراري، فأنت تحتاج إلى DP مع التخزين المؤقت. ارسم شجرة الاستدعاء التكراري وابحث عن العقد المتكررة. فيبوناتشي، تُحسب fib(3) مرتين في شجرة fib(5). وفي مسألة تغيير العملات باستخدام العملات [1,3,4] والهدف 6، تظهر المسائل الفرعية للأهداف 3 و2 و1 عدة مرات. تداخل المسائل الفرعية مع البنية المثلى = DP.
# Recursion tree for coin change [1,3,4], target=6
# bt(6) → bt(5) → bt(4) → bt(3) (repeated!)
# → bt(2) → bt(1) (repeated!)
# → bt(3) (repeated!)
# → bt(2) (repeated!)
# Without memoisation: exponential time
# With DP table: O(target * len(coins)) time
def coin_change_dp(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change_dp([1, 3, 4], 6)) # 2 (3+3)
print(coin_change_dp([2], 3)) # -1 (impossible)مسائل جشعة كلاسيكية
من المسائل التي ثبتت صحة الأسلوب الجشع فيها: (1) جدولة الأنشطة/الفواصل — اختيار الفاصل ذي وقت الانتهاء الأسبق بطريقة جشعة. (2) الشجرة الممتدة الدنيا — خوارزميتا Prim وKruskal. (3) ترميز Huffman — دمج العقدتين الأقل تكرارًا دائمًا. (4) حقيبة الظهر الكسرية — اختيار العناصر وفق أعلى نسبة قيمة إلى وزن. (5) لعبة القفز — تتبع أكبر فهرس يمكن الوصول إليه. وتملك جميع هذه المسائل تبريرات تعتمد على الإثبات بحجة الاستبدال.
# Fractional Knapsack: greedy works
def fractional_knapsack(items, capacity):
# Sort by value/weight ratio descending
items.sort(key=lambda x: x[1]/x[0], reverse=True)
total = 0
for weight, value in items:
if capacity <= 0: break
take = min(weight, capacity)
total += take * (value / weight)
capacity -= take
return total
items = [(10, 60), (20, 100), (30, 120)] # (weight, value)
print(fractional_knapsack(items, 50)) # 240.0
# 0/1 Knapsack: greedy FAILS
# Must use DP (can't take fractions)متى يفشل الأسلوب الجشع: أمثلة مضادة
يُعد العثور على مثال مضاد أسرع طريقة لدحض فرضية جشعة. ففي مسألة تغيير العملات باستخدام العملات [1, 3, 4] والهدف 6، يختار الأسلوب الجشع، بدءًا من الأكبر، 4 ثم 1+1، أي 3 عملات. أما DP فيجد 3+3، أي عملتين. وفي مسألة حقيبة الظهر 0/1، يختار الأسلوب الجشع بحسب النسبة العنصر ذي أفضل نسبة، لكنه قد يفوّت مجموعات تملأ السعة بصورة أفضل. إذا استطعت إنشاء مثال مضاد في أقل من دقيقة، فانتقل إلى DP.
# Counterexample: coin change with non-standard coins
def greedy_coins(coins, amount):
coins.sort(reverse=True)
count = 0
for c in coins:
while amount >= c:
amount -= c
count += 1
return count if amount == 0 else -1
def dp_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a-c] + 1)
return dp[amount] if dp[amount] < float('inf') else -1
coins, target = [1, 3, 4], 6
print('Greedy:', greedy_coins(coins[:], target)) # 3 (4+1+1)
print('DP: ', dp_coins(coins, target)) # 2 (3+3)جدول مقارنة: الجشع مقابل DP
الفروق الأساسية جنبًا إلى جنب: التعقيد الزمني — يكون عادةً O(n log n) في الأسلوب الجشع، بسبب هيمنة الفرز، بينما يكون O(n × عدد الحالات) في DP. التعقيد المكاني — يستخدم الأسلوب الجشع مساحة إضافية قدرها O(1)، بينما يستخدم DP مساحة قدرها O(عدد الحالات). الصحة — يحتاج الأسلوب الجشع إلى إثبات، بينما يكون DP صحيحًا دائمًا إذا كانت الحالات والعلاقة التكرارية صحيحتين. قابلية التطبيق — يُستخدم الجشع للجدولة والأشجار الممتدة وترميز Huffman، بينما يُستخدم DP لحقيبة الظهر ومحاذاة السلاسل وأقصر مسار ذي أوزان سالبة.
# Performance comparison
import time
def time_it(func, *args):
start = time.time()
result = func(*args)
return result, time.time() - start
# Large coin change test
coins = [1, 5, 10, 25, 100]
amount = 10000
def dp_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a-c]+1)
return dp[amount]
result, elapsed = time_it(dp_coins, coins, amount)
print(f'DP coin change(amount={amount}): {result} coins in {elapsed:.4f}s')إطار اتخاذ القرار
مخطط اتخاذ القرار في المقابلات: (1) هل يمكنك إثبات خاصية الاختيار الجشع باستخدام حجة الاستبدال؟ إذا كانت الإجابة نعم → استخدم الجشع. (2) هل تتداخل المسائل الفرعية، أي هل تُوصل الحالة نفسها بطرق متعددة؟ إذا كانت الإجابة نعم → استخدم DP. (3) هل تطلب المسألة عدّ جميع الحلول أو تعدادها؟ → استخدم DP أو البحث بالتراجع. (4) هل تطلب المسألة قيمة مثلى واحدة مع ترتيب طبيعي؟ فاشتبه في صلاحية الأسلوب الجشع. (5) عند الشك، اكتب تنفيذ DP؛ فهو صحيح دائمًا إذا كانت العلاقة التكرارية صحيحة، حتى إن كان أبطأ.
# Decision questions to ask:
questions = [
'1. Is there a natural ordering (by time, ratio, size)?',
'2. Does making the greedy choice leave a smaller same-type problem?',
'3. Can I construct a counterexample quickly?',
'4. Are sub-problems reused across different choice sequences?',
'5. Does the problem involve counting or listing (not just optimising)?',
]
for q in questions:
print(q)
print()
print('Greedy signals: scheduling, spanning tree, Huffman, jump game')
print('DP signals: knapsack, edit distance, LCS, coin change (general)')مسائل الفواصل: الجشع مقابل DP
تنقسم مسائل الفواصل بين الجشع وDP. في الفواصل غير المتداخلة، حيث نريد إزالة أقل عدد منها، رتّب الفواصل حسب وقت الانتهاء واخترها بطريقة جشعة؛ فالجشع مثالي بصورة مثبتة. أما في جدولة الفواصل الموزونة، حيث نريد تعظيم الوزن الإجمالي، فنحتاج إلى DP، لأن الفواصل ذات الوزن الكبير قد تتداخل مع فواصل خفيفة كثيرة، مما يتطلب مقارنة جميع المجموعات الفرعية الصالحة. والعامل الفاصل هو ما إذا كانت جميع الفواصل متساوية الوزن، فيناسبها الجشع، أم متفاوتة الوزن، فيناسبها DP.
# Non-overlapping intervals: greedy works
def erase_overlap_intervals(intervals):
if not intervals: return 0
intervals.sort(key=lambda x: x[1])
count = 0
last_end = float('-inf')
for start, end in intervals:
if start >= last_end:
last_end = end # keep this interval
else:
count += 1 # remove this interval
return count
print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]])) # 1
print(erase_overlap_intervals([[1,2],[1,2],[1,2]])) # 2التعرّف على مؤشرات المسألة
من المؤشرات الشائعة في نص المسألة: «الحد الأدنى لعدد العمليات»، «أقصى ربح»، «الاختيار الأمثل» → قد يكون الحل جشعًا أو باستخدام DP، فتحقق من وجود التداخل. «عدّ عدد الطرق» → استخدم DP دائمًا. «العثور على أي جدولة صالحة» → قد يناسبها الجشع. «جميع الاحتمالات» → استخدم البحث بالتراجع. «لا يمكن اختيار عناصر متجاورة» → استخدم DP، كما في مسألة لص المنازل. «الاجتماعات والفواصل والمهام» → غالبًا ما يناسبها الجشع. ويسرّع ربط المؤشرات بعائلات الخوارزميات تشخيص مسائل المقابلات.
# Signal-to-algorithm mapping
signals = {
'minimum steps/coins/operations': 'DP (unless trivially greedy)',
'maximum profit/value with constraint': 'DP (knapsack family)',
'count ways to reach/achieve': 'DP (always)',
'all combinations/permutations': 'Backtracking',
'schedule tasks within time': 'Greedy (sort by deadline/end)',
'cannot pick adjacent': 'DP (house robber pattern)',
'free to pick any subset': 'DP or Greedy (check overlap)',
'interval merging/selecting': 'Greedy (sort by end time)',
}
for signal, algo in signals.items():
print(f'{signal!r}: → {algo}')إثبات صحة الخوارزمية الجشعة
لإثبات صحة خوارزمية جشعة، استخدم حجة الاستبدال: (1) افترض وجود حل أمثل OPT يختلف عن الحل الجشع G عند الخيار الأول. (2) أثبت أنه يمكنك استبدال اختيار الحل الجشع في OPT من دون زيادة قيمة الهدف. (3) بالاستقراء، يكون الحل الجشع جيدًا بقدر أي حل أمثل. في المقابلات، لا تحتاج إلى تقديم إثبات كامل، لكن شرح فكرة حجة الاستبدال يوضح فهمًا عميقًا.
# Exchange argument demo: earliest-finish-time activity selection
# Suppose OPT starts with activity A (not earliest-ending)
# Let G = earliest-ending activity available
# A.end >= G.end (G ends earlier or same time)
# Swap A for G in OPT:
# - G.end <= A.end, so G does not conflict with anything A allowed after it
# - OPT remains valid with the same number of activities
# - Repeat: after swap, OPT begins with G, matching greedy first choice
# By induction, OPT can be transformed to match G activity by activity
# without losing activities → greedy is optimal
print('Exchange argument: any OPT can be modified to match Greedy without loss')
print('This proves Greedy >= OPT in objective value')اختبار سريع
اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
تعلمت في هذا الدرس أن الأسلوب الجشع يكون صحيحًا عند تحقق خاصية الاختيار الجشع، ويمكن إثبات ذلك باستخدام حجة الاستبدال، وأن DP مطلوب عندما تتداخل المسائل الفرعية، أي عندما تُوصل المسألة الفرعية نفسها بطرق متعددة، ولا يمكن حلها بقاعدة جشعة واحدة، وأن أسرع طريقة لدحض فرضية جشعة هي إنشاء مثال مضاد باستخدام مدخلات غير اعتيادية. في الجزء التالي، سنحل مسائل جدولة الفواصل ودمجها باستخدام الأسلوب الجشع القائم على الفرز حسب وقت الانتهاء.
الأسئلة الشائعة
هل درس «الخوارزميات الجشعة مقابل البرمجة الديناميكية: متى تستخدم كلًّا منهما» مجاني؟
نعم — نص درس «الخوارزميات الجشعة مقابل البرمجة الديناميكية: متى تستخدم كلًّا منهما» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «الخوارزميات الجشعة مقابل البرمجة الديناميكية: متى تستخدم كلًّا منهما»؟
حدّد سمات المسائل التي يمكن حلّها بخوارزمية جشعة، وتلك التي تتطلب البرمجة الديناميكية، باستخدام خاصية الاختيار الجشع وحجة الاستبدال. تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.
كم من الوقت يستغرق درس «الخوارزميات الجشعة مقابل البرمجة الديناميكية: متى تستخدم كلًّا منهما»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- الخوارزميات الجشعة مقابل البرمجة الديناميكية: متى تستخدم كلًّا منهما
- جدولة الفواصل ودمجها
- لعبة القفز I وII
- جدولة المهام ومحطة الوقود