نوفيل
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
دخولابدأ مجاناً
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
نوفيل

منصة تعليم البرمجة الأولى بالعربي. تعلم من الصفر حتى الاحتراف مع كورسات احترافية وتحديات ذكاء اصطناعي.

المنصة

  • الكورسات
  • التحديات
  • المقالات
  • الأدوات

الحساب

  • إنشاء حساب
  • تسجيل الدخول
  • لوحة التحكم
  • الملف الشخصي

روابط

  • سياسة الخصوصية
  • شروط الاستخدام
  • عن نوفيل
  • تواصل معنا

© 2026 نوفيل. جميع الحقوق محفوظة.

صُنع بـ في مصر

نوفيل
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
دخولابدأ مجاناً
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
نوفيل

منصة تعليم البرمجة الأولى بالعربي. تعلم من الصفر حتى الاحتراف مع كورسات احترافية وتحديات ذكاء اصطناعي.

المنصة

  • الكورسات
  • التحديات
  • المقالات
  • الأدوات

الحساب

  • إنشاء حساب
  • تسجيل الدخول
  • لوحة التحكم
  • الملف الشخصي

روابط

  • سياسة الخصوصية
  • شروط الاستخدام
  • عن نوفيل
  • تواصل معنا

© 2026 نوفيل. جميع الحقوق محفوظة.

صُنع بـ في مصر

نوفيل
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
دخولابدأ مجاناً
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
نوفيل

منصة تعليم البرمجة الأولى بالعربي. تعلم من الصفر حتى الاحتراف مع كورسات احترافية وتحديات ذكاء اصطناعي.

المنصة

  • الكورسات
  • التحديات
  • المقالات
  • الأدوات

الحساب

  • إنشاء حساب
  • تسجيل الدخول
  • لوحة التحكم
  • الملف الشخصي

روابط

  • سياسة الخصوصية
  • شروط الاستخدام
  • عن نوفيل
  • تواصل معنا

© 2026 نوفيل. جميع الحقوق محفوظة.

صُنع بـ في مصر

نوفيل
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
دخولابدأ مجاناً
الرئيسيةالكورساتالتحديات⚔️ المبارزاتالمقالاتالأدواتتغريداتالمجتمع
المقالات/الخوارزميات
الخوارزميات

Dynamic Programming: عندما تصبح الـ Loops كارثة وكيف تنقذ مشروعك بخوارزمية واحدة

هل تعاني من أكواد بطيئة تتجمد عند ١٠٠٠ عنصر؟ اكتشف كيف تحول الـ Dynamic Programming مشاكل الـ O(n²) إلى O(n) بخطوات عملية وأمثلة واقعية من مشروعات حقيقية.

فريق نوفيل١٧ يوليو ٢٠٢٦10 دقائق قراءة١١ مشاهدة

في أحد المشاريع التي عملت عليها مع فريق في شركة ناشئة، كان لدينا نظام توصية يعرض منتجات مشابهة بناءً على سلوك المستخدم. الكود كان يعمل بشكل جيد مع ١٠٠ منتج، لكن عندما وصلنا إلى ١٠٠٠ منتج، أصبح السيرفر يتجمد لمدة ١٥ ثانية قبل الرد. المشكلة؟ كنا نستخدم حلاً بسيطاً يعتمد على حلقات متداخلة (nested loops) لحساب التشابه بين كل زوج من المنتجات. عندما فحصنا الـ CPU Usage، وجدنا أن المعالج كان يعمل بنسبة ١٠٠٪ لمدة طويلة، والذاكرة كانت تفيض بسبب الـ Recursion غير المتحكم فيه. هذا هو بالضبط النوع من الكوارث التي تنقذها الـ Dynamic Programming.

الكثير من المطورين يسمعون عن Dynamic Programming ويشعرون بالخوف منها. قد تبدو معقدة، مليئة بالمصطلحات مثل memoization و tabulation، وكأنها شيء لا يستخدم إلا في مسابقات البرمجة أو في كتب الخوارزميات الأكاديمية. لكن الحقيقة هي أن Dynamic Programming ليست سوى طريقة ذكية لتجنب إعادة حساب نفس الشيء مراراً وتكراراً. إذا كنت قد كتبت كوداً يحسب نفس القيمة أكثر من مرة، فأنت بحاجة إليها. في هذا المقال، سأريك كيف تتحول من الخوف إلى الإتقان بخطوات عملية، مع أمثلة متدرجة تبدأ من مشكلة بسيطة وتنتهي بمشكلة حقيقية من سوق العمل.

المشكلة الأساسية: لماذا تفشل الحلول البسيطة؟

لنبدأ بمثال كلاسيكي: مشكلة حساب العدد n من متتالية فيبوناتشي. إذا كتبت دالة بسيطة تعتمد على الـ Recursion، فستبدو هكذا:

python
def fib(n):
 if n <= 1:
 return n
 return fib(n-1) + fib(n-2)

هذا الكود يبدو أنيقاً وبسيطاً، لكنه كارثة من الناحية الأداء. لماذا؟ لأن كل استدعاء لـ fib(n) يؤدي إلى استدعاءين آخرين، مما ينتج عنه شجرة استدعاءات هائلة. على سبيل المثال، حساب fib(5) يتطلب ١٥ استدعاء، وحساب fib(50) يتطلب ٢٠.٣٦٥.٠١١.٠٧٤ استدعاء! هذا يعني أن التعقيد الزمني هنا هو O(2^n)، وهو أسوأ مما يمكنك تخيله. إذا حاولت تشغيل هذا الكود لـ n=50 على جهازك، فستجد أن البرنامج يتجمد تماماً، وربما يتوقف بسبب استهلاك الذاكرة الزائد.

المشكلة هنا ليست في الـ Recursion بحد ذاتها، بل في أننا نحسب نفس القيم مراراً وتكراراً. مثلاً، fib(3) يتم حسابه عدة مرات في شجرة الاستدعاءات. هذا هو بالضبط ما تعالجه الـ Dynamic Programming: تجنب إعادة الحساب عن طريق تخزين النتائج واسترجاعها عند الحاجة. لكن قبل أن ننتقل إلى الحل، دعنا نفهم لماذا تفشل الحلول البسيطة الأخرى أيضاً.

الحلول البديلة وفخاخها

هناك حلول أخرى لحساب فيبوناتشي، مثل استخدام حلقة تكرارية (loop):

python
def fib_iterative(n):
 if n <= 1:
 return n
 a, b = 0, 1
 for _ in range(2, n+1):
 a, b = b, a + b
 return b

هذا الحل أفضل بكثير، حيث يعمل في O(n) زمنياً و O(1) مكانياً. لكنه ليس حلاً عاماً يمكن تطبيقه على جميع المشاكل التي تعالجها الـ Dynamic Programming. مثلاً، إذا كانت المشكلة تتطلب حساب جميع القيم من fib(0) إلى fib(n) واستخدامها في حسابات أخرى (مثل مشكلة

هناك أيضاً حلول تعتمد على الصيغ الرياضية، مثل صيغة بينيه (Binet's formula) لحساب فيبوناتشي، لكنها غالباً ما تكون غير دقيقة للأعداد الكبيرة بسبب أخطاء التقريب في العمليات الحسابية على الفاصلة العائمة. بالإضافة إلى ذلك، ليست كل المشاكل لها صيغ رياضية مغلقة، لذا فإن هذه الحلول ليست عامة.


Dynamic Programming: الخطوات العملية لتحويل المشكلة

الآن، دعنا ننتقل إلى كيفية تطبيق الـ Dynamic Programming بشكل عملي. هناك طريقتان رئيسيتان: الـ Top-Down (Memoization) والـ Bottom-Up (Tabulation). سأشرح كل منهما بالتفصيل، مع أمثلة توضح متى تستخدم كل طريقة.

الطريقة الأولى: Top-Down مع Memoization

في هذه الطريقة، نبدأ من المشكلة الكبيرة ونقسمها إلى مشاكل أصغر، ونخزن نتائج المشاكل الأصغر في جدول (عادةً مصفوفة أو قاموس) لتجنب إعادة حسابها. هذه الطريقة تشبه إلى حد كبير الـ Recursion، لكنها تضيف خطوة تخزين النتائج. لنطبقها على مشكلة فيبوناتشي:

python
def fib_memo(n, memo={}):
 if n in memo:
 return memo[n]
 if n <= 1:
 return n
 memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
 return memo[n]

هنا، نستخدم قاموس memo لتخزين النتائج. عندما نستدعي fib_memo(n)، نتحقق أولاً إذا كانت النتيجة موجودة في memo. إذا كانت موجودة، نعيدها مباشرة دون الحاجة إلى إعادة الحساب. إذا لم تكن موجودة، نحسبها ونخزنها في memo قبل إعادتها. هذا يقلل التعقيد الزمني من O(2^n) إلى O(n)، والتعقيد المكاني يصبح O(n) بسبب الـ Call Stack والـ memo.

متى تستخدم هذه الطريقة؟ عندما يكون لديك مشكلة يمكن تقسيمها إلى مشاكل فرعية متداخلة (overlapping subproblems)، وعندما تريد الحفاظ على الكود قريباً من الحل التكراري الأصلي. هذه الطريقة أيضاً أسهل في الفهم والتنفيذ بالنسبة للمبتدئين، لأنها تعتمد على نفس منطق الـ Recursion الذي اعتاد عليه المطورون.

الطريقة الثانية: Bottom-Up مع Tabulation

في هذه الطريقة، نبدأ من أصغر مشكلة ونبني الحلول الأكبر تدريجياً. نستخدم جدولاً (عادةً مصفوفة) لتخزين النتائج ونملأه بشكل تكراري. لنطبقها على فيبوناتشي:

python
def fib_tab(n):
 if n <= 1:
 return n
 dp = [0] * (n + 1)
 dp[1] = 1
 for i in range(2, n + 1):
 dp[i] = dp[i-1] + dp[i-2]
 return dp[n]

هنا، نبدأ بملء المصفوفة dp من الأسفل إلى الأعلى. dp[0] و dp[1] هما القيم المعروفة مسبقاً، ثم نحسب dp[2] باستخدام dp[0] و dp[1]، وهكذا حتى نصل إلى dp[n]. هذه الطريقة أيضاً تعمل في O(n) زمنياً و O(n) مكانياً، لكنها تتجنب الـ Recursion تماماً، مما يجعلها أكثر كفاءة في بعض الحالات (مثلاً، عندما يكون عمق الـ Recursion كبيراً جداً وقد يؤدي إلى stack overflow).

متى تستخدم هذه الطريقة؟ عندما تريد تجنب الـ Recursion تماماً، أو عندما تريد تحسين استخدام الذاكرة (يمكنك في بعض الحالات تقليل التعقيد المكاني إلى O(1) باستخدام متغيرات بدلاً من مصفوفة كاملة). هذه الطريقة أيضاً أكثر ملاءمة للمشاكل التي تتطلب حساب جميع القيم الوسيطة واستخدامها لاحقاً، مثل مشكلة


متى تستخدم Dynamic Programming ومتى تتجنبها؟

ليس كل مشكلة تحتاج إلى Dynamic Programming. في الواقع، استخدام Dynamic Programming في مشكلة لا تحتاج إليها يمكن أن يجعل الكود أكثر تعقيداً ويزيد من استخدام الذاكرة دون فائدة. إذن، كيف تعرف متى تستخدمها؟ هناك شرطان أساسيان:

  • •المشكلة لها مشاكل فرعية متداخلة (overlapping subproblems): أي أنك تحسب نفس الشيء أكثر من مرة.
  • •المشكلة لها خاصية التحسين الفرعي (optimal substructure): أي أن الحل الأمثل للمشكلة الكبيرة يمكن بناؤه من الحلول المثلى للمشاكل الفرعية.

إذا كانت المشكلة لا تحقق هذين الشرطين، فربما لا تحتاج إلى Dynamic Programming. مثلاً، مشكلة البحث الثنائي (Binary Search) لا تحتاج إليها، لأن كل خطوة تعتمد فقط على الخطوة السابقة ولا تتكرر الحسابات. أما مشكلة مثل

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


مثال متقدم: مشكلة Knapsack وكيف تحلها بخطوات عملية

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

لنبدأ بتحليل المشكلة. إذا حاولت حلها بطريقة بسيطة باستخدام الـ Recursion، فستجد أن التعقيد الزمني هو O(2^n)، وهو غير مقبول حتى لـ n صغيرة. لكن المشكلة تحقق شرطي Dynamic Programming: لها مشاكل فرعية متداخلة (مثلاً، اختيار عنصر معين أو عدم اختياره يؤثر على نفس المجموعة الفرعية من العناصر)، ولها خاصية التحسين الفرعي (الحل الأمثل للمشكلة الكبيرة يعتمد على الحلول المثلى للمشاكل الفرعية).

الحل باستخدام Tabulation

سنستخدم طريقة Tabulation لحل هذه المشكلة. الفكرة هي بناء جدول dp بحجم (n+1) × (W+1)، حيث n هو عدد العناصر و W هو سعة العلبة. dp[i][w] سيمثل القيمة القصوى التي يمكن الحصول عليها باستخدام أول i عنصر وسعة w. إليك الكود:

python
def knapsack(values, weights, W):
 n = len(values)
 dp = [[0] * (W + 1) for _ in range(n + 1)]
 
 for i in range(1, n + 1):
 for w in range(1, W + 1):
 if weights[i-1] <= w:
 dp[i][w] = max(dp[i-1][w], values[i-1] + dp[i-1][w - weights[i-1]])
 else:
 dp[i][w] = dp[i-1][w]
 
 return dp[n][W]

في هذا الكود، نملأ الجدول dp بشكل تكراري. لكل عنصر، نقرر إما أن نأخذه أو لا نأخذه بناءً على وزنه وقيمته. إذا كان وزن العنصر أقل من أو يساوي السعة الحالية، نقارن بين القيمة التي نحصل عليها إذا أخذنا العنصر والقيمة التي نحصل عليها إذا لم نأخذه، ونختار القيمة الأكبر. إذا كان وزن العنصر أكبر من السعة الحالية، فلا يمكننا أخذه، لذا نحمل القيمة من الصف السابق.

هذا الحل يعمل في O(nW) زمنياً و O(nW) مكانياً. لاحظ أن التعقيد الزمني هنا ليس متعدد الحدود تماماً (pseudo-polynomial)، لأنه يعتمد على قيمة W وليس فقط على عدد العناصر. هذا يعني أنه إذا كانت W كبيرة جداً، فقد يكون الحل بطيئاً. لكن في معظم الحالات العملية، يكون هذا الحل فعالاً جداً.

تحسين استخدام الذاكرة

يمكننا تحسين استخدام الذاكرة في حل Knapsack عن طريق ملاحظة أن كل صف في الجدول dp يعتمد فقط على الصف السابق. لذا، بدلاً من استخدام جدول ثنائي الأبعاد، يمكننا استخدام مصفوفة أحادية البعد وتحديثها بشكل عكسي:

python
def knapsack_optimized(values, weights, W):
 n = len(values)
 dp = [0] * (W + 1)
 
 for i in range(n):
 for w in range(W, weights[i] - 1, -1):
 dp[w] = max(dp[w], values[i] + dp[w - weights[i]])
 
 return dp[W]

في هذا الكود، نستخدم مصفوفة dp بحجم (W+1) فقط. نملأها بشكل عكسي من W إلى وزن العنصر الحالي لتجنب الكتابة فوق القيم التي نحتاجها لاحقاً. هذا يقلل التعقيد المكاني إلى O(W)، مما يجعل الحل أكثر كفاءة في استخدام الذاكرة.


Dynamic Programming في العالم الحقيقي: أمثلة من شركات تقنية

Dynamic Programming ليست مجرد نظرية أكاديمية، بل تستخدم بكثرة في الشركات التقنية لحل مشاكل حقيقية. مثلاً، في شركة أمازون، تستخدم Dynamic Programming في أنظمة التوصية لتحسين اختيار المنتجات المعروضة للمستخدمين بناءً على سلوكهم السابق. المشكلة هنا مشابهة لمشكلة Knapsack، حيث تريد اختيار مجموعة من المنتجات التي تحقق أعلى قيمة (مثلاً، أعلى احتمال للنقر أو الشراء) ضمن قيود معينة (مثلاً، عدد المنتجات المعروضة أو وقت التحميل).

في شركة جوجل، تستخدم Dynamic Programming في خوارزميات البحث والتحسين. مثلاً، في نظام خرائط جوجل، يمكن استخدام Dynamic Programming لحساب أقصر مسار بين نقطتين مع مراعاة قيود مثل ازدحام المرور أو رسوم الطرق. هذه المشكلة مشابهة لمشكلة

في شركة أوبر، تستخدم Dynamic Programming في تحسين توزيع الرحلات على السائقين. المشكلة هنا هي مطابقة الركاب مع السائقين بطريقة تحقق أعلى ربح للشركة وأقل وقت انتظار للركاب. هذه المشكلة يمكن نمذجتها كمشكلة

حتى في الشركات الناشئة، يمكن استخدام Dynamic Programming لحل مشاكل مثل تحسين استهلاك الطاقة في الأجهزة المحمولة، أو تحسين أداء التطبيقات التي تعتمد على البيانات الكبيرة. مثلاً، في تطبيق للصحة والتغذية، يمكن استخدام Dynamic Programming لحساب أفضل خطة غذائية بناءً على القيود الغذائية للمستخدم (مثل السعرات الحرارية، البروتين، الكربوهيدرات، إلخ). هذه المشكلة مشابهة لمشكلة Knapsack متعددة الأبعاد.


الفخاخ الشائعة وكيف تتجنبها

حتى بعد فهمك لـ Dynamic Programming، هناك بعض الفخاخ التي يمكن أن تقع فيها وتجعل الحل غير فعال أو حتى خاطئ. سأذكر هنا بعضاً من هذه الفخاخ وكيف تتجنبها:

  • •عدم تحديد المشاكل الفرعية بشكل صحيح: إذا لم تحدد المشاكل الفرعية بشكل صحيح، فقد ينتهي بك الأمر إلى حل مشاكل غير ضرورية أو تفوتك بعض الحالات. دائماً ابدأ بتحديد المشاكل الفرعية الأصغر وتأكد من أنها تغطي جميع الحالات.
  • •استخدام Memoization بدون فهم عميق للـ Recursion: Memoization تعتمد على الـ Recursion، لذا إذا لم تفهم كيف تعمل الـ Recursion في مشكلتك، فقد تواجه مشاكل مثل stack overflow أو استخدام ذاكرة زائدة. دائماً اختبر حلولك على مدخلات صغيرة أولاً.
  • •عدم تحسين استخدام الذاكرة: في بعض الحالات، يمكنك تقليل استخدام الذاكرة بشكل كبير باستخدام تقنيات مثل ملء الجدول بشكل عكسي أو استخدام مصفوفة أحادية البعد بدلاً من ثنائية الأبعاد. دائماً فكر في كيفية تحسين استخدام الذاكرة، خاصة إذا كانت المشكلة كبيرة.
  • •التسرع في اختيار طريقة الحل: أحياناً، قد تبدو طريقة Memoization أسهل في التنفيذ، لكن طريقة Tabulation قد تكون أكثر كفاءة في استخدام الذاكرة أو أسرع في التنفيذ. دائماً قارن بين الطريقتين واختر الأنسب لمشكلتك.
  • •عدم اختبار الحالات الحدية: دائماً اختبر حلولك على الحالات الحدية، مثل المدخلات الفارغة أو المدخلات الكبيرة جداً. هذه الحالات غالباً ما تكشف عن أخطاء في الكود أو في منطق الحل.

هناك أيضاً فخ آخر يتعلق بفهم التعقيد الزمني والمكاني. مثلاً، في مشكلة Knapsack، قد تعتقد أن التعقيد الزمني هو O(nW) وأنه متعدد الحدود، لكن في الواقع، W يمكن أن تكون كبيرة جداً، مما يجعل الحل غير فعال عملياً. في هذه الحالات، قد تحتاج إلى استخدام تقنيات أخرى مثل البرمجة الخطية أو الخوارزميات التقريبية.


خلاصة المهندس: نصائح عملية لإتقان Dynamic Programming

Dynamic Programming ليست مجرد أداة لحل مشاكل الأكاديميا، بل هي سلاح سري في ترسانتك البرمجية. عندما تواجه مشكلة يبدو أن حلها البسيط بطيء جداً، اسأل نفسك: هل هذه المشكلة لها مشاكل فرعية متداخلة؟ هل يمكن بناء الحل الأمثل من الحلول المثلى للمشاكل الفرعية؟ إذا كانت الإجابة نعم، فأنت أمام مرشح قوي لـ Dynamic Programming.

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

وأخيراً، لا تخف من Dynamic Programming. نعم، قد تبدو معقدة في البداية، لكن مع الممارسة، ستصبح جزءاً طبيعياً من طريقة تفكيرك البرمجية. ابدأ بمشاكل بسيطة مثل فيبوناتشي و Climbing Stairs، ثم انتقل إلى مشاكل أكثر تعقيداً مثل Knapsack و Longest Common Subsequence. كلما حللت المزيد من المشاكل، كلما أصبحت أكثر راحة مع التفكير بطريقة Dynamic Programming.

Dynamic Programming هي فن تحويل المشاكل المعقدة إلى سلسلة من الخطوات البسيطة التي يمكن إعادة استخدامها. المفتاح هو رؤية الأنماط والتكرار في المشكلة، ثم استغلالها لتوفير الوقت والذاكرة.

— ريتشارد بيلمان، مخترع Dynamic Programming
Dynamic Programming الخوارزميات تحسين الأداء البرمجة الديناميكية حل المشاكل البرمجية

التعليقات

العودة للمقالات
نوفيل

منصة تعليم البرمجة الأولى بالعربي. تعلم من الصفر حتى الاحتراف مع كورسات احترافية وتحديات ذكاء اصطناعي.

المنصة

  • الكورسات
  • التحديات
  • المقالات
  • الأدوات

الحساب

  • إنشاء حساب
  • تسجيل الدخول
  • لوحة التحكم
  • الملف الشخصي

روابط

  • سياسة الخصوصية
  • شروط الاستخدام
  • عن نوفيل
  • تواصل معنا

© 2026 نوفيل. جميع الحقوق محفوظة.

صُنع بـ في مصر