پاسخ به:ویژگی های برنامهریزی پویا
چهارشنبه 3 مهر 1392 2:55 PM
اگر بنا باشد پرانتزبندی کل عبارت بهینه شود، پرانتزبندی زیرمسالهها هم باید بهینه باشند. یعنی بهینه بودن مساله، بهینه بودن زیرمسالهها را ایجاب میکند. پس میتوان از روش برنامهنویسی پویا استفاده کرد.
حل بهینه، سومین مرحله از بسط یک الگوریتم برنامه نویسی پویا برای مسائل بهینه سازی است. مراحل بسط چنین الگوریتمی به شرح زیر است:
1- ارائه یک ویژگی بازگشتی که حل بهینه ی نمونه ای از مسئله را به دست می دهد.
2- محاسبه مقدار حل بهینه به شیوهی جزء به کل.
3- بنا کردن یک حل نمونه به شیوهی جزء به کل.
تمام مسائل بهینهسازی را نمیتوان با برنامه نویسی پویا حل کرد چرا که باید اصل بهینگی در مسئله صدق کند.
اصل بهینگی در یک مسئله صدق می کند اگر یک حل بهینه برای نمونهای از مسئله، همواره حاوی حل بهینه برای همهی زیر نمونهها باشد
در روش برنامهنویسی پویا اغلب از یک آرایه برای ذخیره نتایج جهت استفاده مجدد استفاده شده، و زیرمسائل به صورت جزء به کل حل میشوند. در مورد این مساله، ابتدا زیرمسائلی که تنها از دو ماتریس تشکیل شدهاند محاسبه میشوند. سپس زیرمسائلی که از سه ماتریس تشکیل شدهاند محاسبه میشوند. این دسته از زیرمسائل به زیرمسائلی متشکل از دو ماتریس تجزیه میشوند که قبلا محاسبات آنها صورت گرفته، و نتایج آنها در آرایه ذخیره شده اند. در نتیجه نیاز به محاسبه مجدد آنها نیست. به همین ترتیب در مرحله بعد زیرمسائل با چهار ماتریس محاسبه میشوند، و الی آخر.
درختهای جستوجوی دودویی بهینه
درخت جست و جوی دودویی یک درخت دودویی از عناصر است که معمولا کلید نامیده میشوند به قسمی که:
1- هر گره حاوی یک کلید است.
2- کلیدهای موجود در زیردرخت چپ هر گره، کوچکتر یا مساوی کلید آن گره هستند.
3- کلیدهای موجود در زیردرخت راست هر گره، بزرگتر یا مساوی کلید آن گره هستند
استفاده از روش زیرسازه بهینه به این معناست که مسئله را به زیرمسئلههایی کوچکتر بشکانیم و برای هر یک از این زیرمسئلهها پاسخی بهینه بیابیم و پاسخ بهینه مسئلهٔ کلی را از کنار هم قرار دادن این پاسخهای بهینهٔ جزئی به دست آوریم. مثلاً در هنگام حل مسئلهٔ یافتن کوتاهترین مسیر از یک رأس یک گراف به رأسی دیگر، میتوانیم کوتاهترین مسیر تا مقصد از همهٔ رئوس مجاور را به دست آوریم و از آن برای جواب کلی استفاده کنیم. به طور کلی حل مسئله از این روش شامل سه مرحله است.
گفته میشود مسئلهای دارای زیرمسئلههای همپوشان است اگر بتوان مسئله را به زیرمسئلههای کوچکتری شکست که پاسخ هرکدام چند بار در طول فرایند حل مورد استفاده قرار بگیرد. برنامهریزی پویا کمک میکند تا هر کدام از این پاسخها فقط یک بار محاسبه شوند فرایند حل از بابت دوبارهکاری هزینهای را متحمل نشود. برای مثال در دنباله فیبوناچی برای محاسبهٔ عدد چهارم دنباله به دانستن عدد سوم نیاز داریم. برای محاسبهٔ عدد پنجم هم باز به عدد سوم نیاز داریم. حال اگر مثلاًَ در شرایطی بخواهیم عدد ششم دنبالهٔ فیبوناچی را حساب کنیم، در این محاسبه هم مقدار عدد پنجم را میخواهیم و هم مقدار عدد چهارم را. اگر تصمیم بگیریم اعداد چهارم و پنجم را به نوبت حساب کنیم در هنگام محاسبهٔ هرکدام به مقدار عدد سوم نیاز پیدا میکنیم و باید دوباره آن را محاسبه کنیم. برای جلوگیری از این محاسبات چندباره، معمولاً الگوریتمهایی که مبتنی بر برنامهریزی پویا هستند از یکی از دو راه زیر استفاده میکنند.
یک پیادهسازی ساده از یک تابع برای یافتن عدد فیبوناچی nام میتواند به شکل زیر باشد.
function fib(n)
if n = 0
return 0
else if n = 1
return 1
return fib(n − 1) + fib(n − 2)
برای مثال اگر از چنین تابعی (fib(5
را بخواهیم، تابعهایی که صدا میشوند به شکل زیر خواهند بود.
fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((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
else if n = 1
return 1
repeat n − 1 times
var newFib := previousFib + currentFib
previousFib := currentFib
currentFib := newFib
return currentFib
برتری این روش به روش قبلی در این است که در این روش حتی به فضای ذخیره از مرتبه n هم نیازی نیست. فضای ذخیره از مرتبه 1 کفایت میکند. علت این که همیشه از رویکرد پایین به بالا استفاده نمیکنیم این است که گاهی از قبل نمیدانیم باید کدام زیرمسئلهها را حل کنیم تا به مرحله اصلی برسیم، یا این که مجبوریم زیرمسئلههایی که استفاده نمیشوند را هم حل کنیم.