مرحبًا
لقد قمت مؤخرًا بحل المشاكل من أرشيف
Timus Online Judge ووجدت قسمًا يسمى
مهام البرمجة الديناميكية . هذا النوع من المهام له أهمية خاصة بالنسبة لي ، لأنه غالبًا ما يضمن هذا النهج سرعة وأناقة الحل. ما هي البرمجة الديناميكية؟
البرمجة الديناميكية هي نهج لحل المشكلات حيث يوجد تقسيم إلى مهام فرعية "أبسط" مقارنة بالمهمة الأصلية. كلمة "ديناميكي" قريبة من معنى "حثي": من المفترض أن الإجابة معروفة لبعض المعنى
، وأريد العثور على إجابة
. في الرياضيات ، يسمى هذا الانتقال الاستقرائي ، وهو الفكرة الرئيسية للبرمجة الديناميكية.
أمثلة بسيطة
إن المهمة الأبرز والأكثر دلالة هي مهمة الحوسبة
رقم تسلسل فيبوناتشي.
من المعروف أن التسلسل له الخصائص التالية:
هذا يعني على الفور صيغة التكرار:
int Fibonacci(int n) { if(n == 1 || n == 2) return 1; return Fibonacci(n-1) + Fibonacci(n-2); }
إذا كان العودية يبحث عن رقم "من النهاية" ، عندئذٍ تحسب الطريقة التالية بالتتابع جميع الأرقام الموجودة بين
و
:
int dpFibonacci(int n) { int prev1 = 1; int prev2 = 1; int curr = 0; for(int j = 2; j < n; j++) { curr = prev1 + prev2; prev1 = prev2; prev2 = curr; } return curr; }
من الواضح أنه كبير بما فيه الكفاية
تعمل هذه الخوارزمية بشكل أسرع: لا تحسب القيم الوسيطة عدة مرات. فكر في مثال أكثر تعقيدًا قليلاً.
مثال 1. أنت تمشي على سلم رسوم. للمضي قدما
الخطوة التي عليك دفعها
عملات معدنية. يمكنك الانتقال إلى الخطوة التالية أو القفز فوق واحدة. المهمة: لتمرير
الخطوات وإنفاق أقل عدد ممكن من العملات المعدنية.
من الواضح أنه عند تجاوز كل خطوة ، نقوم بتقليل عدد "المدفوعات" إلى أدنى حد ، ولكن يمكننا أن نواجه خطوة باهظة الثمن ، والتي نود تجنبها. قم بإنشاء مجموعة من القيم
فيه يوم
- سيكون المكان هو (الحد الأدنى) عدد العملات التي يجب إنفاقها للوصول إليها
الخطوة ال. من الواضح على الفور أن
. وبعد ذلك سنتخذ الحد الأدنى من الخطوتين السابقتين ونضيف تكلفة الخطوة نفسها:
نغير شروط المشكلة قليلاً: لنفترض أنه في بعض الخطوات يمكنك الحصول على عملات معدنية (وهذا يعني ذلك
) ما الذي يجب تغييره في الخوارزمية بحيث تعطي النتيجة الصحيحة؟
الحلمن الضروري فقط تغيير "بداية" دينامياتنا. إذا لم يجلب لنا الدرج الأول عملات معدنية ، فمن المستحسن القفز فوقه ، إذا ، من الأفضل أن تخطو وتجمع عملاتك المعدنية. لذا .
لنأخذ مثالاً آخر يستخدم ديناميكيات "ثنائية الأبعاد".
مثال 2. يوجد في المتاهة
غرف ، كل منها يحتوي على الذهب (في قفص
الأكاذيب
ذهب). تتمثل المهمة في تحديد الحد الأقصى لكمية الذهب التي يمكن جمعها بالطريق الأمثل من نقطة
إلى هذه النقطة
إذا كنت تستطيع النزول أو إلى اليمين.
لذا ، نريد أن نعرف أفضل طريق للخلية
. يمكننا الوصول إلى هنا من خليتين -
و
. بالنظر إلى أن المسارات المثلى لهاتين الخليتين معروفة (يتم تخزينها في بعض الجدول
) ، ثم إجابة الخلية
تم الحصول عليها على النحو التالي:
هذه مهمة برمجة ديناميكية كلاسيكية أخرى ، التعديلات شائعة جدًا في مهام البرمجة الرياضية. يتم شرح مهمة مماثلة بمزيد من التفصيل
هنا .
مهام أكثر صعوبة
إذا رغبت في ذلك ، يمكن ثمل نهج ديناميكي أينما تريد. النظر في
مهمة من أرشيف Timus Online Judge.
الصيغة الرياضية للمشكلة هي كما يلي: مطلوب العثور على الحد الأدنى لعدد المصطلحات اللازمة لتحليل رقم معين في مربعات كاملة.
كما كان من قبل ، افترض أننا نعرف إجابات جميع الأرقام
التي يتم تخزينها في بعض الصفيف
ونود أن نجد
.
خذ هذا الرقم
وتحليل الحالات التي قد تكون:
- مربع كامل. في هذه الحالة .
- ربما الرقم السابق كان مربع كامل. ثم .
بشكل عام ، لا يبدو خيار إضافة وحدة إلى الوحدة السابقة سيئًا للغاية.
نمضي على النحو التالي: نسعى إلى التحلل
مثل هذا
منذ ذلك الحين
- مربع كامل ثم
و
أي أننا وجدنا قسمًا أفضل ببساطة من
، والجواب في هذه الحالة سيكون
نموذج كود Java الذي يقوم بتنفيذ هذه الخوارزمية: for(int k = 1; k <= n; k++) { int best = d[k - 1] + 1;
خذ بعين الاعتبار
المشكلة التالية. الهدف هو بناء درج من
مكعبات حسب القواعد:
- الدرج له خطوتين على الأقل ؛
- لا يمكن أن يكون للسلالم خطوتين متطابقتين ؛
- خطوات السلالم تصاعديًا (أي أن الدرج التالي أكبر من السابق).
هذه المرة سوف نبني ديناميكيات ثنائية الأبعاد. قم بإنشاء جدول
فيها الموقف
عدد السلالم المكونة من
مكعبات لا يتجاوز ارتفاعها
. إذا نجحت ، فسيكون الجواب لمشكلتنا هو المجموع
لذا سنحل مشكلة إيجاد عدد السلالم المكونة لها
مكعبات طويلة
. تظهر الصورة السلالم التي تقع في
:

نظرًا لأننا نعرف جميع السلالم ، التي تتكون من عدد أقل من المكعبات ، فسنقوم "بتقسيم" الدرج
العمود الأيمن. والنتيجة سلم ج
مكعبات. مثال ل
:

ولكن بالنسبة لهذه السلالم ، فإن النتيجة معروفة بالفعل ، لذلك سنقوم بفرز جميع هذه السلالم مع دورة فيها
واجمع كل النتائج. بهذه الطريقة
الآن سنقوم بفرز ارتفاعات السلالم:
أخيرا ، يتغير
من
من قبل
نحصل على الجواب.
هام : في عملية بناء المصفوفة الخاصة بنا ، من الضروري النظر
، لأنه بخلاف ذلك سيتم "فقدان" بعض أنواع الدرج (عند "الانقسام") ، ولكن من نافلة القول أن مثل هذا الدرج لا يفي بشروط المشكلة ، لذلك سيكون الجواب هو الرقم
.
نموذج كود Java الذي يقوم بتنفيذ هذه الخوارزمية: dp = new long[n + 1][n+1]; d[1][1] = 1; d[2][1] = 0; d[2][2] = 1; for(int i = 3; i < n + 1; i++) { for(int j = 2; j <i; j++) { long cnt = 0; for(int k = 1; k < j; k++) { cnt += d[i - j][k]; } d[i][j] = cnt; } d[i][i] = 1;
يتم حل
المهمة التالية باستخدام صفيف أحادي البعد.
لذا ما لدينا. يعرف المدخل الأول كلمتين. كل علم يعلم كل الكلمات التي يعرفها بنفسه بشرتين وشيوخ. في المقابل ، تم تعليم الصغار عدد الكلمات التي يعرفها بالفعل ، وتم تعليم القديم كلمة واحدة فقط. أنت بحاجة إلى معرفة عدد النمل الذين يعرفون بالضبط
الكلمات (من الضروري استنتاج عدد هذه الوصفات
)
الحل بسيط للغاية. قم بإنشاء مصفوفة
فيه يوم
-المكان الذي سنقوم بتخزين عدد الايجابيات (مودولو
) الذين يعرفون
الكلمات. يبدأ كل شيء مع ent الأول ، الذي يعرف كلمتين ، لذلك
. ثم كل شيء بسيط:
- جميع الأهل الذين يعرفون عددًا فرديًا من الكلمات قديمة ويمكنهم التعلم فقط من الكلمات السابقة. لذلك للغريب
- بالنسبة للأهل الذين يعرفون عددًا زوجيًا من الكلمات ، فإنهم جميعًا الذين تلقوا نفس عدد الكلمات من الجان (صغار) أولئك الذين تعلموا من السابق (القديم) ؛ أي حتى لدينا .
يبقى للتعامل مع نموذج الحساب. حتى لا نخزن أعدادًا ضخمة ، سنتذكر فورًا جميع القيم المعيارية.
نموذج كود Java الذي يقوم بتنفيذ هذه الخوارزمية: int[] d = new int[K + 1]; if(K >= 2) d[2] = 1; if(P != 1) { for(int i = 3; i <= K; i++) { if(i % 2 != 0) { d[i] = d[i - 1]; } else { d[i] = ((d[i/2] % P) + d[i - 1] % P) % P; } } } else d[K] = 0;
الموارد المستخدمة:
- قاضي Timus Online ؛
- قليلا عن البرمجة الديناميكية.
- modulo خصائص المقارنة.