ش | ی | د | س | چ | پ | ج |
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
برج هانوی از سه میله و تعدادی دیسک در اندازههای متفاوت تشکیل شدهاست که میتوان آنها را بر میلهها جای داد.
علاقهمندان به مباحث مختلف طراحی الگوریتم و همینطور شرکت کنندگان مسابقات برنامه نویسی به خوبی میدانند که یکی از مهمترین پارامترهای طراحی موفقیت آمیز یک الگوریتم، شیوه صحیح فکر کردن روی مساله است. حل انواع سوالات الگوریتمی به ما کمک میکند ذهن خودمان را برای حل مسائل پیچیده تر آماده کنیم. در همین راستا و به عنوان یک تمرین ساده، به بررسی یکی از روشهای حل مساله کلاسیک برج هانوی می پردازیم. مساله برج هانوی یکی از مسائل جذاب، قدیمی و مشهور است که به یک مساله کلاسیک در علوم رایانه تبدیل شدهاست.
در محوطه معبدی در آسیای دور سه میله الماسی قرار داشت که یکی از آنها حاوی تعدادی قرص طلایی بود. کاهنان معبد در تلاش بودند تا قرصهای طلائی را از آن میله به یکی دیگر از میلهها تحت شرایطی انتقال دهند، و باور داشتند که با تمام شدن انتقال قرصها عمر جهان نیز به پایان خواهد رسید! میله اولیه ۶۴ قرص داشت، که بر روی هم به طور نزولی بر اساس اندازهشان چیده شدهبودند.
همانند شکل سه میله داریم. یکی از میلهها میله مبدا (A)، دیگری میله کمکی (B) و دیگری میله مقصد (C) است. هدف انتقال تمام دیسکها از میله مبدا به میله مقصد با رعایت شرایط زیر است:
در هر زمان فقط یک دیسک را میتوان جابجا نمود. نباید در هیچ زمانی دیسکی بر روی دیسک با اندازه کوچکتر قرار بگیرد.
هدف ما ارائه الگوریتمی است که کمترین توالی حرکتها را برای انتقال دیسکها به ما بدهد. مثلاً اگر n=۲ باشد، توالی حرکت به صورت زیر است:
توجه داشته باشید که بر اساس قانون اول نمیتوان به غیر از بالاترین دیسک هر میله، به دیسک دیگری از آن دسترسی پیدا کرد.
حال سوال این است که آیا این مساله به کمک تکنیک بازگشت قابل حل است؟ اصولاً چه مسائلی را میتوان بازگشتی حل نمود؟
برای اینکه مسالهای بتواند با روش بازگشتی حل شود باید یک ویژگی اساسی داشته باشد. مساله اصلی (مسالهای که به ما داده میشود) قابل خرد شدن به زیر مسالههایی از همان نوع مساله اصلی باشد، به شرطی که اندازه زیر مسالههای ایجاد شده کمتر باشد. آنگاه میتوان امیدوار بود که آن را به طور بازگشتی حل کرد! این ویژگی در مورد مساله برج هانوی صدق میکند. ایده اصلی این است که توجهمان را به جای حرکت بالاترین دیسک، روی پایینترین دیسک میله متمرکز کرده، و مراحل زیر را طی میکنیم:
n - ۱ دیسک بالایی را با شرایط ذکر شده و به کمک میله C به میله B منتقل میکنیم. بزرگترین دیسک را از میله مبدا به میله مقصد حرکت میدهیم. n - ۱ دیسک را که هم اکنون در میله B هستند با شرایط داده شده به میله مقصد انتقال میدهیم. میبینیم که توانستیم عملیات جابجا کردن n دیسک را به دو عملیات مشابه ولی با اندازه کمتر و یک عملیات ساده تقسیم کنیم. واضح است که جابجا کردن n - ۱ قرص راحتتر از جابجا نمودن n قرص است.
تابع بازگشتی زیر به زبان ++C ترتیب حرکتها را چاپ میکند:
void hanoi ( int nDisk, char start, char temp, char finish ) { if ( nDisk == 1 ) cout <<start <<" --> " <<finish <<endl; else { hanoi ( nDisk - 1, start, finish, temp ); cout <<start <<" --> " <<finish <<endl; hanoi ( nDisk - 1, temp, start, finish ); } }
برای مثال فراخوانی تابع به شکل ('hanoi(3, ‘A’, ‘B’, ‘C مساله برج هانوی را با سه دیسک که در میله A قرار دارند و با کمک میله B به میله C منتقل خواهد شد، حل میکند.
برای این که به کاهنان کمک کنیم، باید دستور ('hanoi(64, ‘A’, ‘B’, ‘C را اجرا کنیم. ولی چه زمانی طول میکشد تا این دستور اجرا شود؟ در حالت کلی میخواهیم بدانیم اگر تعداد دیسکها n باشد، کمترین تعداد حرکت برای جابجا نمودن دیسکها چقدر است؟
در ابتدا باید بررسی کنیم که آیا تابع بازگشتی فوق کمترین تعداد حرکت را چاپ میکند؟ جواب مثبت است. زیرا واضح است که برای جابجا کردن بزرگترین دیسک از پایین میله A، بقیه دیسکها باید در میله B باشند. فقط در این صورت این دیسک جابجا میشود. در فراخونیهای بعدی دیسک دوم از نظر بزرگی جابجا میشود و الی آخر. پس در این فراخوانیها جابجایی بیهودهای صورت نمیگیرد. همچنین توالی حرکتها برای هر n منحصربهفرد است. یعنی برای یک n دو توالی متمایز از جابجاییها وجود ندارد که تعداد جابجایی آنها کمتر یا مساوی این حالت باشد.
حال به مساله مرتبه اجرایی مساله میپردازیم: فرض کنیم (T(n تعداد حرکتهای لازم جهت انتقال n دیسک به مقصد باشد. بر اساس توضیحات فوق (T(n - ۱ حرکت برای انتقال n - ۱ دیسک به میله کمکی، یک حرکت برای انتقال بزرگترین دیسک به میله مقصد، و باز (T(n - ۱ حرکت برای انتقال n - ۱ دیسک موجود در میله کمکی به میله مقصد نیاز است. پس میتوان نوشت:
T(n) =2 T(n - ۱) + 1
با حل این رابطه بازگشتی داریم:
T( n ) = 2n-1
همانطور که مشاهده میکنیم مرتبه اجرایی این الگوریتم (O(2n است که هرچند مرتبه مناسبی نیست، این روش حداقل تعداد حرکتهای ممکن را میدهد.
اگر فرض کنیم کاهنان با سرعت عمل زیاد توانسته باشند به صورت شبانه روزی و نسل به نسل در هر دو ثانیه یک قرص را جابجا کنند، برای انتقال تمامی ۶۴ قرص به میله مقصد، در حدود ۱٫۱۶۹ ترلیون (میلیون میلیون) سال زمان لازم دارند!
در واقع ما از روش Divide and Conquer یا حل و تقسیم برای ارائه راه حل استفاده نمودهایم. اما چون در تقسیم مساله اصلی به دو زیر مساله، اندازه ورودیهای زیر مسالهها نزدیک به اندازه ورودی اصلی هستند، کارایی الگوریتم مطلوب نیست.
این مساله علاوه بر روش تابع بازگشتی راه حلهای غیربازگشتی نیز دارد. در بالا به این نتیجه رسیدیم که بهترین راه حل برای جابجا کردن n دیسک ۲n - ۱ حرکت نیاز دارد. در نتیجه مرتبه راه حلهای آن در بهینهترین حالت، چه بازگشتی و چه غیربازگشتی، از مرتبه ( O( 2n خواهد بود. اما آنچه که راه حل بازگشتی و غیربازگشتی را از هم متمایز میکند مرتبه فضای مصرفی آن است. حل بازگشتی مساله، فراخوانیهای تو در تو و فضای پشته از مرتبه ( O( n نیاز دارد. در حالی که میتوان با استفاده از روش غیربازگشتی این مرتبه را به ( O( 1 کاهش داد. البته این مساله تنها دلیل بررسی روش غیربازگشتی نیست. تبدیل مرتبه مصرف فضا از ( O( n به ( O( 1 زمانی که مرتبه اجرایی الگورینم ( O( 2n است چندان قابل توجه نیست. دلیل دیگر میتواند این باشد که برخی زبانهای برنامه نویسی از فراخوانی بازگشتی توابع پشتیبانی نمیکنند و مجبور به استفاده ار روشهای غیربازگشتی هستند. اما دلیل اصلی این است که با بررسی این روشها تمرین کوچکی برای تبدیل الگوریتمهای بازگشتی به غیربازگشتی انجام می دهیم.
تا کنون چندین روش مختلف جهت پیاده سازی غیربازگشتی حل مساله برج هانوی ارائه شده است، که ما در اینجا دو روش را معرفی کرده، و تنها یکی از آنها را به طور کامل بررسی می کنیم. توجه داشته باشید که همه جزئیات حل مساله به صورت دقیق و مشروح مطرح نمیشود، و استدلال قسمتی از نتیجه گیریها به عنوان تمرین به شما واگذار میشود.
حل مساله برج هانوی را میتوان معادل پاسخ دادن به این سوال دانست که: در هر مرحله کدام دیسک به کدام میله منتقل میشود؟
کدام دیسک؟
استدلال 2 میگوید که اولین حرکت همیشه با دیسک 1 است. استدلال 3 میگوید حرکت بعدی با دیسکی غیر از دیسک 1 است. استدلال 4 میگوید این دیسک نمیتواند بزرگترین دیسک موجود در بالای میلهها باشد. پس در مرحله بعدی دیسک y جابجا خواهد شد. و بالاخره حرکت بعدی باز هم با دیسک 1 است (چرا؟).
پس با بررسی منطقی خود به این نتیجه رسیدیم که دیسک 1 و دیسکی که بزرگترین دیسک در آن مرحله نیست، به صورت متناوب جابجا میشوند. مراحل با شماره فرد برای دیسک 1، و مراحل با شماره زوج برای دیسک y.
کدام میله؟
فرض کنیم دیسک 1 روی میله A قرار داشته باشد و آن را به میله C منتقل کنیم. در مرحله بعدی دیسک y جابجا میشود. و در مرحله بعد باز هم دیسک ۱ باید جابجا شود. حال اگر این دیسک را به میله A بازگردانیم، به نوعی کار اضافی و بازگشت به عقب انجام داده ایم. برای آشکار شدن این موضوع کافی است مساله برج هانوی را با دو دیسک حل کنید، و در حرکت دوم دیسک 1، آنرا به میلهای بازگردانید که از آن آمده بود. پس میتوان گفت در حرکتهای متوالی، دیسک شماره 1 به میلهای حرکت میکند که از آن به میله فعلی خود نیامده است. این مساله نه تنها در مورد دیسک 1، که در مورد همه دیسکها صادق است. یعنی همه دیسکها در حرکتهای خود به سمت میلهای میروند که در حرکت قبلی خود از آن نیامده اند. اما لحاظ کردن این شرط برای این دیسکها لازم نیست. چرا که در هر مرحله، تنها یک انتخاب برای حرکت خود دارند.
تنها مساله باقیمانده، میله مقصد دیسک 1 در اولین حرکت خود است. زمانی که این دیسک اولین حرکت خود را انجام میدهد، نمیتوان از استدلال فوق برای تشخیص میله مقصد استفاده کرد (چرا!؟). استدلال این قسمت را هم که چندان دشوار نیست به شما وا می گذاریم و تنها به بیان نتیجه می پردازیم: اگر n (تعداد دیسکها) زوج باشد، دیسک 1 در اولین حرکت به میله کمکی (یعنی میله B)، و در غیراینصورت به میله مقصد (یعنی میله C) منتقل می کنیم.
به این ترتیب حل مساله برج هانوی به صورت غیربازگشتی به صورت کامل پیاده سازی میشود. حال می دانیم که در هر مرحله کدام دیسک به کدام میله منتقل میشود. تعداد مراحل هم همواره برابر ۲n - ۱ است. پیاده سازی کد این الگوریتم را نیز به شما وا می گذاریم تا با کار روی آن به خوبی بر الگوریتم تشریح شده مسلط شوید.
void hanoi ( int nDisk, char start, char temp, char finish ) { int max = nDisk; char dest = finish; int disk = max; while( true ) { while( disk> 0 ) { if( moving disk succeeds ) { if( disk == max ) { max--; if( max == 0 ) { return; } } dest = the final place of max; } else { dest = the alternative place between dest and the current place of disk; } disk--; } p and q = the places different of dest; disk = the smaller of the disks on top of p and q; dest = the place between p and q with greater disk on top; } }
در پایان توجه داشته باشید که دو روش ذکر شده، تنها روشهای پیاده سازی غیربازگشتی حل مساله نیستند.
#include <stdlib.h> #include <conio.h> #define COUNT 8 enum Bar{L,C,R}; struct disk{int Size,Color;}; struct stack{int i; disk *Disks;}; void transfer(int,Bar,Bar,Bar); void init(); // Init bars void MoveDisk(Bar from,Bar to); void DrawBars(); stack Bars[3]={ {0,{0}} ,{0,{0}}, {0,{0}} }; int main(void) { textmode(C4350); clrscr(); init(); DrawBars(); transfer(COUNT,L,R,C); getch(); return 0; } char ConvertBarEnum2Char(Bar E){ char r=0; switch (E) { case L: r='L'; break; case C: r='C'; break; case R: r='R'; break; } return r; } void msg(Bar from,Bar to){ gotoxy(25,4); textattr(15|16*0); cprintf("Press anykey to move from %c to %c",ConvertBarEnum2Char(from),ConvertBarEnum2Char(to)); gotoxy(37,5); cprintf("Esc = Exit"); } void transfer(int n,Bar from,Bar to,Bar temp){ if(n>0){ transfer(n-1,from,temp,to); msg( from, to); MoveDisk(from,to); transfer(n-1,temp,to,from); } } void init(){ Bars[L].Disks=new disk[COUNT]; for(int i=0;i<COUNT;i++){ Bars[L].Disks[i].Size=COUNT-i+1; Bars[L].Disks[COUNT-i-1].Color=i+1; } Bars[L].i=COUNT-1; Bars[R].Disks=new disk[COUNT]; for(i=0;i<COUNT;i++){ Bars[R].Disks[i].Size=0; Bars[R].Disks[i].Color=0; } Bars[R].i=-1; Bars[C].Disks=new disk[COUNT]; for(i=0;i<COUNT;i++){ Bars[C].Disks[i].Size=0; Bars[C].Disks[i].Color=0; } Bars[C].i=-1; } void MoveDisk(Bar from,Bar to){ char kb=getch(); if(kb==27) exit(1); Bars[to].Disks[++(Bars[to].i)]= Bars[from].Disks[(Bars[from].i)--]; clrscr(); DrawBars(); } void me(){ char c; for(int i=0;str[i];i++){ c=i%14+1; if(c==1)c=2; textattr(c|16); cprintf("%c",str[i]); } } void DrawBars(){ int n=0; for(int j=0;j<3;j++){ for(int i=0;i<=Bars[j].i;i++){ gotoxy(1+j*27,24-i); textattr(Bars[j].Disks[i].Color|16*0); for(n=0;n<28 && n-13<Bars[j].Disks[i].Size ;n++){ if(n<14-Bars[j].Disks[i].Size) cprintf("%s"," "); else cprintf("%s","ـ"); } } textattr(15|16*0); for(n=0;n<15;n++){ gotoxy(1+j*27+13,n+10); cprintf("%s","؛"); } } gotoxy(5,28); me(); }