كانت حساباتنا تتكرر إلى ما لا نهاية: كيف أنقذتنا ‘البرمجة الديناميكية’ من جحيم التعقيد الأسي؟

يا جماعة الخير، السلام عليكم ورحمة الله وبركاته. معكم أخوكم أبو عمر.

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

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

بعد حفر وتنقيب في الكود وسهر ليالي، اكتشفنا الكارثة: الخوارزمية التعاودية (Recursive) اللي كتبناها كانت بتعيد حساب نفس الأشياء آلاف، بل ملايين المرات. كنا بنطلب منها تحسب أفضل تشكيلة لـ 100 دولار، وفي داخلها كانت بتحسب أفضل تشكيلة لـ 50 دولار، وبعد شوي في مسار ثاني بترجع تحسب أفضل تشكيلة لـ 50 دولار مرة ثانية، وثالثة، ورابعة… جحيم حقيقي من التكرار.

هنا لمعت في بالي فكرة قرأت عنها في كتاب خوارزميات قديم… فكرة بسيطة لكنها عبقرية: “ليش ما نخزن نتيجة كل عملية حسابية أول مرة بنعملها، ولما نحتاجها مرة ثانية، ناخذها من الذاكرة بدل ما نحسبها من جديد؟”. هذا المبدأ، يا أصدقائي، هو روح وقلب ما يعرف بـ “البرمجة الديناميكية” (Dynamic Programming)، وهو اللي أنقذ مشروعنا من الفشل الذريع. تعالوا أحكي لكم القصة بالتفصيل التقني.

ما هي البرمجة الديناميكية (Dynamic Programming)؟

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

  1. حل كل مشكلة فرعية مرة واحدة فقط.
  2. تخزين حل هذه المشكلة الفرعية في مكان ما (ذاكرة مؤقتة أو جدول)، عشان لو احتجناه مرة ثانية، نجيبه جاهز بدون إعادة حساب.

تخيل أنك تحل واجب رياضيات، وواحدة من المسائل تطلب منك حساب 17 × 9. حسبتها وطلع الجواب 153. بعد قليل، في مسألة أخرى، احتجت نفس حاصل الضرب. هل سترجع تحسبها من جديد؟ طبعًا لا، لأنك “تتذكر” الجواب. البرمجة الديناميكية هي بالضبط هذا المبدأ، لكن للكمبيوتر.

لكي تعمل هذه الطريقة، يجب أن تتمتع المشكلة بخاصيتين أساسيتين.

الخاصية الأولى: المسائل الفرعية المتداخلة (Overlapping Subproblems)

هذه هي مربط الفرس. تعني أن الخوارزمية أثناء حلها للمشكلة الكبيرة، تعود وتواجه نفس المشاكل الفرعية مرارًا وتكرارًا. هذا بالضبط ما حدث معنا في مشروعنا، وما يحدث في الكثير من الخوارزميات التعاودية الساذجة.

الخاصية الثانية: البنية المثلى للمسائل الفرعية (Optimal Substructure)

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

مثال عملي يوضح الفكرة: متتالية فيبوناتشي

أفضل وأشهر مثال لشرح البرمجة الديناميكية هو متتالية فيبوناتشي. كل رقم هو مجموع الرقمين اللي قبله (…,1, 1, 2, 3, 5, 8). المعادلة هي: Fib(n) = Fib(n-1) + Fib(n-2).

الطريقة الساذجة: التعاودية البحتة (The Naive Recursive Way)

أول ما يفكر فيه أي مبرمج هو كتابة دالة تعاودية بسيطة تعكس المعادلة مباشرةً. بلغة بايثون مثلاً:


def fib(n):
    # Base cases (الحالات الأساسية)
    if n <= 1:
        return n
    # Recursive step (الخطوة التعاودية)
    return fib(n - 1) + fib(n - 2)

# خلينا نجرب نحسب فيبوناتشي للرقم 40
# print(fib(40)) # لا تجرب هذا في البيت! سيستغرق وقتاً طويلاً جداً

لماذا هذا الكود كارثي؟ لأنه لحساب fib(5) مثلاً، سيتم حساب fib(3) مرتين، و fib(2) ثلاث مرات، وهكذا. كلما زاد الرقم `n`، زاد التكرار بشكل أسي (Exponential). التعقيد الزمني هنا هو O(2^n)، وهو ما نسميه “جحيم التعقيد الأسي”.

مهمة الإنقاذ: تقنيات البرمجة الديناميكية

هنا يأتي دور أبطالنا. لدينا طريقتان لتطبيق البرمجة الديناميكية وحل هذه المشكلة.

1. التخزين المؤقت (Memoization) – النهج من الأعلى للأسفل (Top-Down)

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

نحن ببساطة نضيف “ذاكرة” (cache) للدالة التعاودية.


# الذاكرة لتخزين النتائج المحسوبة
memo = {} 

def fib_memo(n):
    # هل الرقم موجود في الذاكرة؟ إذا نعم، رجّع القيمة مباشرة
    if n in memo:
        return memo[n]
    
    # الحالات الأساسية
    if n <= 1:
        return n
    
    # إذا لم يكن في الذاكرة، احسبه
    result = fib_memo(n - 1) + fib_memo(n - 2)
    
    # ... ثم خزنه في الذاكرة للمستقبل
    memo[n] = result
    
    return result

# الآن حساب فيبوناتشي للرقم 40 سيحدث في لمح البصر
print(fib_memo(40))

هذا بالضبط ما فعلناه في مشروعنا القديم! بضعة أسطر من الكود حولت الخوارزمية من سلحفاة إلى فهد. التعقيد الزمني هنا تحسن بشكل مذهل ليصبح O(n) لأن كل قيمة فيبوناتشي من 0 إلى n يتم حسابها مرة واحدة فقط.

2. الجدولة (Tabulation) – النهج من الأسفل للأعلى (Bottom-Up)

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

الفكرة: “أنا أعرف الحلول للمشاكل الصغيرة جدًا (fib(0) و fib(1)). سأستخدمها لبناء حل المشكلة الأكبر قليلًا (fib(2))، ثم أستخدمه لبناء حل (fib(3))، وهكذا حتى أصل إلى الحل الذي أريده.”

هذه الطريقة زي اللي ببني عمارة، ببدأ من الأساسات وبيطلع طابق طابق.


def fib_tab(n):
    if n <= 1:
        return n
        
    # إنشاء جدول (أو قائمة في بايثون) لتخزين النتائج
    # حجم الجدول هو n+1 لنتمكن من تخزين fib(n)
    table = [0] * (n + 1)
    
    # وضع الحالات الأساسية في الجدول
    table[1] = 1
    
    # بناء الجدول من الأسفل للأعلى
    for i in range(2, n + 1):
        table[i] = table[i - 1] + table[i - 2]
        
    # النتيجة النهائية موجودة في آخر خانة في الجدول
    return table[n]

# سريعة جدًا وفعالة
print(fib_tab(40))

هذه الطريقة أيضًا تعقيدها الزمني O(n). يفضلها البعض لأنها لا تستخدم التعاودية (Recursion)، وبالتالي لا يوجد خطر من الوصول إلى أقصى عمق للتعاودية (Stack Overflow) في بعض اللغات للمدخلات الكبيرة جدًا.

نصائح أبو عمر العملية 💡

الورقة والقلم أصدق أنباءً من الكود أحيانًا. قبل أن تكتب سطرًا واحدًا، حاول حل المشكلة على ورقة لأرقام صغيرة. ارسم شجرة التعاود، أو ابنِ الجدول بنفسك. هذا سيعطيك فهمًا عميقًا للمشكلة لا يقدر بثمن.

  • ابدأ بالحل التعاودي الساذج: حتى لو كنت تعرف أنه بطيء، كتابة الحل التعاودي البسيط أولاً تساعدك على فهم هيكل المشكلة والمسائل الفرعية بشكل صحيح.
  • أضف التخزين المؤقت (Memoization): الانتقال من الحل التعاودي إلى حل الـ Memoization غالبًا ما يكون سهلًا ومباشرًا. إنه أسرع طريق للحصول على حل صحيح وفعال.
  • فكر في الجدولة (Tabulation) للتحسين: إذا واجهت مشاكل مع حدود التعاودية، أو أردت التخلص من الحمل الإضافي للدوال التعاودية (function call overhead)، قم بتحويل حلك إلى طريقة الجدولة.
  • حدد الحالة (State): في مشاكل البرمجة الديناميكية، أهم شيء هو تحديد “الحالة”. ما هي المعلومات التي تحتاجها لتصف مشكلة فرعية بشكل فريد؟ في فيبوناتشي، الحالة هي ببساطة الرقم `n`. في مشاكل أخرى (مثل مشكلة حقيبة الظهر)، قد تكون الحالة هي (الوزن المتبقي، العنصر الحالي). تحديد الحالة هو 90% من الحل.

الخلاصة… والزبدة

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

المبدأ بسيط: لا تحسب نفس الشيء مرتين!

سواء اخترت طريقة التخزين المؤقت (Memoization) أو الجدولة (Tabulation)، فإنك تطبق نفس المبدأ العبقري: قسّم، حل، وخزّن. هذا المبدأ أنقذنا في ذلك المشروع، ومن يومها أصبح واحدًا من أهم الأدوات في صندوق عدتي كمبرمج.

فلا تخافوا من مصطلح “البرمجة الديناميكية”، فهي ليست “بعبعًا”. هي مجرد أداة قوية تنتظر منكم أن تفهموها وتستخدموها لتبنوا برمجيات أسرع وأكثر كفاءة. بالتوفيق يا وحوش! 👍

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

​معمارية البرمجيات

كان تاريخ قراراتنا ضبابياً: كيف أنقذتنا ‘سجلات القرارات المعمارية’ (ADRs) من جحيم الأسئلة المتكررة؟

في عالم تطوير البرمجيات سريع الخطى، غالباً ما ننسى "لماذا" اتخذنا قراراً معمارياً معيناً. أشارككم تجربتي كـ "أبو عمر" وكيف أنقذتنا سجلات القرارات المعمارية (ADRs)...

19 مايو، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

كانت واجهاتنا خليطاً فوضوياً: كيف أنقذنا ‘نظام التصميم’ من جحيم عدم الاتساق؟

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

19 مايو، 2026 قراءة المزيد
برمجة وقواعد بيانات

كنا نعدل قاعدة البيانات يدوياً بخوف: كيف أنقذتنا ‘هجرات قواعد البيانات’ (Database Migrations) من جحيم التحديثات الفوضوية؟

أشارككم قصة من ليالي البرمجة الطويلة، وكيف انتقلنا من التعديل اليدوي المرعب لقواعد البيانات إلى عالم منظم وآمن بفضل "هجرات قواعد البيانات". مقالة لكل مبرمج...

19 مايو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

كان حسابي على GitHub مقبرة للمشاريع المنسية: كيف أنقذني ‘ملف README الشخصي’ من جحيم الانطباع الأول الباهت؟

أشارككم قصتي مع حسابي المهمل على GitHub وكيف تحولت صفحتي من مقبرة للمشاريع غير المكتملة إلى بطاقة تعريف احترافية تجذب الفرص. اكتشفوا معي قوة ملف...

19 مايو، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

كان فشل خدمة واحدة يُسقط النظام بأكمله: كيف أنقذنا نمط ‘قاطع الدائرة’ من جحيم الأعطال المتتالية؟

أشارككم قصة من قلب المعركة التقنية، عندما كان نظامنا ينهار بسبب عطل بسيط في إحدى الخدمات. سأشرح لكم بالتفصيل كيف أنقذنا نمط التصميم "قاطع الدائرة"...

19 مايو، 2026 قراءة المزيد
البودكاست