البرمجة الديناميكية في مشاكل الأولمبياد

مرحبًا

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

البرمجة الديناميكية هي نهج لحل المشكلات حيث يوجد تقسيم إلى مهام فرعية "أبسط" مقارنة بالمهمة الأصلية. كلمة "ديناميكي" قريبة من معنى "حثي": من المفترض أن الإجابة معروفة لبعض المعنى k، وأريد العثور على إجابة k+1. في الرياضيات ، يسمى هذا الانتقال الاستقرائي ، وهو الفكرة الرئيسية للبرمجة الديناميكية.

أمثلة بسيطة


إن المهمة الأبرز والأكثر دلالة هي مهمة الحوسبة nرقم تسلسل فيبوناتشي. من المعروف أن التسلسل له الخصائص التالية:

F0=F1=1، Fn=Fn1+Fn2.


هذا يعني على الفور صيغة التكرار:

int Fibonacci(int n) { if(n == 1 || n == 2) return 1; return Fibonacci(n-1) + Fibonacci(n-2); } 

إذا كان العودية يبحث عن رقم "من النهاية" ، عندئذٍ تحسب الطريقة التالية بالتتابع جميع الأرقام الموجودة بين 0و n:

 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; } 

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

مثال 1. أنت تمشي على سلم رسوم. للمضي قدما iالخطوة التي عليك دفعها aiعملات معدنية. يمكنك الانتقال إلى الخطوة التالية أو القفز فوق واحدة. المهمة: لتمرير nالخطوات وإنفاق أقل عدد ممكن من العملات المعدنية.

من الواضح أنه عند تجاوز كل خطوة ، نقوم بتقليل عدد "المدفوعات" إلى أدنى حد ، ولكن يمكننا أن نواجه خطوة باهظة الثمن ، والتي نود تجنبها. قم بإنشاء مجموعة من القيم dفيه يوم j- سيكون المكان هو (الحد الأدنى) عدد العملات التي يجب إنفاقها للوصول إليها jالخطوة ال. من الواضح على الفور أن d1=a1، d2=a2. وبعد ذلك سنتخذ الحد الأدنى من الخطوتين السابقتين ونضيف تكلفة الخطوة نفسها:

di= min left(di1،di2 right)+ai.



نغير شروط المشكلة قليلاً: لنفترض أنه في بعض الخطوات يمكنك الحصول على عملات معدنية (وهذا يعني ذلك ak<0) ما الذي يجب تغييره في الخوارزمية بحيث تعطي النتيجة الصحيحة؟

الحل
من الضروري فقط تغيير "بداية" دينامياتنا. إذا لم يجلب لنا الدرج الأول عملات معدنية ، فمن المستحسن القفز فوقه ، إذا a1<0، من الأفضل أن تخطو وتجمع عملاتك المعدنية. لذا d2= min left(0،d1 right)+a2.

لنأخذ مثالاً آخر يستخدم ديناميكيات "ثنائية الأبعاد".

مثال 2. يوجد في المتاهة n مراتمغرف ، كل منها يحتوي على الذهب (في قفص (i،j)الأكاذيب aijذهب). تتمثل المهمة في تحديد الحد الأقصى لكمية الذهب التي يمكن جمعها بالطريق الأمثل من نقطة (0،0)إلى هذه النقطة (n،m)إذا كنت تستطيع النزول أو إلى اليمين.

لذا ، نريد أن نعرف أفضل طريق للخلية (i،j). يمكننا الوصول إلى هنا من خليتين - (i1،j)و (i،j1). بالنظر إلى أن المسارات المثلى لهاتين الخليتين معروفة (يتم تخزينها في بعض الجدول d) ، ثم إجابة الخلية (i،j)تم الحصول عليها على النحو التالي:

dij= max left(di1j،dij1 right)+aij.


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

مهام أكثر صعوبة

إذا رغبت في ذلك ، يمكن ثمل نهج ديناميكي أينما تريد. النظر في مهمة من أرشيف Timus Online Judge.

الصيغة الرياضية للمشكلة هي كما يلي: مطلوب العثور على الحد الأدنى لعدد المصطلحات اللازمة لتحليل رقم معين في مربعات كاملة.

كما كان من قبل ، افترض أننا نعرف إجابات جميع الأرقام k1التي يتم تخزينها في بعض الصفيف dونود أن نجد dk.

خذ هذا الرقم kوتحليل الحالات التي قد تكون:

  1. kمربع كامل. في هذه الحالة dk=1.
  2. ربما الرقم السابق k1كان مربع كامل. ثم dk=dk1+1.

بشكل عام ، لا يبدو خيار إضافة وحدة إلى الوحدة السابقة سيئًا للغاية.

نمضي على النحو التالي: نسعى إلى التحلل k=q2+sمثل هذا

dq2+ds<dk1+1.

منذ ذلك الحين q2- مربع كامل ثم dq2=1و

ds<dk1،

أي أننا وجدنا قسمًا أفضل ببساطة من dk1+1، والجواب في هذه الحالة سيكون

dk=ds+dq2=ds+1.



نموذج كود Java الذي يقوم بتنفيذ هذه الخوارزمية:
 for(int k = 1; k <= n; k++) { int best = d[k - 1] + 1; //      int q = 1; while(k - q*q >= 0) { // k = q*q + s if(k - q*q == 0) { // k -   best = 1; break; } else if(d[k - q*q] < best) best = d[k - q*q] + 1; q++; } d[k] = best; } 


خذ بعين الاعتبار المشكلة التالية. الهدف هو بناء درج من Nمكعبات حسب القواعد:

  1. الدرج له خطوتين على الأقل ؛
  2. لا يمكن أن يكون للسلالم خطوتين متطابقتين ؛
  3. خطوات السلالم تصاعديًا (أي أن الدرج التالي أكبر من السابق).

هذه المرة سوف نبني ديناميكيات ثنائية الأبعاد. قم بإنشاء جدول dفيها الموقف (i،j)عدد السلالم المكونة من iمكعبات لا يتجاوز ارتفاعها j. إذا نجحت ، فسيكون الجواب لمشكلتنا هو المجموع

 sum limitsj=1ndnj.


لذا سنحل مشكلة إيجاد عدد السلالم المكونة لها iمكعبات طويلة j. تظهر الصورة السلالم التي تقع في d74:

نظرًا لأننا نعرف جميع السلالم ، التي تتكون من عدد أقل من المكعبات ، فسنقوم "بتقسيم" الدرج (i،j)العمود الأيمن. والنتيجة سلم ج ijمكعبات. مثال ل i=9، j=4:

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

dij= sum limitsk=1j1dijk.


الآن سنقوم بفرز ارتفاعات السلالم:

dij= sum limitsk=1j1dijk، j= overline1،i.

أخيرا ، يتغير iمن 1من قبل nنحصل على الجواب.

هام : في عملية بناء المصفوفة الخاصة بنا ، من الضروري النظر dii=1، لأنه بخلاف ذلك سيتم "فقدان" بعض أنواع الدرج (عند "الانقسام") ، ولكن من نافلة القول أن مثل هذا الدرج لا يفي بشروط المشكلة ، لذلك سيكون الجواب هو الرقم dnn1.

نموذج كود 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; //    } long answer = 0L; for(int i = 0; i <= n; i++) { answer += d[n][i]; } answer--; //     


يتم حل المهمة التالية باستخدام صفيف أحادي البعد.

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

الحل بسيط للغاية. قم بإنشاء مصفوفة dفيه يوم i-المكان الذي سنقوم بتخزين عدد الايجابيات (مودولو P) الذين يعرفون iالكلمات. يبدأ كل شيء مع ent الأول ، الذي يعرف كلمتين ، لذلك d2=1. ثم كل شيء بسيط:

  • جميع الأهل الذين يعرفون عددًا فرديًا من الكلمات قديمة ويمكنهم التعلم فقط من الكلمات السابقة. لذلك للغريب i: di=di1؛
  • بالنسبة للأهل الذين يعرفون عددًا زوجيًا من الكلمات ، فإنهم جميعًا الذين تلقوا نفس عدد الكلمات من الجان (صغار) +أولئك الذين تعلموا من السابق (القديم) ؛ أي حتى iلدينا di=di backslash2+di1.

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

نموذج كود 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; 


الموارد المستخدمة:

  1. قاضي Timus Online ؛
  2. قليلا عن البرمجة الديناميكية.
  3. modulo خصائص المقارنة.

Source: https://habr.com/ru/post/ar418867/


All Articles