باز کردن منو اصلی

برنامه‌نویسی پویا

(تغییرمسیر از برنامه‌ریزی پویا)

در علوم رایانه و ریاضیات، برنامه‌ریزی پویا یا داینامیک روشی کارآمد برای حل مسائل جستجو و بهینه‌سازی با استفاده از دو ویژگی زیرمسئله‌های هم‌پوشان و زیرساخت‌های بهینه است. بر خلاف برنامه‌ریزی خطی، چارچوب استانداردی برای فرموله کردن مسائل برنامه‌ریزی پویا وجود ندارد. در واقع، آنچه برنامه‌ریزی پویا انجام می‌دهد ارائه روش برخورد کلی جهت حل این نوع مسائل است. در هر مورد، باید معادلات و روابط ریاضی مخصوصی که با شرایط آن مسئله تطبیق دارد نوشته شود.[۱]

تاریخچهویرایش

این روش در سال ۱۹۵۳ توسط ریاضی‌دانی به نام ریچارد بلمن معرفی شد. برنامه‌ریزِی پویا در ریاضی و علوم رایانه روشی شناخته شده‌است که از آن در نوشتن الگوریتم‌های بهینه با استفاده از حذف اجرای چند بارهٔ یک زیر مسئله یکسان استفاده می‌شود. تعریف برنامه ریزی پویا در ریاضی و علوم رایانه متفاوت است. نشان داده شده‌است که روش علوم رایانه‌ای برای برنامه ریزی پویا کارایی بالاتری دارد زیرا محاسبات تکراری را حذف می‌کند در حالی که در روش ریاضی برنامه ریزی پویا امکان کاهش فضای حافظه بیشتر است.

برنامه‌ریزی پویا شبیه روش تقسیم و حل، مسائل را با استفاده از ترکیب کردن جواب زیرمسئله‌ها حل می‌کند. الگوریتم تقسیم و حل، مسئله را به زیر مسئله‌های مستقل تقسیم می‌کند و پس از حل زیر مسئله‌ها به صورت بازگشتی، نتایج را با هم ترکیب کرده و جواب مسئله اصلی را بدست می‌آورد. به عبارت دقیق تر، برنامه ریزی پویا در مواردی قابل استفاده است که زیرمسأله‌ها مستقل نیستند؛ یعنی زمانی که زیرمسأله‌ها ،خود دارای چندین زیر مسئله‌ي یکسان هستند. در این حالت روش تقسیم و حل با اجرای مکرر زیرمسأله‌های یکسان، بیشتر از میزان لازم محاسبات انجام می‌دهد. یک الگوریتم برنامه ریزی پویا زیرمسأله‌ها را یک بار حل و جواب آن‌ها را در یک جدول ذخیره می‌کند و با این کار از تکرار اجرای زیرمسأله‌ها در زمانی که مجدداً به جواب آن‌ها نیاز است جلوگیری می‌کند.

ویژگی‌هاویرایش

یک مسئله باید دارای دو مشخصهٔ کلیدی باشد تا بتوان برنامه‌نویسی پویا را برای آن به کار برد: زیرساختار بهینه و زیرمسئله‌های هم‌پوشان.[۲] در طرف مقابل، به حل یک مسئله با ترکیب جواب‌های بهینهٔ زیرمسئله‌های ناهم‌پوشان، «تقسیم و حل» گفته‌می‌شود. به همین علت است که مرتب‌سازی ادغامی و سریع به عنوان مسائل برنامه‌نویسی پویا شناخته‌نمی‌شوند.

اصل بهینگیویرایش

اگر بنا باشد پرانتزبندی کل عبارت بهینه شود، پرانتزبندی زیرمسئله‌ها هم باید بهینه باشند. یعنی بهینه بودن مسئله، بهینه بودن زیرمسئله‌ها را ایجاب می‌کند. پس می‌توان از روش برنامه‌نویسی پویا استفاده کرد.

حل بهینه، سومین مرحله از بسط یک الگوریتم برنامه‌نویسی پویا برای مسائل بهینه‌سازی است. مراحل بسط چنین الگوریتمی به شرح زیر است:

  1. ارائه یک ویژگی بازگشتی که حل بهینهٔ نمونه‌ای از مسئله را به دست می‌دهد.
  2. محاسبه مقدار حل بهینه به شیوهٔ جزء به کل.
  3. بنا کردن یک حل نمونه به شیوهٔ جزء به کل.

تمام مسائل بهینه‌سازی را نمی‌توان با برنامه‌نویسی پویا حل کرد چرا که باید اصل بهینگی در مسئله صدق کند.

اصل بهینگی در یک مسئله صدق می‌کند اگر یک حل بهینه برای نمونه ای از مسئله، همواره حاوی حل بهینه برای همهٔ زیر نمونه‌ها باشد.

در روش برنامه‌نویسی پویا اغلب از یک آرایه برای ذخیره نتایج جهت استفاده مجدد استفاده شده، و زیرمسائل به صورت جزء به کل حل می‌شوند. در مورد این مسئله، ابتدا زیرمسائلی که تنها از دو ماتریس تشکیل شده‌اند محاسبه می‌شوند. سپس زیرمسائلی که از سه ماتریس تشکیل شده‌اند محاسبه می‌شوند. این دسته از زیرمسائل به زیرمسائلی متشکل از دو ماتریس تجزیه می‌شوند که قبلاً محاسبات آن‌ها صورت گرفته، و نتایج آن‌ها در آرایه ذخیره شده‌اند. در نتیجه نیاز به محاسبه مجدد آن‌ها نیست. به همین ترتیب در مرحله بعد زیرمسائل با چهار ماتریس محاسبه می‌شوند، و الی آخر.

درختهای جستجوی دودویی بهینه

درخت جستجوی دودویی یک درخت دودویی از عناصر است که معمولاً کلید نامیده می‌شوند به قسمی که:

  1. هر گره حاوی یک کلید است.
  2. کلیدهای موجود در زیردرخت چپ هر گره، کوچکتر یا مساوی کلید آن گره هستند.
  3. کلیدهای موجود در زیردرخت راست هر گره، بزرگتر یا مساوی کلید آن گره هستند

زیرسازه بهینهویرایش

زیرساختار بهینه به این معناست که جواب یک مسئلهٔ بهینه‌سازی را بتوان از ترکیب جواب‌های بهینهٔ زیرمسئله‌های آن به دست آورد. چنین زیرساختارهای بهینه‌ای معمولاً با رویکرد بازگشتی به دست می‌آیند؛ برای مثال، در گراف (G=(V,E، کوتاه‌ترین مسیر ِ م از رأس آ به رأس ب، زیرساختار بهینه از خود نشان می‌دهد: هر رأس میانیِ ث از مسیرِ م را در نظر بگیرید. اگر م واقعاً کوتاه‌ترین مسیر باشد، آنگاه می‌توان آن را به دو زیرمسیر م۱ از آ تا ث و م۲ از ث تا ب در نظر گرفت، به نحوی که هر کدام از این‌ها کوتاه‌ترین مسیر بین دو سر خود هستند (به همان طریق برش و چسباندن کتاب مقدمه‌ای بر الگوریتم‌ها). بر این اساس، می‌توان جواب را به صورت بازگشتی بیان کرد، درست مانند الگوریتم‌های بلمن-فورد و فلوید-وارشال.[۳]

استفاده از روش زیرسازه بهینه به این معناست که مسئله را به زیرمسئله‌هایی کوچک‌تر بشکنیم و برای هر یک از این زیرمسئله‌ها پاسخی بهینه بیابیم و پاسخ بهینه مسئلهٔ کلی را از کنار هم قرار دادن این پاسخ‌های بهینهٔ جزئی به دست آوریم. مثلاً در هنگام حل مسئلهٔ یافتن کوتاه‌ترین مسیر از یک رأس یک گراف به رأسی دیگر، می‌توانیم کوتاه‌ترین مسیر تا مقصد از همهٔ رئوس مجاور را به دست آوریم و از آن برای جواب کلی استفاده کنیم. به‌طور کلی حل مسئله از این روش شامل سه مرحله است.

  1. شکستن مسئله به بخش‌های کوچک‌تر
  2. حل خود این زیرمسئله‌ها با شکاندن آن‌ها به صورت بازگشتی
  3. استفاده از پاسخ‌های جزئی برای یافتن پاسخ کلی

زیرمسئله‌های هم‌پوشانویرایش

زیرمسئله‌های هم‌پوشان به معنای کوچک بودن فضای زیرمسئله‌هاست، به این معنا که هر الگوریتم بازگشتی برای حل این مسئله، باید به جای ایجاد زیرمسئله‌های جدید، زیرمسئله‌های تکراری را بارها حل کند. برای مثال، به فرمول بازگشی دنبالهٔ فیبوناچی دقت کنید: Fi = Fi−1 + Fi−2، با حالات پایهٔ F1 = F2 = 1. آنگاه F43 = F42 + F41، و F42 = F41 + F40. اکنون F41 در زیردرخت‌های هر دوی F43 و F42 محاسبه می‌شود. در صورت اتخاذ چنین رویکرد ساده‌انگارانه‌ای، نهایتاً زیرمسئله‌های یکسانی را بارها حل می‌کنیم، در صورتی که تعداد کل زیرمسئله‌ها در واقعیت کم است (تنها ۴۳تا). برنامه‌نویسی پویا به این حقیقت دقت می‌کند و هر زیرمسئله را تنها یک بار حل می‌کند.[۴]

گفته می‌شود مسئله‌ای دارای زیرمسئله‌های هم‌پوشان است، اگر بتوان مسئله را به زیرمسئله‌های کوچکتری شکست که پاسخ هرکدام چند بار در طول فرایند حل مورد استفاده قرار بگیرد.[۵] برنامه‌ریزی پویا کمک می‌کند تا هر کدام از این پاسخ‌ها فقط یک بار محاسبه شوند و فرایند حل از بابت دوباره‌کاری هزینه‌ای را متحمل نشود. برای مثال در دنباله فیبوناچی برای محاسبهٔ عدد چهارم دنباله به دانستن عدد سوم نیاز داریم. برای محاسبهٔ عدد پنجم هم باز به عدد سوم نیاز داریم. حال اگر مثلاً در شرایطی بخواهیم عدد ششم دنبالهٔ فیبوناچی را حساب کنیم، در این محاسبه هم مقدار عدد پنجم را می‌خواهیم و هم مقدار عدد چهارم را. اگر تصمیم بگیریم اعداد چهارم و پنجم را به نوبت حساب کنیم در هنگام محاسبهٔ هرکدام به مقدار عدد سوم نیاز پیدا می‌کنیم و باید دوباره آن را محاسبه کنیم. برای جلوگیری از این محاسبات چندباره، الگوریتمهایی که مبتنی بر برنامه‌ریزی پویا هستند، معمولاً یکی از دو راه زیر را استفاده می‌کنند.

  • رویکرد بالا به پایین: راه درروی مستقیم از صورت‌بندی بازگشتی هر مسئله‌ای است. اگر جواب مسئله‌ای را بتوان به صورت بازگشتی با جواب زیرمسئله‌های آن به دست آورد، و در صورت هم‌پوشانی زیرمسئله‌ها، می‌توان جواب زیرمسئله‌ها را در یک جدول به خاطر سپرد. هر گاه که برای حل یک زیرمسئله اقدام می‌کنیم، ابتدا بررسی می‌کنیم که آیا این زیرمسئله قبلاً حل شده یا نه؛ اگر جواب آن را داشتیم، می‌توانیم آن را مستقیماً استفاده کنیم؛ در غیر این صورت، زیرمسئله را حل می‌کنیم و جواب آن را به جدول اضافه می‌کنیم. در این رویکرد مسئله به زیرمسئله‌هایی شکسته می‌شود و پاسخ هر زیرمسئله پس از محاسبه در جایی ذخیره می‌شود. در مراحل بعدی هر وقت به آن پاسخ نیاز بود پاسخ از روی حافظه خوانده می‌شود. این فرایند ترکیبی از الگوریتم بازگشتی و ذخیره‌سازی در حافظه است.
  • رویکرد پایین به بالا: پس از آن که جواب یک مسئله را به صورت بازگشتی با استفاده از زیرمسئله‌های آن صورت‌بندی کردیم، می‌توانیم مسئله را از پایین به بالا نگاه کنیم: ابتدا زیرمسئله‌ها را حل می‌کنیم و سپس جواب آن‌ها را برای به دست آوردن جواب زیرمسئله‌های بزرگ‌تر استفاده می‌کنیم تا نهایتاً به مسئلهٔ اصلی برسیم. این روش نیز معمولاً به کمک یک جدول با تولید مرحله به مرحلهٔ زیرمسئله‌های بزرگ‌تر و بزرگ‌تر به کمک جواب زیرمسئله‌های کوچک‌تر انجام می‌شود؛ برای مثال، اگر مقادیر F41 و F40 را بدانیم، می‌توانیم مستقیماً مقدار F42 را به دست آوریم. در این رویکرد همهٔ زیرمسئله‌های مورد نیاز از کوچک به بزرگ حل می‌شوند و از جواب‌ها "بلافاصله" برای محاسبهٔ بعدی‌ها استفاده می‌شود و فرایند محاسبه تا رسیدن به زیرمسئلهٔ مورد نیاز (که در واقع مسئلهٔ اصلی ماست) ادامه می‌یابد. بدیهی است که در این حالت استفاده از الگوریتم بازگشتی ضروری نیست. مثال زیر این تفاوت‌ها را روشن‌تر می‌کند. برخی از زبان‌های برنامه‌نویسی می‌توانند به طور خودکار جواب صدا زدن یک تابع با ورودی‌های مشخص را به خاطر بسپارند تا صدا زدن با نام را سرعت ببخشند (این فرایند با نام صدا زدن با نیاز شناخته‌می‌شود). برخی زبان‌ها به شکل سیار این امکان را در اختیار برنامه‌نویس قرار می‌دهند (مانند Scheme ،Common Lisp ،[۶]Perl و D). برخی زبان‌های نیز به صورت خودکار به‌خاطرسپاری را در خود دارند: مانند Prolog جدول‌دار و J، که به‌خاطرسپاری را با قید .M پشتیبانی می‌کند.[۷] در هر صورت، این تنها برای یک تابع با شفافیت ارجاعی امکان دارد. به‌خاطرسپاری به عنوان یک الگوی طراحی در دسترس نیز در زبان‌های بازنویسی جملات مانند زبان ولفرام یافت‌می‌شود.

گراف زیرمسئله‌هاویرایش

 
تصویر ۱. گراف زیرمسئله‌های دنبالهٔ فیبوناچی. درخت نبودن این گراف نشان‌دهندهٔ زیرمسئله‌های هم‌پوشان است.

زمانی که به یک مسئلهٔ برنامه‌نویسی پویا می‌اندیشیم، باید مجموعهٔ زیرمسئله‌های موجود و چگونگی وابستگی آن‌ها را درک کنیم.

گراف زیرمسئله‌ها دقیقاً همین اطلاعات را برای یک مسئله در بر می‌گیرد. تصویر ۱ گراف زیرمسئله‌های دنباله‌های فیبوناچی را نشان می‌دهد. گراف زیرمسئله‌ها یک گراف جهت‌دار، شامل یک رأس به ازای هر زیرمسئلهٔ متمایز است و در صورتی یک یال جهت‌دار از رأس زیرمسئلهٔ x به رأس زیرمسئلهٔ y دارد که برای تعیین یک جواب بهینه برای زیرمسئلهٔ x مستقیماً نیاز به در نظر گرفتن یک جواب بهینه برای زیرمسئلهٔ y داشته‌باشیم. برای نمونه، گراف زیرمسئله دارای یک یال از x به y است، در صورتی که یک رویهٔ (procedure) بازگشتی بالا به پایین برای حل x، مستقیماً خود را برای حل y صدا بزند. می‌توان گراف زیرمسئله‌ها را یک نسخهٔ کاهش‌یافتهٔ درخت بازگشتی برای روش بازگشتی بالا به پایین در نظر گرفت، به گونه‌ای که همهٔ رئوس مربوط به یک زیرمسئله را یکی کنیم و یال‌ها را از والد به فرزند جهت‌دار کنیم.

روش پایین به بالا برای برنامه‌‌نویسی پویا رئوس گراف زیرمسئله‌ها را به ترتیبی در نظر می‌گیرد که همهٔ زیرمسئله‌های مجاور یک زیرمسئله، پیش از آن حل شوند. در یک الگوریتم برنامه‌نویسی پویای پایین به بالا، رئوس گراف زیرمسئله‌ها را به صورتی در نظر می‌گیریم که «معکوس مرتب توپولوژیکی» و یا «مرتب توپولوژیک وارون» زیرگراف مسئه‌ها است. به زبان دیگر، هیچ زیرمسئله‌ای تا زمانی که همهٔ زیرمسئله‌هایی که به آن‌ها وابسته است حل نشده‌اند، در نظر گرفته‌نمی‌شود. به طور مشابه، می‌توانیم رویکرد بالا به پایین (همراه به‌خاطرسپاری) برای برنامه‌نویسی پویا را به شکل جستجوی ژرفانخست گراف زیرمسئله‌ها ببینیم.

اندازهٔ گراف زیرمسئله‌ها می‌تواند ما را در تعیین زمان اجرای الگوریتم برنامه‌نویسی پویا یاری کند. از آنجایی که هر زیرمسئله را فقط یک بار حل می‌کنیم، زمان اجرا برابر است با مجموع تعداد بارهایی که نیاز است هر زیرمسئله را حل کنیم. به طور معمول، زمان محاسبهٔ جواب یک زیرمسئله، متناسب با درجهٔ رأس متناظر در گراف، و تعداد زیرمسئله‌ها برابر با تعداد رئوس گراف است.[۸]

مثالویرایش

یک پیاده‌سازی ساده از یک تابع برای یافتن عدد فیبوناچی nام می‌تواند به شکل زیر باشد.

 function fib(n)
 if n = 0
 return 0
 if n = 1
 return 1
 return fib(n − 1) + fib(n − 2)

برای مثال اگر از چنین تابعی (fib(5 را بخواهیم، تابع‌هایی که صدا می‌شوند به شکل زیر خواهند بود.

  1. fib(5)
  2. fib(4) + fib(3)
  3. (fib(3) + fib(2)) + (fib(2) + fib(1))
  4. ((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
  5. (((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

مشخص است که چنین فرایندی پر از محاسبات تکراری است. مثلاً عدد فیبوناچی دوم به تنهایی سه بار حساب شده‌است. در محاسبات بزرگ‌تر چنین تکرارهایی برنامه را به شدت کند می‌کنند. این الگوریتم دارای پیچیدگی زمانی نمایی است. حال فرض کنید ما یک آرایه دوتایی map داریم که عدد n را به مقدار عدد فیبوناچی nام مربوط کرده و ذخیره می‌کند. پیچیدگی زمانی چنین الگوریتمی n خواهد بود. همچنین میزان فضای اشغال‌شده‌اش هم از مرتبه n خواهد بود.

 var m := map(0 → 1, 1 → 1)
 function fib(n)
 if map m does not contain key n
 m[n] := fib(n − 1) + fib(n − 2)
 return m[n]

این نمونه‌ای از فرایند بالا به پایین بود. چون ابتدا مسئله را شکستیم و بعد به محاسبه و ذخیرهٔ پاسخ زیرمسئله‌ها پرداختیم.

در فرایند پایین به بالا برای حل چنین مسئله‌ای از عدد فیبوناچی یکم شروع می‌کنیم تا به عدد خواسته‌شده برسیم. با این کار باز هم پیچیدگی زمانی از مرتبه n است.

function fib(n)
 var previousFib := 0, currentFib := 1
 if n = 0
 return 0
 if n = 1
 return 1
 repeat n − 1 times
 var newFib := previousFib + currentFib
 previousFib := currentFib
 currentFib := newFib
 return currentFib

برتری این روش به روش قبلی در این است که در این روش حتی به فضای ذخیره از مرتبه n هم نیازی نیست. فضای ذخیره از مرتبه ۱ کفایت می‌کند. علت این که همیشه از رویکرد پایین به بالا استفاده نمی‌کنیم این است که گاهی از قبل نمی‌دانیم باید کدام زیرمسئله‌ها را حل کنیم تا به مرحله اصلی برسیم، یا این که مجبوریم زیرمسئله‌هایی که استفاده نمی‌شوند را هم حل کنیم.

تفاوت این روش و روش تقسیم و غلبه (تقسیم و حل)ویرایش

یکی از روش‌های پرکاربرد و مشهور طراحی الگوریتم روش برنامه‌نویسی پویا (یا برنامه‌ریزی پویا - Dynamic Programming) است. این روش همچون روش تقسیم و حل (Divide and Conquer) بر پایه تقسیم مسئله بر زیرمسئله‌ها کار می‌کند. اما تفاوت‌های چشم‌گیری با آن دارد.

زمانی که یک مسئله به دو یا چند زیرمسئله تقسیم می‌شود، دو حالت ممکن است پیش بیاید:

  1. داده‌های زیرمسئله‌ها هیچ اشتراکی با هم نداشته و کاملاً مستقل از هم هستند. نمونه چنین مواردی مرتب‌سازی آرایه‌ها با روش ادغام یا روش سریع است که داده‌ها به دو قسمت تقسیم شده و به صورت مجزا مرتب می‌شوند. در این حالت داده‌های یکی از بخش‌ها هیچ ارتباطی با داده‌های بخش دیگر نداشته و در نتیجۀ حاصل از آن بخش، اثری ندارند. معمولاً روش تقسیم و حل برای چنین مسائلی کارایی خوبی دارد.
  2. داده‌های زیرمسئله وابسته به هم بوده یا با هم اشتراک دارند. در این حالت به اصطلاح زیرمسئله‌ها هم‌پوشانی دارند. نمونه بارز چنین مسائلی محاسبه جمله nام دنباله اعداد فیبوناچی است.

روش برنامه‌نویسی پویا غالباً برای الگوریتم هایی به کار برده می‌شود که در پی حل مسئله‌ای به صورت بهینه می‌باشند.

در روش تقسیم و غلبه ممکن است برخی از زیرمسائلِ کوچکتر، با هم برابر باشند که در این صورت زیرمسائلِ برابر، به طور تکراری چندین مرتبه حل می‌شوند که این یکی از معایب روش تقسیم و غلبه است.

ایده‌ای که در فرایِ روش برنامه‌نویسی پویا نهفته‌ است، جلوگیری از انجام این محاسبات تکراری است و روشی که معمولاً برای این عمل به کارگرفته می‌شود استفاده از جدولی برای نگهداری نتایج حاصل از حل زیرمسائل است. در این صورت اگر الگوریتم به زیرمسئله‌ای برخورد کرد که پیش از این حل شده است، به جای حل مجدد آن، نتیجه محاسبه قبلی را از جدول برداشته و کار را با زیرمسئله بعدی دنبال می‌کند.

روش برنامه‌نویسی پویا یک روش پایین به بالا است (البته همان طور که گفته شد، اگر از قبل بدانیم باید کدام زیرمسئله‌ها را حل کنیم تا به مرحله اصلی برسیم، یا این که مجبور نباشیم زیرمسئله‌هایی که استفاده نمی‌شوند را هم حل کنیم) به این معنی که از حل مسائل کوچکتر به حل مسائل بزرگتر می‌رسیم. در حالیکه از نظر ساختاری، روش تقسیم و غلبه روشی است از بالا به پایین، یعنی به طور منطقی (نه عملی) پردازش از مسئله اولیه آغاز شده و به سمت پایین و حل مسائلِ کوچکتر، به پیش می‌رود.


مجموع زیرمجموعه و کوله پشتی : اضافه کردن یک متغیرویرایش

ما بیشتر و بیشتر می بینیم که مسائل مربوط به برنامه ریزی منبع خیلی خوبی از مسایل الگوریتمی را با انگیزه عملی ارائه می دهد.  تاکنون مشکلاتی را در نظر گرفتیم که در آن درخواستها با فاصله زمانی مشخص از یک منبع مشخص شده و همچنین مشکلاتی که در آن درخواستها دارای مدت و مهلت هستند ، اما فواصل خاصی را که در طی آن باید انجام شود تعیین نشده است. در این بخش ، ما نسخه ای از نوع دوم مشکل را با مدت زمان و مهلت در نظر می گیریم ، که حل مستقیم با استفاده از تکنیک هایی که تاکنون دیده ایم ، دشوار است.

ما برای حل مسئله از برنامه نویسی پویا استفاده خواهیم کرد ، اما با یک تغییر :مشخص می‌شود مجموعه زیرمسئله‌ها به اندازه کافی نخواهد بود ، و بنابراین ما در نهایت به ایجاد یک مجموعه غنی تر از زیرمسئله‌ها می پردازیم. همانطور که خواهیم دید ، این کار با اضافه کردن یک متغیر جدید به بازگشت در زیر برنامه پویا انجام می شود.

مسئله :ویرایش

در مسيله برنامه ریزی که در اینجا مورد توجه قرار می دهیم ، ما یک ماشین داریم که می تواند دستورها را پردازش کند همچنین یک سری درخواست ها 1 ، 2 ..... n داریم که میتوانیم از آن در بازه ی زمانی 0 تا W استفاده کنیم.هر درخواست مربوط به یک دستور است که برای پردازش آن به w_i واحد زمانی نیاز داریم. اگر هدف ما این باشد که دستور ها را پردازش کنیم به طوری که ماشین را تا زمان W مشغول نگه داریم ، کدام دستور ها را باید انتخاب کنیم ؟

به زبان دقیق تر ، فرض کنید که ما آیتم های 1 , 2, ... , n را داریم ، که هر کدام دارای وزن w_i است . هدف ما انتخاب یک زیرمجمعه از این آیتم‌ها است به طوری که مجموع ‌ w_i های آیتم های انتخاب شده از W کمتر مساوی باشند و با توجه به این محدودیت ، مجموع این وزن ها بیشترین مقدار ممکن باشد. ما این مسئله را " مسئله مجموع زیرمجموعه " می‌نامیم.

این مسئله یک حالت خاص رایج از مسيله ی کوله پشتی است ، که هر درخواست i یک ارزش v_i و یک وزن w_i دارد . هدف در این مسئله ی کلی تر این است که زیرمجموعه ای با بیشترین مجموع ارزش انتخاب کنیم با توجه به اینکه نباید مجموع وزن ها از W بیشتر باشد. به طور معمول مسئله ی کوله پشتی یک زیرمسئله از مسائل کلی تر است . نام " کوله پشی " اشاره به مسئله ی پر کردن یک کوله پشتی با ظرفیت W دارد که باید آن را تا جای ممکن با استفاده از مجموعه ای از آیتم ها پرکرد . ما از واژه ی وزن یا زمان برای اشاره به w_i , W استفاده خواهیم کرد.

چون این مسئله ، مسايل برنامه ریزی دیگری که قبلا دیده‌ایم را شبیه سازی می‌کند ، طبیعی است که از خود بپرسیم آیا الگوریتم حریصانه‌ای برای راه‌حل بهینه وجو دارد یا خیر. به نظر می‌آید که پاسخ خیر باشد ، حداقل تا کنون هیچ روش حریصانه‌ی کارایی شناخته نشده‌است که آیتم ها را به صورت نزولی مرتب کند ‌; یا حداقل این کار را برای آیتم‌های با وزن حداکثر W انجام دهد ، و سپس ما شروع به انتخاب آیتم ها با این ترتیب کنیم تا جایی که مجموع وزن آیتم های انتخاب شده کمتر از ‌W باشد . اما اگر W مضرب دو باشد ، و ما سه آیتم با وزن های { w/2 , w/2 , w/2+1} داشته باشیم ، خواهیم دید که الگوریتم حریصانه جواب بهینه را تولید نخواهد کرد. به طور جایگزین می‌توانستیم آیتم ها را بر اساس وزنشان به صورت صعودی مرتب کنیم و کار مشابه را انجام دهیم ، ولی این بار در ورودی های { 1, w/2 , w/2 } شکست می‌خورد .

هدف ما این است که نشان دهیم که چگونه با استفاده از برنامه نویسی پویا این مسئله را حل کنیم. اصول اولیه ی برنامه نویسی پویا را به یاد بیاورید : ما باید تعداد کمی از زیرمسئله ها را داشته باشیم به طوری که هر زیرمسئله را بتوان به راحتی با استفاده از زیرمسئله های "کوچکتر" حل کرد و پاسخ به مسئله ی اصلی براحتی بدست می‌آید ، هنگامی که ما پاسخ همه‌ی زیر مسئله ها را بدانیم.

طراحی الگوریتمویرایش

میخواهیم از تعداد زیادی زیرمسئله استفاده کنیم . در واقع برای هر مجموعه‌ی اولیه {i, ..., 1} از آیتم ها و هر مقدار ممکن w برای بیشینه وزن ممکن ، زیرمسئله ای در نظر می‌گیریم تا با استفاده از آنها بتوانیم سریعتر مسئله را حل کنیم . فرض کنید که W یک عدد صحیح باشد و همه ی درخواست های i= 1,..., n مقادیر صحیح w_i هستند. ما یک زیرمسئله به ازای هر i=1,...,n و هر w<= W داریم . ما opt(i,w) را به عنوان مقدار پاسخ بهینه برای آیتم های 1,..,i و بیشینه وزن مجاز w تعریف می‌کینم که برابر است با :

opt(i,w) = max_s \sgima_s w_j

که max روی زیر مجموعه s \in {1,...,i} است که w_i <= W را ارضا می‌کند . استفاده از این مجموعه ی جدید از زیرمسئله ها ، این اجازه را به ما می‌دهد که مقدار opt(i,w) به عنوان یک عبارت ساده بر حسب مقادیر بدست آمده از مسائل کوچکتر بیان کرد . علاوه بر این otp(i, w) کمیتی است که ما در انتها به دنبال آن هستیم . فرض کنید که o نمایانگر راه‌حل بهینه برای مسئله ی اصلی باشد. دو حالت داریم :

۱- اگر n \in o آنگاه opt(n,W)= opt(n-1, W) پس به راحتی میتوانیم عبارت n را نادیده بگیریم.

۲- اگر n \in o آنگاه opt(n,W)=w_n + opt(n-1,W-w_n) ، از این به بعد ما به دنبال استفاده از ظرفیت باقی‌مانده‌ی ‌W-w_n هستیم.

هنگامی که آیتم n ام بسیار بزرگ است ، w_n > W ما باید داشته باشیم opt(n,W)=opt(n-1, W) . در غیر این صورت ، ما راه‌حل بهینه را از این طریق با توجه به اینکه برای هر n درخواست کدام یک از گزینه های بالا بهتر است بدست می‌آوریم. با استفاده از استدلال مشابه برای زیرمسئله‌ی آیتم های {1, ... , i} و بیشینه وزن w ، رابطه ی بازگشتی روبرو را می‌دهد :

(*)

اگر w_i > w آنگاه opt(i,w) = opt(i-1 ,w) در غیر اینصورت :

opt(i,w) = max(opt(i-1,w), w_i + opt(i-1,w-w_i))

مانند قبل ، ما میخواهیم الگوریتمی طراحی کنیم که جدولی از همه‌ی مقادیر ممکن opt(i,w) داشته باشیم درحالی که هر کدام از آنها را حداکثر یکبار محاسبه کرده باشیم.

با استفاده از لم (*) می‌توان فورا ثابت کرد که مقدار برگشتی M[n,W]  برای در خواست های 1 ,….,n و وزن در دسترس W ، پاسخ بهینه ای است .( M یک آرایه ی دو بعدی است که مقادیر opt(i,w) را در خود ذخیره کرده است)

آنالیز کردن آلگوریتمویرایش

برای الگوریتمی که در این شرایط طراحی کرده ایم ، اما به یک جدول دو بعدی نیاز داریم ، که بازتاب دو بعدی جدول زیر سوالات است که در حال ساخته شدن هستند. به عنوان یک مثال از اجرای این الگوریتم ، به یک نمونه با وزن W = 6 و n = 3 و آیتم هایی با اندازه w1 = w2 = 2  و w3 = 3  توجه کنید . ما در خواهیم یافت که مقدار بهینه opt( 3 , 6 ) = 6 خواهد بود  ( که با استفاده از سومین آیتم و یکی از دو آیتم اول آن را بدست آورده ایم ) .

قبل از آن ، در  مورد  برنامه ریزی فواصل زمانی  موزون ، جدول راه حل های M را ساخته ایم ، و مقادیر  M[ i , w ] را با استفاده از مقادیر قبلی در زمان اجرای O(1) محاسبه می کنیم . بنابراین مدت زمان اجرا متناسب با تعداد ورودی ها به جدول می باشد .

(**)

الگوریتم مجموع زیرمجموعه (n و w) بهینه جواب مسئله را محاسبه می‌کند و زمان اجرای آن ‌O(nw) است .


توجه کنید که زمان اجرای برنامه تابعی از چندجمله‌ای بر حسب n نیست ، در واقع یک تابع چندجمله ای از n,w است . ما چنین الگوریتم هایی را " شبه چندجمله‌ای " می‌نامیم. الگوریتم های شبه‌چندجمله‌ای می‌توانند به طور قابل توجهی کارا باشند هنگامی که اعداد {w_i}یی که به عنوان ورودی دریافت می‌کنیم به طور قابل توجهی کوچک باشند. هرچند هنگامی که w_i ها بزرگ می‌شوند عملکرد این الگوریتم ها کاهش می‌یابد .

برای بدست آوردن یک مجموعه بهینه s از آیتم ها ، می‌توانیم آرایه‌ی M را با استفاده از روشی که در قبل گفته شد ردیابی کنیم.

(***)

یک جدول M از بهینه مقدار های زیرمسئله ها داده شده است . بهترین مجموعه s را می‌توان در زمان O(n) پیدا کرد.

مسئله‌ی کوله پشتیویرایش

مسئله‌ی کوله پشتی کمی از مسئله‌ی برنامه ریزی که در قبل بحث کردیم سخت تر است. شرایطی را در نظر بگیرید که مانند قبل آیتم i یک وزن نامنفی w_i دارد و همچنین یک ارزش جداگانه v_i نیز دارد . هدف ما پیدا کردن مجموعه s با بیشینه ارزش \sigma v_i با محدودیت مجموع وزن این آیتم ها نباید از W بیشتر شود .

سخت نیست که الگوریتم پویای خود را برای این مسئله کلی تر گسترش دهیم. ما از مجموعه زیرمسئله های مشابه استفاده می‌کنیم ، opt(i,w) تا مقدار راه‌حل بهینه را مشخص کنیم با استفاده از زیرمجموعه از آیتم های {1,...,i} و بیشینه وزن دردسترس w. ما یک راه‌حل بهینه o در نظرمی‌گیریم و دوحالت را با توجه به اینکه n \in o هست یا خیر ، مشخص می‌کنیم :

۱- اگر n \notin o آنگاه opt(n,w)=opt(n-1,w)

۲- اگر n \in o آنگاه opt(n,w) = v_n + opt(n-1, w-w_n)

استفاده از این خط از استدلال برای زیرمسئله ها نتیجه ی مشابه (*) را می‌دهد.

(****)

اگر w< w_i آنگاه opt(i,w)=opt(i-1,w) در غیر این صورت :

opt(i,w)=max(opt(i-1,w), v_i +opt(i-1,w-w_i))

با استفاده از این روش بازگشت ، می‌توانیم یه الگوریتم پویای کاملا مشابه پیاده سازی کنیم ، که نتیجه ی روبرو را می‌دهد :

مسئله‌ی کوله پشتی در زمان اجرای O(nW) قابل حل است.[۹]

الگوریتم ضریب دو جمله‌ای با استفاده از برنامه‌ریزی پویاویرایش

ورودی‌ها : اعداد صحیح و مثبت n و k که در آن k ≤ n است.

خروجی‌ها : bin2 ضریب دو جمله‌ای (n¦k)

 (int bin2 (int n , int k
 }
 index i,j
 ;[int B[0..n][0..K
                                                                                     (++for (i=0; i <= n ;i
                                                                         (++for (j=0;j <= min(i,k);j
                                                                           (if (j == 0 || j == i
 ۱ =[B[i][j
                                                                                            else
                                                              ;[B[i][j]=B[i-1][j-1]+B[i-1][j
                                                                                        ;[return B[n][k
                                                                                                          {

الگوریتم‌هایی که از برنامه‌نویسی پویا استفاده می‌کنندویرایش

پانویسویرایش

  1. هیلیر، ج 2، ص 65
  2. Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2009) [1990]. Introduction to Algorithms (3rd ed.). MIT Press and McGraw-Hill. ISBN 0-262-03384-4. p. 378.
  3. Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2009) [1990]. Introduction to Algorithms (3rd ed.). MIT Press and McGraw-Hill. ISBN 0-262-03384-4. pp. 379-383.
  4. Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2009) [1990]. Introduction to Algorithms (3rd ed.). MIT Press and McGraw-Hill. ISBN 0-262-03384-4. pp. 384-386.
  5. «Introduction to Dynamic Programming | 20bits». بایگانی‌شده از اصلی در ۳ نوامبر ۲۰۰۸. دریافت‌شده در ۲۵ ژوئیه ۲۰۱۱.
  6. «Memoize - perldoc.perl.org». perldoc.perl.org. دریافت‌شده در ۲۰۱۹-۱۲-۰۵.
  7. «Memoization». www.metalevel.at. دریافت‌شده در ۲۰۱۹-۱۲-۰۵.
  8. Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2009) [1990]. Introduction to Algorithms (3rd ed.). MIT Press and McGraw-Hill. ISBN 0-262-03384-4. pp. 367-368.
  9. Kleinberg, Jon; Tardos, Éva (2005). Algorithm Design. Pearson. ISBN 9780321295354. pp. 266-272.
  10. Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2009) [1990]. Introduction to Algorithms (3rd ed.). MIT Press and McGraw-Hill. ISBN 0-262-03384-4. pp. 390-396.
  11. Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2009) [1990]. Introduction to Algorithms (3rd ed.). MIT Press and McGraw-Hill. ISBN 0-262-03384-4. pp. 306-408.
  12. Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2009) [1990]. Introduction to Algorithms (3rd ed.). MIT Press and McGraw-Hill. ISBN 0-262-03384-4. pp. 408-409.


منابعویرایش

  • هیلیر، فردریک؛ لیبرمن، جرالد.ج (۱۳۸۲). تحقیق در عملیات، ترجمه محمد مدرس و اردوان آصف‌وزیری، تهران: نشر جوان، چاپ دهم.
  • نیپولیتان، ریچارد؛ نعیمی‌پور، کیومرث (۱۳۹۰). طراحی الگوریتم‌ها، ترجمه عین‌الله جعفرنژاد قمی، بابل: انتشارات علوم رایانه.

پیوند به بیرونویرایش

جستارهای وابستهویرایش