كانت حلولنا التعاودية (Recursive) تنسف موارد الخادم: كيف أنقذتنا ‘البرمجة الديناميكية’ من جحيم تكرار الحسابات؟

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

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

وهنا بدأت الدراما. في الاختبارات الأولية مع كم مستخدم، الأمور كانت ماشية. لكن لما عملنا اختبار ضغط يحاكي عدد كبير من المستخدمين… الكارثة! مؤشر استخدام المعالج (CPU) على الخادم نط للـ 100% وثبت هناك، والنظام كله صار أبطأ من سلحفاة مصابة بالزكام. رسائل الخطأ بدأت تظهر، والخادم صار يطلب الرحمة. قعدنا أنا والفريق، كاسات الشاي جنبنا، وبنحلل شو اللي بصير. بعد حفر وتنقيب في سجلات الأداء (Logs)، لقينا المتهم الرئيسي: دالتنا التعاودية البريئة! كانت بتستنزف كل موارد الخادم بشكل أُسّي، وكلما زاد عمق الطلب، زادت الكارثة.

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

ما هي المشكلة بالضبط؟ جحيم التعاودية (Recursion)

الحل التعاودي بحد ذاته ليس سيئًا، بل هو أنيق جدًا لحل المشاكل التي يمكن تقسيمها إلى مشاكل أصغر من نفس النوع. المشكلة الحقيقية تظهر عندما تكون هذه المشاكل الفرعية “متداخلة” (Overlapping Subproblems)، يعني إنك بتحل نفس المشكلة الفرعية مرارًا وتكرارًا بدون أي داعٍ.

مثال حي: سلسلة فيبوناتشي (Fibonacci)

أشهر مثال لشرح هذه الفكرة هو حساب أرقام فيبوناتشي. القاعدة بسيطة: كل رقم هو مجموع الرقمين اللي قبله (F(n) = F(n-1) + F(n-2)). الحل التعاودي المباشر يبدو هكذا:


function fibonacci(n) {
  if (n <= 1) {
    return n;
  }
  // المشكلة هنا! حسابات مكررة بشكل فظيع
  return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(7)); // يعمل بسرعة
console.log(fibonacci(45)); // اذهب لتحضير القهوة، قد يستغرق الأمر بعض الوقت...

لماذا هو بطيء جدًا عند الأرقام الكبيرة؟ لننظر لشجرة الاستدعاءات عند حساب fibonacci(5):

شجرة استدعاءات فيبوناتشي

لاحظ كم مرة قمنا بحساب fibonacci(2) أو fibonacci(3)؟ نحن نكرر نفس العمل مرارًا وتكرارًا. التعقيد الزمني لهذه الدالة هو O(2^n)، وهذا يعني أن الوقت اللازم للتنفيذ يتضاعف تقريبًا مع كل زيادة في `n`. هذا هو ما كان “ينسف” موارد خادمنا.

المنقذ وصل: البرمجة الديناميكية (Dynamic Programming)

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

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

لتطبيق البرمجة الديناميكية، يجب أن تتوافر في المشكلة خاصيتان:

  1. مشاكل فرعية متداخلة (Overlapping Subproblems): كما رأينا في مثال فيبوناتشي، نحن نحل نفس المشكلة الفرعية مرارًا وتكرارًا.
  2. بنية فرعية مثالية (Optimal Substructure): يعني أن الحل الأمثل للمشكلة الكلية يمكن بناؤه من الحلول المثلى لمشاكلها الفرعية.

هناك طريقتان رئيسيتان لتطبيق البرمجة الديناميكية، وهذا هو الجزء العملي والمفيد.

النهج الأول: التخزين المؤقت (Memoization) – أسلوب “من فوق لتحت” (Top-Down)

هذا هو النهج الذي أنقذنا في تلك الليلة. هو في الأساس نفس الحل التعاودي، ولكن مع إضافة “ذاكرة” أو “كاش” (Cache). الفكرة بسيطة جدًا:

  1. قبل أن تبدأ الحساب، تحقق من الذاكرة: “هل حسبت هذه القيمة من قبل؟”.
  2. إذا كانت موجودة، أرجعها مباشرة. لا تكمل.
  3. إذا لم تكن موجودة، قم بحسابها كالمعتاد.
  4. قبل أن تُرجع النتيجة الجديدة، قم بتخزينها في الذاكرة لتستخدمها في المستقبل.

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

مثال بالكود (مع الـ Memoization)

لنعدّل دالة فيبوناتشي. كل ما نحتاجه هو كائن أو مصفوفة لتكون بمثابة الذاكرة المؤقتة (الكاش).


// نمرر الكاش (memo) كـ object فارغ في البداية
function fibMemo(n, memo = {}) {
  // 1. هل النتيجة موجودة في الكاش؟
  if (n in memo) {
    return memo[n];
  }
  
  // القاعدة الأساسية للتعاود
  if (n <= 1) {
    return n;
  }
  
  // 2. إذا لم تكن موجودة، احسبها
  // 3. وخزنها في الكاش قبل إرجاعها
  memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
  
  return memo[n];
}

console.log(fibMemo(45)); // النتيجة تظهر فورًا! ⚡️

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

النهج الثاني: الجدولة (Tabulation) – أسلوب “من تحت لفوق” (Bottom-Up)

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

كيف تعمل؟

بالنسبة لفيبوناتشي، سنقوم ببناء “جدول” (عادة مصفوفة) بالنتائج من الأسفل للأعلى.

  1. نعرف أن fib(0) = 0 و fib(1) = 1. هذه هي حالاتنا الأساسية.
  2. ننشئ مصفوفة بحجم n+1.
  3. نضع القيم الأولية: table[0] = 0, table[1] = 1.
  4. نستخدم حلقة تكرارية لحساب باقي القيم: table[i] = table[i-1] + table[i-2] حتى نصل إلى table[n].

مثال بالكود (مع الـ Tabulation)


function fibTab(n) {
  if (n <= 1) {
    return n;
  }
  
  // 1. إنشاء الجدول
  const table = Array(n + 1);
  
  // 2. وضع القيم الأساسية
  table[0] = 0;
  table[1] = 1;
  
  // 3. بناء الجدول من تحت لفوق
  for (let i = 2; i <= n; i++) {
    table[i] = table[i - 1] + table[i - 2];
  }
  
  // النتيجة النهائية موجودة في آخر خانة بالجدول
  return table[n];
}

console.log(fibTab(45)); // سريع جدًا أيضًا!

هذا الحل أيضًا تعقيده الزمني O(n). في بعض الحالات، يكون أسرع قليلاً في التنفيذ الفعلي لأنه يتجنب الحمل الزائد (Overhead) الناتج عن استدعاءات الدوال التعاودية المتعددة.

نصائح من خبرة أبو عمر

  • متى أفكر بالبرمجة الديناميكية؟ ابحث عن إشارات في وصف المشكلة مثل: “أوجد العدد الأقصى/الأدنى”، “أوجد عدد الطرق الممكنة”، “هل يمكن الوصول إلى حل؟”، خاصة إذا كانت المشكلة يمكن تجزئتها. إذا شعرت أنك ستحتاج لحل نفس المشكلة الفرعية أكثر من مرة، فهذه إشارة قوية.
  • Memoization أم Tabulation؟ بصراحة، أنا شخصيًا أبدأ بالتفكير بالحل التعاودي أولاً، لأنه غالبًا ما يكون أقرب لطريقة تفكيرنا الطبيعية. ثم أضيف طبقة الـ Memoization. هذا الأسلوب أراه أسهل للمبتدئين. الـ Tabulation قد يكون أقل استهلاكًا للذاكرة في بعض الحالات (لأنه لا يملأ مكدس الاستدعاءات – Call Stack)، ولكنه يتطلب منك التفكير في ترتيب الحسابات بشكل دقيق من البداية.
  • لا تخف من التعقيد: في البداية، تبدو مسائل البرمجة الديناميكية مرعبة. أفضل نصيحة أقدمها لك هي: “امسك ورقة وقلم يا خبير!”. حاول حل المشكلة يدويًا لقيم صغيرة (n=2, 3, 4). راقب كيف تستخدم النتائج السابقة لبناء النتائج الجديدة. هذا سيساعدك على اكتشاف “العلاقة التعاودية” (Recurrence Relation) التي هي مفتاح الحل.
  • الكاش هو صديقك: في أنظمة الويب الحديثة، التخزين المؤقت (Caching) هو كل شيء، سواء على مستوى قاعدة البيانات، أو الـ API، أو حتى داخل دالة واحدة كما رأينا. تعلم كيف وأين تستخدم الكاش، وستوفر على نفسك وعلى خوادمك الكثير من الألم.

الخلاصة: لا تكرر حساباتك مرتين! 🧘‍♂️

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

تذكر دائمًا، المبرمج الشاطر ليس فقط من يكتب كودًا يعمل، بل من يكتب كودًا يعمل بكفاءة ولا يستهلك الموارد بلا داعٍ. بالتوفيق يا جماعة!

أبو عمر

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

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

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

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

آخر المدونات

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

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

هل سئمت من البحث في سجلات Slack لتفهم قرارًا تقنيًا تم اتخاذه قبل أشهر؟ في هذه المقالة، أشارككم كأبو عمر، مطور فلسطيني، كيف أنقذتنا سجلات...

30 أبريل، 2026 قراءة المزيد
الحوسبة السحابية

فاتورتنا السحابية لغز محير: كيف أنقذتنا ‘وسوم تخصيص التكلفة’ من جحيم ‘من أنفق كل هذا؟’

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

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

كانت قاعدة بياناتنا تستغيث: كيف أنقذنا نمط ‘Cache-Aside’ من جحيم اختناق القراءات؟

أشارككم قصة حقيقية عن يوم كادت فيه قاعدة بياناتنا أن تنهار تحت ضغط القراءات الهائل. اكتشف كيف أنقذنا الموقف باستخدام نمط التخزين المؤقت البسيط والفعال...

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