آنچه میخوانید ویراست جدید نوشتهای است که اولین بار با عنوان «روش برنامه نویسی پویا»
تیر ماه ۱۳۸۷
از طریق وبگاه برنامهنویسی و طراحی الگوریتم (عنوان و طرح پیشین وبگاه الگوریتمستان)
منتشر شده بود.
یکی از روشهای پرکاربرد و مشهور طراحی الگوریتم روش برنامهنویسی پویا (یا برنامهریزی پویا، برنامهسازی پویا - Dynamic Programming) است. این روش همچون روش تقسیم و حل (Divide and Conquer) بر پایهی تقسیم مسئله بر زیرمسئلهها کار میکند. اما تفاوتهای چشمگیری با آن دارد.
زمانی که یک مسئله به دو یا چند زیرمسئله تقسیم میشود، دو حالت ممکن است پیش بیاید:
1- دادههای زیرمسئلهها هیچ اشتراکی با هم نداشته و کاملا مستقل از هم هستند. نمونهی چنین مواردی مرتبسازی آرایهها با روش ادغام یا روش سریع است که دادهها به دو قسمت تقسیم شده و به صورت مجزا مرتب میشوند. در این حالت دادههای یکی از بخشها هیچ ارتباطی با دادههای بخش دیگر نداشته و در نتیجه حاصل از آن بخش اثری ندارند. معمولا روش تقسیم و حل برای چنین مسائلی کارآیی خوبی دارد.
2- دادههای زیرمسئله وابسته به هم بوده و یا با هم اشتراک دارند. در این حالت به اصطلاح زیرمسئلهها همپوشانی دارند. نمونهی بارز چنین مسائلی محاسبهی جملهی nام دنبالهی اعداد فیبوناچی است.
دنبالهی اعداد فیبوناچی (فیبوناتچی)
[بازگشت به فهرست]
دنبالهی اعداد فیبوناچی (Fibonacci) یکی از دنبالههای عددی مشهور ریاضیات با تعریف بازگشتی زیر است:
\[F(n) = F(n - 1) + F(n - 2) \qquad n > 2 \qquad, \qquad F(1) = F(2) = 1 \]
محاسبهی جملهی nام دنباله به محاسبهی دو جملهی قبلی آن نیاز دارد. پس میتوان گفت محاسبهی
$ F(n - 1) $ و $ F(n - 2) $ دو زیرمسئله برای مسئلهی اصلی هستند. اما در عین حال این دو زیرمسئله از هم مستقل نیستند. برای محاسبهی $ F(n - 1) $ بر اساس رابطهی بالا باید داشته باشیم:
\[F(n - 1) = F(n - 2) + F(n - 3) \]
که نشان میدهد خود $ F(n - 1) $ وابسته به $ F(n - 2) $ است.
اگر این مسئله را به روش تقسیم و حل - که سادهترین روش است - حل کنیم:
1 | int fibo(int n){
|
2 | if(n > 2)
|
3 | return fibo(n - 1) + fibo(n - 2);
|
4 | return 1;
|
5 | } |
تابع fibo مقدار n را دریافت کرده و به صورت بازگشتی و بر اساس رابطهی ذکر شده، جملهی nام دنبالهی فیبوناچی را محاسبه میکند. حال درخت فراخوانیهای بازگشتی تابع را به ازای n = 7 رسم میکنیم:

هر گره درخت، فراخوانی تابع را با مقدار داخل آن نشان میدهد. برای محاسبهی جملهی هفتم دنبالهی فیبوناچی تابع fibo به صورت (fibo(7 فراخوانی میشود که آن هم (fibo(6 و (fibo(5 را فراخوانی میکند و الی آخر. همانطور که مشاهده میکنید، برای محاسبهی این جمله، (fibo(7 یک بار، (fibo(6 یک بار، (fibo(5 دو بار، (fibo(4 سه بار، (fibo(3 پنج بار، (fibo(2 هشت بار، (fibo(1 پنج بار و روی هم رفته تابع fibo بیست و پنج بار فراخوانی میشود.
ما خود چگونه جملات دنبالهی فیبوناچی را محاسبه میکنیم؟ ابتدا جملهی اول و دوم را جمع زده و جملهی سوم را محاسبه میکنیم. سپس با استفاده از جملهی به دست آمده و جملهی دوم، جملهی چهارم را محاسبه میکنیم و همینطور ادامه میدهیم:
1 1 2(= 1 + 1)
1 1 2 3(= 2 + 1)
1 1 2 3 5(= 3 + 2)
1 1 2 3 5 8(= 5 + 3)
1 1 2 3 5 8 13(= 8 + 5)
و به این ترتیب جملهی هفتم دنباله تنها با پنج محاسبهی ساده به دست میآید. در حالت کلی با استفاده از این روش تنها به n - 2 عمل جمع نیاز است که نشان از الگوریتمی با مرتبهی خطی دارد. در حالی که میتوان ثابت کرد در حالت اول تعداد کل فراخوانیهای بازگشتی تابع از مرتبهی نمایی است. دلیل اختلاف این دو عدد در این است که در حالت دوم، هر جملهی دنباله فقط و فقط یک بار محاسبه میشود. این همان روش برنامهنویسی پویا است.
در برنامهنویسی پویا مسئله به صورت جزء به کل حل میشود. یعنی ابتدا زیرمسائل خرد حل شده و نتیجهی آنها در مکانی ذخیره میشود. سپس به سمت زیرمسائل کلیتر رفته و با استفاده از دادههای از پیش محاسبه شده، آنها نیز حل میشوند. در مورد دنبالهی فیبوناچی میتوان نوشت:
1 | int fibo(int n){
|
2 | int f[MAX], i;
|
3 | f[1] = f[2] = 1;
|
4 | for(i = 3 ; i <= n ; i++)
|
5 | f[i] = f[i - 1] + f[i - 2];
|
6 | return f[n];
|
7 | } |
در این روش ما جملات دنبالهها را پس از محاسبه در یک آرایه ذخیره میکنیم. برای این کار به جای حرکت از کل به جزء (یعنی از n به 1 که در روش تقسیم و حل استفاده میشود)، از جزء به سمت کل حرکت میکنیم. هر جملهی دنباله تنها به دو جملهی قبل خود نیاز دارد که با حرکت جزء به کل قبلا محاسبه شدهاند و نیاز به محاسبهی مجدد آنها نیست. البته این کد را میتوان سادهتر کرد:
1 | int fibo(int n){
|
2 | int i, f1, f2, f3;
|
3 | f1 = f2 = 1;
|
4 | for(i = 3 ; i <= n ; i++){
|
5 | f3 = f1 + f2;
|
6 | f1 = f2;
|
7 | f2 = f3;
|
8 | }
|
9 | return f3;
|
10 | } |
تحلیل این تابع ساده را به خود شما وا میگذارم.
نکتهی مهم این است که اگر زیرمسئلهها همپوشانی نداشته باشند روش برنامهنویسی پویا هیچ کمکی به ما نخواهد کرد. چرا که خاصیت اصلی این روش ذخیره دادههایی است که ممکن است به کرات به آنها مراجعه شود. حال اگر هیچ اشتراکی در کار نباشد، طبیعتا از هر داده تنها یک بار استفاده خواهد شد.
برنامهنویسی پویا برای طراحی الگوریتمهای محاسبهی حالتهای بهینهی مسائل نیز کاربرد زیادی دارد. به عنوان مثال در یافتن کوتاهترین مسیر بین دو نقطه، محاسبهی بهینهترین حالت ضرب زنجیری ماتریسها، درخت جستجوی بهینه، مسئلهی فروشندهی دورهگرد، محاسبهی ضرب چندجملهایها، مسئلهی کولهپشتی صفر و یک و چندین مسئلهی دیگر، از برنامهنویسی پویا استفاده میشود. شرط اساسی امکان استفاده از این روش برای محاسبهی حالت بهینه به اصل بهینگی مشهور است.
اصل بهینگی: اصل بهینگی یعنی حل مسئله به صورت بهینه، حاوی حل بهینهی تمامی زیرمسائل آن نیز باشد
.
به عبارت دیگر، مسئله باید به گونهای باشد که با یافتن حل بهینهی آن، حل بهینهی همه زیرمسئلهها نیز به دست آمده باشد. به عنوان مثال، در یافتن کوتاهترین مسیر بین دو شهر، مسیر بین مبدأ و هر گرهی که در مسیر بهینه وجود دارد، بهینهترین مسیر بین آن دو نیز هست.