تغيير العملات والسلم ذي التكلفة الدنيا
صُغ علاقات التكرار لمسألتي coin-change وmin-cost-climbing-stairs، واختر اتجاه DP الصحيح، وتتبع الجدول يدويًا
تغيير العملات والسلم ذي التكلفة الدنيا درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
تبديل العملات: تعريف المسألة
تعطيكم مسألة تبديل العملات (LeetCode #322) فئات من العملات ومبلغًا مستهدفًا. المطلوب هو إيجاد الحد الأدنى لعدد العملات اللازمة لتكوين المبلغ بالضبط. لديكم عدد غير محدود من العملات من كل فئة. هذه صيغة من مسألة حقيبة الظهر غير المحدودة — إذ يمكن استخدام كل عنصر، أي كل عملة، أي عدد من المرات. وتُعد من أهم مسائل البرمجة الديناميكية لأنها تختبر قدرتكم على صياغة علاقة تكرارية من الصفر.
# Problem examples:
# coins=[1,5,6,9], amount=11 -> 2 (5+6 or 2+9? no: 5+6=11 YES)
# coins=[2], amount=3 -> -1 (impossible)
# coins=[1,2,5], amount=11 -> 3 (5+5+1)
# coins=[186,419,83,408], amount=6249 -> 20
# Key choices:
# - Try each coin denomination at each step
# - Minimum coins = 1 + minimum(coins to make amount - coin)
# - If amount < 0: impossible
# - If amount = 0: done (0 coins)
print('Coin change: unbounded knapsack, find minimum count')تبديل العملات: اشتقاق العلاقة التكرارية
عرّفوا dp[i] بأنه الحد الأدنى لعدد العملات اللازمة لتكوين المبلغ i. لكل مبلغ i، جرّبوا استخدام كل عملة c: إذا كان i >= c، فحينها dp[i] = min(dp[i], 1 + dp[i-c]). تمثل «1» العملة التي استخدمناها للتو، بينما تمثل dp[i-c] الحل الأمثل للمبلغ المتبقي. يفترض ذلك توفر عدد لا نهائي من العملات. حالة الأساس هي: dp[0] = 0. هيّئوا جميع العناصر الأخرى إلى اللانهاية لتمثيل الحالات التي لم يصبح تحقيقها ممكنًا بعد.
def coin_change(coins, amount):
# dp[i] = min coins to make amount i
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # base: 0 coins for amount 0
for i in range(1, amount + 1):
for coin in coins:
if i >= coin and dp[i - coin] != float('inf'):
dp[i] = min(dp[i], 1 + dp[i - coin])
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change([1, 5, 6, 9], 11)) # 2
print(coin_change([2], 3)) # -1
print(coin_change([1, 2, 5], 11)) # 3
# Trace dp for coins=[1,5] amount=6:
# dp[0]=0, dp[1]=1, dp[2]=2, dp[3]=3, dp[4]=4, dp[5]=1, dp[6]=2تبديل العملات: لماذا تفشل الخوارزمية الجشعة
تفشل الخوارزمية الجشعة، التي تختار دائمًا أكبر عملة مناسبة، في مسألة تبديل العملات. مثال: coins=[1, 3, 4]، amount=6. تختار الخوارزمية الجشعة 4 ثم 1+1، أي 3 عملات. أما الحل الأمثل فهو 3+3، أي عملتان. تنجح الخوارزمية الجشعة مع الفئات القياسية (1، 5، 10، 25 سنتًا) لأنها تصادف أن تحقق خاصية الجشع. لكن بالنسبة إلى مجموعات العملات العشوائية، نحتاج إلى البرمجة الديناميكية. وهذه نقطة شائعة في المقابلات — فذكر فشل الخوارزمية الجشعة وشرح السبب يدل على تفكير تحليلي قوي.
# Greedy failure example:
# coins=[1,3,4], amount=6
# Greedy: 4 (rem=2), 1 (rem=1), 1 (rem=0) -> 3 coins
# Optimal: 3 (rem=3), 3 (rem=0) -> 2 coins
def coin_change_greedy_wrong(coins, amount):
coins_sorted = sorted(coins, reverse=True)
count = 0
for coin in coins_sorted:
while amount >= coin:
amount -= coin
count += 1
return count if amount == 0 else -1
print('Greedy:', coin_change_greedy_wrong([1,3,4], 6)) # 3 (WRONG)
print('DP: ', coin_change([1,3,4], 6)) # 2 (CORRECT)تبديل العملات II: عدّ الطرق
تطلب مسألة تبديل العملات II (LeetCode #518) حساب عدد الطرق لتكوين المبلغ، وليس الحد الأدنى لعدد العملات. تتغير العلاقة التكرارية هنا: فبدلًا من استخدام min، نستخدم الجمع. طبّقوا dp[i] += dp[i-coin] لكل عملة. ويكتسب ترتيب الملء أهمية: لعدّ كل تركيب مرة واحدة، اجعلوا العملات في الحلقة الخارجية والمبالغ في الحلقة الداخلية. أما عكس الحلقتين فيحسب التبديلات بدلًا من التراكيب، وهي مسألة مختلفة.
def coin_change_ii(coins, amount):
# dp[i] = number of ways to make amount i
dp = [0] * (amount + 1)
dp[0] = 1 # one way to make amount 0: use no coins
# Outer loop: coins -- ensures each coin type processed once
for coin in coins:
# Inner loop: amounts
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
return dp[amount]
print(coin_change_ii([1, 2, 5], 5)) # 4: [1,1,1,1,1],[1,1,1,2],[1,2,2],[5]
print(coin_change_ii([2], 3)) # 0: impossible
print(coin_change_ii([10], 10)) # 1
# Key: coin outer, amount inner = COMBINATIONS (unordered)
# Reverse (amount outer, coin inner) = PERMUTATIONS (ordered)السلم ذو أقل تكلفة: تعريف المسألة
تعطيكم مسألة Min Cost Climbing Stairs (LeetCode #746) سلمًا لكل درجة فيه تكلفة. يمكنكم صعود درجة واحدة أو درجتين في كل مرة. المطلوب هو إيجاد أقل تكلفة للوصول إلى القمة، أي إلى درجة واحدة بعد آخر درجة. يمكنكم البدء من الدرجة 0 أو الدرجة 1 مجانًا. تجمع هذه المسألة بطريقة أنيقة بين العلاقة التكرارية لمسألة صعود الدرج ونمط تقليل التكلفة في مسألة تبديل العملات، مما يجعلها جسرًا طبيعيًا بين المسألتين.
# cost = [10, 15, 20]
# Pay cost[i] to leave step i
# You can step to i+1 or i+2
# Goal: reach top (index 3) with minimum cost
# Path options:
# Start at 0: cost 10, go to 2: cost 20, done -> 30
# Start at 1: cost 15, go to 3: done -> 15 <- OPTIMAL
# Start at 0: cost 10, go to 1: cost 15 -> 25
cost = [10, 15, 20]
# Optimal: start at step 1, pay 15, jump to top -> cost = 15
print('Expected:', 15)السلم ذو أقل تكلفة: العلاقة التكرارية
عرّفوا dp[i] بأنه أقل تكلفة للوصول إلى الدرجة i. يمكن الوصول إلى الدرجة i بدفع cost[i-1] من الدرجة i-1، أو cost[i-2] من الدرجة i-2. لذلك تكون العلاقة dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]). حالات الأساس هي: dp[0] = 0، لأننا نبدأ قبل الدرج مجانًا، وdp[1] = 0، إذ يمكننا أيضًا البدء من الدرجة 1 مجانًا. الإجابة هي dp[n]، حيث n = len(cost).
def min_cost_climbing_stairs(cost):
n = len(cost)
# dp[i] = minimum cost to reach step i
# Steps 0 to n; step n is the top (goal)
dp = [0] * (n + 1)
# dp[0] = 0 (free to start here)
# dp[1] = 0 (free to start here)
for i in range(2, n + 1):
dp[i] = min(dp[i-1] + cost[i-1], # step from i-1
dp[i-2] + cost[i-2]) # jump from i-2
return dp[n]
print(min_cost_climbing_stairs([10, 15, 20])) # 15
print(min_cost_climbing_stairs([1,100,1,1,1,100,1,1,100,1])) # 6السلم ذو أقل تكلفة: تحسين المساحة
بما أن dp[i] يعتمد فقط على dp[i-1] وdp[i-2]، يمكننا تقليل المساحة إلى O(1) باستخدام متغيرين، تمامًا كما في فيبوناتشي. استبدلوا المصفوفة بـ prev2 وprev1، ثم حدّثوهما في كل خطوة. يُعد هذا تحسينًا قياسيًا من سطر واحد يتوقعه المحاورون بعد عرض حل الجدول ذي التعقيد O(n). اذكروه دائمًا بشكل استباقي: «يمكننا تقليل المساحة إلى O(1)، لأننا لا نحتاج إلا إلى آخر قيمتين».
def min_cost_optimised(cost):
n = len(cost)
prev2, prev1 = 0, 0 # dp[0] and dp[1]
for i in range(2, n + 1):
curr = min(prev1 + cost[i-1], prev2 + cost[i-2])
prev2, prev1 = prev1, curr
return prev1
print(min_cost_optimised([10, 15, 20])) # 15
print(min_cost_optimised([1,100,1,1,1,100,1,1,100,1])) # 6
# Alternative: directly use cost array as rolling storage
def min_cost_v2(cost):
n = len(cost)
for i in range(2, n):
cost[i] += min(cost[i-1], cost[i-2])
return min(cost[-1], cost[-2])
from copy import deepcopy
cost_test = [10,15,20]
print(min_cost_v2(deepcopy(cost_test))) # 15صياغة بديلة للبرمجة الديناميكية
تحتوي بعض المسائل على عدة صيغ صحيحة للبرمجة الديناميكية. في مسألة السلم ذي أقل تكلفة، يمكنكم تعريف dp[i] بأنه أقل تكلفة لمغادرة الدرجة i، وذلك بدفع cost[i] واختيار الانتقال إلى i+1 أو i+2. عندها تكون dp[i] = cost[i] + min(dp[i+1], dp[i+2])، مع ملء الجدول من اليمين إلى اليسار، وتكون الإجابة min(dp[0], dp[1]). كلتا الصياغتين صحيحتان. تدرّبوا على توضيح الصياغة التي اخترتموها وسبب اختيارها، فهذا يبرهن على إتقانكم للبرمجة الديناميكية.
def min_cost_alternative(cost):
n = len(cost)
# dp[i] = min cost when starting FROM step i
# Fill right to left
dp = cost[:] + [0] # dp[n] = 0 (already at top)
for i in range(n - 1, -1, -1):
# Pay cost[i], then choose i+1 or i+2
if i + 2 <= n:
dp[i] = cost[i] + min(dp[i+1], dp[i+2])
else:
dp[i] = cost[i] + dp[i+1]
# Can start at step 0 or step 1
return min(dp[0], dp[1])
print(min_cost_alternative([10, 15, 20])) # 15
print(min_cost_alternative([1,100,1,1,1,100,1,1,100,1])) # 6الربط بين تبديل العملات والسلم
ينتمي كل من تبديل العملات والسلم ذي أقل تكلفة إلى نمط البرمجة الديناميكية نفسه: ففي كل خطوة، نختار خيارًا من مجموعة محدودة من الخيارات، ثم نحسّن هدفًا على امتداد سلسلة الاختيارات. أما الاختلافات فهي شكلية: تتتبع مسألة تبديل العملات العدد، فتضيف 1 لكل عملة، بينما تتتبع مسألة السلم التكلفة، فتضيف cost[i] لكل خطوة. يتيح لكم التعرّف على هذا الهيكل المشترك حل مسائل برمجة ديناميكية جديدة عبر مطابقتها مع قوالب مألوفة.
# Shared pattern:
# dp[state] = optimise(dp[prev_state_1] + cost_1,
# dp[prev_state_2] + cost_2, ...)
# Coin change: dp[amount] = min(1 + dp[amount - coin] for coin in coins)
# Min stair: dp[step] = min(cost[step-1]+dp[step-1], cost[step-2]+dp[step-2])
# Max path sum: dp[cell] = max(dp[top], dp[left]) + grid[cell]
# House robber: dp[house] = max(dp[house-1], dp[house-2] + value[house])
# All four are the SAME pattern with different:
# - State representation
# - Number of choices per state
# - Objective (min/max)
# - Transition cost
print('DP pattern: state + choices + objective + cost = template')الحد الأدنى لعدد المربعات الكاملة
تطلب مسألة Perfect Squares (LeetCode #279) إيجاد الحد الأدنى لعدد المربعات الكاملة (1، 4، 9، 16، ...) التي يساوي مجموعها n. وهذه هي بالضبط مسألة تبديل العملات، حيث تكون «العملات» أعدادًا مربعة كاملة. ولّّدوا جميع المربعات الكاملة التي لا تتجاوز n، ثم طبّقوا خوارزمية تبديل العملات. تمنحكم البرمجة الديناميكية زمنًا قدره O(n * sqrt(n)). تنص مبرهنة لاغرانج للمربعات الأربعة على أن الإجابة لا تتجاوز 4، مما يتيح أيضًا نهجًا رياضيًا بزمن O(sqrt(n)) — لكن البرمجة الديناميكية هي الحل المتوقع.
import math
def num_squares(n):
# Generate all perfect squares up to n
squares = [i*i for i in range(1, int(math.sqrt(n)) + 1)]
# Coin change with squares as 'coins'
dp = [float('inf')] * (n + 1)
dp[0] = 0
for i in range(1, n + 1):
for sq in squares:
if i >= sq:
dp[i] = min(dp[i], 1 + dp[i - sq])
return dp[n]
print(num_squares(12)) # 3: 4+4+4
print(num_squares(13)) # 2: 4+9
print(num_squares(1)) # 1: 1تنقيح البرمجة الديناميكية: الأخطاء الشائعة
تشمل أخطاء البرمجة الديناميكية الشائعة: حالة أساس خاطئة، مثل ضبط dp[0] بشكل غير صحيح؛ وترتيب ملء خاطئ، أي الوصول إلى قيمة لم تُحسب بعد؛ وخطأ بمقدار واحد في تعريف الحالة، مثل الخلط بين dp[i] بوصفها تكلفة الوصول إلى i وتكلفة مغادرة i؛ وعدم إرجاع -1 عند بقاء قيمة اللانهاية، أي في الحالات المستحيلة. اختبروا الحل دائمًا على أبسط الحالات، مثل الإدخال الفارغ والعنصر الواحد وtarget=0، قبل اختباره على مدخلات أكبر.
# Common DP debugging checklist:
# 1. Base case: what is dp[0]? dp[1]? Are they correct?
# 2. State definition: write it in English before coding
# 3. Recurrence: trace manually on a 3-element example
# 4. Fill order: dependency arrows point left/up? Fill left/up first
# 5. Infinity check: return -1 or 0 when dp[target] == inf?
# 6. Array bounds: dp has size n+1 for 0..n, or n for 0..n-1?
# Quick test template:
def test_coin_change():
assert coin_change([1], 0) == 0 # base case
assert coin_change([1], 1) == 1 # single coin
assert coin_change([2], 3) == -1 # impossible
assert coin_change([1,5,6,9], 11) == 2
print('All tests passed!')
test_coin_change()اختبار سريع
تحقّقوا من فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
ملخص الدرس
تعلّمتم في هذا الدرس: البرمجة الديناميكية لإيجاد الحد الأدنى لعدد العملات، وهي صيغة من حقيبة الظهر غير المحدودة، وسبب فشل الخوارزمية الجشعة؛ وتبديل العملات II لعدّ التراكيب باستخدام العملات في الحلقة الخارجية والمبالغ في الحلقة الداخلية؛ والسلم ذي أقل تكلفة بصيغتيه من اليسار إلى اليمين ومن اليمين إلى اليسار. بعد ذلك، سنستكشف أنماط البرمجة الديناميكية أحادية الأبعاد باستخدام سارق المنازل وخوارزمية Kadane وWord Break.
الأسئلة الشائعة
هل درس «تغيير العملات والسلم ذي التكلفة الدنيا» مجاني؟
نعم — نص درس «تغيير العملات والسلم ذي التكلفة الدنيا» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «تغيير العملات والسلم ذي التكلفة الدنيا»؟
صُغ علاقات التكرار لمسألتي coin-change وmin-cost-climbing-stairs، واختر اتجاه DP الصحيح، وتتبع الجدول يدويًا تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «تغيير العملات والسلم ذي التكلفة الدنيا»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- التعرّف إلى DP: المسائل الفرعية المتداخلة
- DP من الأعلى إلى الأسفل مع التخزين المؤقت
- DP من الأسفل إلى الأعلى باستخدام الجدولة
- تغيير العملات والسلم ذي التكلفة الدنيا