Przedstawienie problemu

Wiesz już, że zagadka Wież Hanoizagadka Wież Hanoizagadka Wież Hanoi polega na przeniesieniu krążków o różnych średnicach z jednego słupka na drugi. Warunkiem poprawnego rozwiązania zadania jest pojedyncze przenoszenie każdego krążka, który znajduje się na szczycie, a także brak możliwości położenia krążka o większej średnicy na krążku o mniejszej średnicy. Do dyspozycji mamy jednak pomocniczy słupek, na który możemy tymczasowo nakładać krążki.

Zatem mamy trzy słupki: A, B, C oraz n krążków. Początkowo wszystkie krążki położone są na słupku A. Docelowo wszystkie krążki mają znaleźć się na słupku C. Pomocniczym słupkiem jest słupek B. Celem naszego zadania jest przedstawienie ciągu ruchów, opisującego rozwiązanie zagadki Wież Hanoi.

R18lEEYeXYasj
Źródło: Contentplus.pl sp. z o.o., licencja: CC BY-SA 3.0.

Implementacja algorytmu w języku C++

Zagadkę Wież Hanoi dla n krążków rozwiążemy w sposób rekurencyjnyalgorytm rekurencyjnyrekurencyjny. Program przetestujemy dla trzech krążków.

Specyfikacja:

Dane:

  • n – liczba krążków; liczba naturalna dodatnia

  • A – nazwa słupka początkowego; znak

  • B – nazwa słupka pomocniczego; znak

  • C – nazwa słupka docelowego; znak

Wynik:

Program na standardowym wyjściu wypisuje ciąg ruchów opisujący rozwiązanie zagadki wież Hanoi dla n krążków, gdzie początkowo wszystkie znajdują się na słupku A. Na końcu wszystkie krążki mają znaleźć się na słupku C, a pomocniczym słupkiem jest B.

Tworzymy funkcję o nazwie hanoi(), której przyjmowanymi parametrami wejściowymi będzie n, czyli liczba krążków, a następnie A, BC, czyli nazwy słupków: początkowego, pomocniczego, a także końcowego.

Linia 1. kratka include otwórz nawias ostrokątny iostream zamknij nawias ostrokątny. Linia 2. using namespace std średnik. Linia 4. void hanoi otwórz nawias okrągły int n przecinek char A przecinek char B przecinek char C zamknij nawias okrągły otwórz nawias klamrowy. Linia 6. zamknij nawias klamrowy.

Następnie, gdy liczba krążków jest większa od 0 (dla liczby krążków równej 0 zajdzie przypadek bazowyprzypadek bazowyprzypadek bazowy), możemy wykonywać algorytm. Wywołamy na początku rekurencyjnie funkcję hanoi, która zrealizuje przeniesienie n - 1 górnych krążków ze słupka A na słupek B. Na słupku A pozostanie tylko jeden, n-ty, największy krążek. Reszta krążków będzie nałożona na słupek B.

Linia 1. kratka include otwórz nawias ostrokątny iostream zamknij nawias ostrokątny. Linia 2. using namespace std średnik. Linia 4. void hanoi otwórz nawias okrągły int n przecinek char A przecinek char B przecinek char C zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. if otwórz nawias okrągły n zamknij nawias ostrokątny 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 6. hanoi otwórz nawias okrągły n minus 1 przecinek A przecinek C przecinek B zamknij nawias okrągły średnik. Linia 7. zamknij nawias klamrowy. Linia 8. zamknij nawias klamrowy.

Operację przeniesienia n - 1 górnych krążków ze słupka A na słupek B przedstawia ilustracja:

R5gonJQUjp5Be
Źródło: Contentplus.pl sp. z o.o., licencja: CC BY-SA 3.0.

Największy krążek musi zostać przełożony na słupek docelowy C. Wypisujemy zatem na ekran komunikat: „Przeniesienie z” A „do” C.

Linia 1. kratka include otwórz nawias ostrokątny iostream zamknij nawias ostrokątny. Linia 2. using namespace std średnik. Linia 4. void hanoi otwórz nawias okrągły int n przecinek char A przecinek char B przecinek char C zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. if otwórz nawias okrągły n zamknij nawias ostrokątny 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 6. hanoi otwórz nawias okrągły n minus 1 przecinek A przecinek C przecinek B zamknij nawias okrągły średnik. Linia 7. cout otwórz nawias ostrokątny otwórz nawias ostrokątny cudzysłów Przeniesienie z cudzysłów otwórz nawias ostrokątny otwórz nawias ostrokątny A otwórz nawias ostrokątny otwórz nawias ostrokątny cudzysłów do cudzysłów otwórz nawias ostrokątny otwórz nawias ostrokątny C otwórz nawias ostrokątny otwórz nawias ostrokątny endl średnik. Linia 8. zamknij nawias klamrowy. Linia 9. zamknij nawias klamrowy.

Operację przeniesienia n-tego krążka na słupek docelowy C, przedstawia rysunek:

R1Z3QddHibMDU
Źródło: Contentplus.pl sp. z o.o., licencja: CC BY-SA 3.0.

Na koniec musimy jeszcze wywołać rekurencyjnie funkcję hanoi, aby przenieść n - 1 krążków ze słupka B na słupek C, na którym położony jest już największy krążek.

Aby to zrealizować, pierwszym przyjmowanym argumentem będzie n - 1, ponieważ chcemy przenieść właśnie taką liczbę krążków. Drugim argumentem będzie B, ponieważ tym razem ten słupek jest słupkiem początkowym. Następnie umieszczamy A, ponieważ w tej sytuacji będzie on słupkiem pomocniczym, a na końcu słupek docelowy C.

Linia 1. kratka include otwórz nawias ostrokątny iostream zamknij nawias ostrokątny. Linia 2. using namespace std średnik. Linia 4. void hanoi otwórz nawias okrągły int n przecinek char A przecinek char B przecinek char C zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. if otwórz nawias okrągły n zamknij nawias ostrokątny 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 6. hanoi otwórz nawias okrągły n minus 1 przecinek A przecinek C przecinek B zamknij nawias okrągły średnik. Linia 7. cout otwórz nawias ostrokątny otwórz nawias ostrokątny cudzysłów Przeniesienie z cudzysłów otwórz nawias ostrokątny otwórz nawias ostrokątny A otwórz nawias ostrokątny otwórz nawias ostrokątny cudzysłów do cudzysłów otwórz nawias ostrokątny otwórz nawias ostrokątny C otwórz nawias ostrokątny otwórz nawias ostrokątny endl średnik. Linia 8. hanoi otwórz nawias okrągły n minus 1 przecinek B przecinek A przecinek C zamknij nawias okrągły średnik. Linia 9. zamknij nawias klamrowy. Linia 10. zamknij nawias klamrowy.
R1ev9uJhTaw90
Źródło: Contentplus.pl sp. z o.o., licencja: CC BY-SA 3.0.

Teraz wystarczy wywołać funkcję hanoi, np. dla trzech krążków z nazwami słupków: A, B, C.

Linia 1. kratka include otwórz nawias ostrokątny iostream zamknij nawias ostrokątny. Linia 2. using namespace std średnik. Linia 4. void hanoi otwórz nawias okrągły int n przecinek char A przecinek char B przecinek char C zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. if otwórz nawias okrągły n zamknij nawias ostrokątny 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 6. hanoi otwórz nawias okrągły n minus 1 przecinek A przecinek C przecinek B zamknij nawias okrągły średnik. Linia 7. cout otwórz nawias ostrokątny otwórz nawias ostrokątny cudzysłów Przeniesienie z cudzysłów otwórz nawias ostrokątny otwórz nawias ostrokątny A otwórz nawias ostrokątny otwórz nawias ostrokątny cudzysłów do cudzysłów otwórz nawias ostrokątny otwórz nawias ostrokątny C otwórz nawias ostrokątny otwórz nawias ostrokątny endl średnik. Linia 8. hanoi otwórz nawias okrągły n minus 1 przecinek B przecinek A przecinek C zamknij nawias okrągły średnik. Linia 9. zamknij nawias klamrowy. Linia 10. zamknij nawias klamrowy. Linia 12. int main otwórz nawias okrągły zamknij nawias okrągły otwórz nawias klamrowy. Linia 13. hanoi otwórz nawias okrągły 3 przecinek apostrof A apostrof przecinek apostrof B apostrof przecinek apostrof C apostrof zamknij nawias okrągły średnik. Linia 14. return 0 średnik. Linia 15. zamknij nawias klamrowy.

Ciąg ruchów otrzymany w wyniku wywołania funkcji dla trzech krążków będzie wyglądał następująco:

  • Przeniesienie z A do C

  • Przeniesienie z A do B

  • Przeniesienie z C do B

  • Przeniesienie z A do C

  • Przeniesienie z B do A

  • Przeniesienie z B do C

  • Przeniesienie z A do C

Zobaczmy jeszcze, jak będzie wyglądało drzewo wywołań rekurencyjnych dla wywołania funkcji hanoi(3, 'A', 'B', 'C'). Dzięki niemu, będziemy mogli zobaczyć wszystkie wywołania funkcji hanoi. Lewa strzałka odchodząca od danej funkcji, będzie wskazywała na pierwsze wywołanie funkcji, umieszczone w szóstej linijce naszego kodu, natomiast prawa strzałka wskaże nam drugie wywołanie, z ósmej linijki. Aby odczytać rozwiązanie problemu z drzewa wywołań rekurencyjnych, najpierw musimy odczytać ruchy z lewego poddrzewa węzła, następnie ruch należący do węzła – ojca, a na końcu ruchy z prawego poddrzewa.

R161ZKB5Hocn61
Źródło: Contentplus.pl sp. z o.o., licencja: CC BY-SA 3.0.

Zagadka wież Hanoi jest przykładem zadania, którego złożność obliczeniowa wzrasta bardzo szybko wraz z liczbą krążków n. Przedstawiony algorytm ma złożoność wykładniczązłożoność wykładniczawykładniczą, to znaczy O ( 2 n ) .

Słownik

algorytm rekurencyjny
algorytm rekurencyjny

algorytm, w którym funkcja lub procedura wywołuje samą siebie; parametry wywołania takiej funkcji lub procedury rekurencyjnej mogą się zmieniać; odwołania rekurencyjnie pozostawiają na stosie systemowym adres i parametry powrotu do danej funkcji lub procedury

zagadka Wież Hanoi
zagadka Wież Hanoi

problem, polegający na przeniesieniu wieży z krążków z jednego słupka na drugi, przy czym można przenosić tylko po jednym krążku z wierzchu stosu; nie jest dozwolone położenie większego krążka na mniejszym

przypadek bazowy
przypadek bazowy

warunek w funkcji rekurencyjnej, po spełnieniu którego funkcja nie wywołuje samej siebie po raz kolejny

złożoność wykładnicza
złożoność wykładnicza

złożoność, która jest określona za pomocą funkcji wykładniczej, rośnie wykładniczo w zależności od rozmiaru danych wejściowych n