bg‑gray2

Treści zawarte w tym e‑materiale wykraczają poza poziom edukacyjny. Jeśli jednak jesteś zainteresowany tematyką rekurencji oraz wykorzystaniem jej do obliczania największego wspólnego dzielnika (NWD), to zachęcamy Cię do zapoznania się z poniższym e‑materiałem.

Ten materiał wymaga znajomości środowiska Scratch. Potrzebne informacje na ten temat znajdziesz w materiale Wprowadzenie do środowiska ScratchP9AYGQXHJWprowadzenie do środowiska Scratch.

Pewnie mieliście okazję zagrać w grę, w której bohater musiał pokonać labirynt. Być może nawet sami tworzyliście grę, w której gracze sterowali bohaterem, prowadząc go przez labirynt. Teraz zadanie dla duszka będzie trudniejsze. Po kliknięciu zielonej flagi, duszek będzie musiał samodzielnie znaleźć drogę na wyjście z labiryntu. Będzie realizował algorytm, który poprowadzi go do wyjścia przez labirynt. Przykładowe działanie aplikacji można zobaczyć na poniższym filmie.

RcYgjpKWcXFJN
Film pokazujący działający algorytm. Na scenie widoczna jest plansza. Jest ona labiryntem z zielonymi polami będącymi ścieżką i czerwonymi reprezentującymi ściany. Niebieskie pole to miejsce, do którego ma trafić duszek. Zgodnie z działaniem algorytmu duszek porusza się po zielonych polach docierając do celu, zostawiając przy tym czerwone kropki w odwiedzonych miejscach.

Na planszy w powyższej aplikacji są pola w trzech kolorach – zielonym, czerwonym i jedno niebieskie. Zielony oznacza korytarz, czyli pola po których duszek może się poruszać, a czerwony - ściany, czyli pola, niedostępne dla duszka. Wyjście oznaczone jest kolorem niebieskim.

Wiedząc, że scena w Scratchu ma 480 pikseli szerokości i 360 pikseli wysokości możesz łatwo obliczyć długość boku pojedynczego pola. Dzieląc, np. wysokość (360) przez liczbę pól, które znajdują się w jednej kolumnie (9) dostaniesz wyniki równy 40. Tyle wynosi długość boku pojedynczego pola.

Przykładowy plik z planszą możesz pobrać poniżej.

R1217LNuF5eNb

Załącznik zawierający planszę będącą labiryntem o wymiarach 9x12.

Przykładowa plansza do gry.
Źródło: GroMar, licencja: CC BY 3.0.
Plik PNG o rozmiarze 3.78 KB w języku polskim

Wykorzystaj poniższy dziennik do zapisywania notatek lub przemyśleń.

RmmJjmSvVzQ5i
Notatnik.
Źródło: GroMar, licencja: CC BY 3.0.
1
Ćwiczenie 1

Wczytaj planszę z labiryntem jako nowe tło sceny. Pamiętaj o usunięciu domyślnego, białego tła. Zmień rozmiary duszka, tak aby mieścił się w pojedynczym polu.

Algorytm znajdowania wyjścia z labiryntu

1
Ćwiczenie 2

Opracuj plan działania dla duszka, określając, jak powinien się poruszać, aby znaleźć drogę. Przyjmij założenie, że duszek „patrzy” tylko o jedno pole wprzód oraz pamięta (oznacza) pola, które już odwiedził. Może więc stwierdzić, czy sąsiednie pole jest ścianą, wolnym polem albo polem, przez które już przeszedł. Może też cofać się, jak trafi w ślepą uliczkę.

Do poprawnego stworzenia algorytmu rozwiązującego to zadanie niezbędne będzie zrozumienie pojęcia rekurencjiPXyPxbpbPrekurencji. Rekurencja jest ważnym konceptem programistycznym, który polega na tym, że funkcja może wywołać samą siebie w celu rozwiązania problemu.

2
Ćwiczenie 3

Wykonaj poniższe kroki, aby ustalić aktualną pozycję i kierunek duszka. Zakładamy, że duszek zaczyna na polu B2 i spogląda w dół. Kierunki jakie może wykonać to: dół, góra, lewo, prawo.

  1. Zapisz początkową pozycję duszka jako B2,

  2. Zapisz początkowy kierunek duszka jako dół,

  3. Przejdź do następnego kroku algorytmu.

R5K5MZejQRBWr
Labirynt z wyróżnionymi współrzędnymi.
Źródło: GroMar, licencja: CC BY 3.0.
R1GEJkQ7V7r1g
Tabela przedstawiająca ruchy duszka.
Źródło: GroMar, licencja: CC BY 3.0.
Ważne!

W naszym rozwiązaniu, gdy napotkaliśmy pewien problem, zastosowaliśmy podejście, w którym odwołujemy się do tego samego problemu, ale dla innych danych. Może to oznaczać, że mamy podobną sytuację do rozwiązania, ale z innymi wartościami lub innymi warunkami. Na przykład, jeśli mamy do wykonania pewne zadanie, które można podzielić na mniejsze zadania tego samego typu, możemy użyć rekurencji (rekursji). To znaczy, że w celu rozwiązania całego zadania, wykonujemy te same kroki dla każdego z tych mniejszych zadań. Rekurencja polega na tworzeniu zbioru poleceń, które wywołują same siebie wewnątrz siebie. W Scratchu takie zbiory stanowią utworzone przez nas bloki w kategorii Moje bloki. Działa to podobnie jak rosyjskie lalki Matrioszka, gdzie jedna lalka jest schowana wewnątrz drugiej. W przypadku rekurencji, funkcja wykonuje się, a następnie wywołuje samą siebie, aby rozwiązać podzadanie, a to podzadanie może z kolei wywołać kolejne podzadania, aż do momentu osiągnięcia warunku zakończenia.

Więcej na temat rekurencji dowiesz się w dalszej części materiału.

Tworzymy skrypty dla duszka

Przed przystąpieniem do tworzenia skryptów należy jeszcze rozstrzygnąć dwa problemy:

  • Jak duszek ma oznaczać pole?

  • W jaki sposób duszek ma rozpoznawać kolor sąsiedniego pola?

Zwróć uwagę, że nie ma znaczenia, czy pole jest ścianą, czy było już odwiedzone. Najwygodniej będzie oznaczać pola odwiedzone tym samym kolorem co ściana. Jak można zauważyć na filmach, duszek stawia na nowo odwiedzanym polu kropkę w kolorze ściany.

Żeby sprawdzić w jakim kolorze jest sąsiednie pole, duszek musi niestety na nim stanąć. Przejdzie więc najpierw na sąsiednie pole. Jeżeli pod duszkiem nie ma koloru czerwonego, kontynuuje działania. Jeżeli jest kolor czerwony, wycofuje się. W związku z tym dostosujemy nasz algorytm w postaci listy kroków do możliwości duszka w środowisku Scratch.

szukaj_drogi:

  1. Oznacz pole, na którym stoisz.

  2. Powtórz 4 razy:
    2.1. Przesuń o 40 kroków.
    2.2. Jeżeli dotyka koloru niebieskiego to:
     ⠀ 2.2.1. Powiedz „Znalazłem wyjście przez 2 sekundy”.
     ⠀ 2.2.2. Zatrzymaj wszystko.
    2.3. Jeżeli nie dotyka koloru czerwonego to:
     ⠀ 2.3.1. Szukaj drogi.
    2.4. Przesuń o -40 kroków.
    2.5. Obróć się w prawo o 90 stopni.

  3. Zatrzymaj ten skrypt.

2
Ćwiczenie 4

Stwórz swój własny skrypt, który będzie wykonywał opisany wcześniej algorytm. Zacznij od utworzenia nowego bloku o nazwie oznacz_pole. Możesz tego dokonać klikając przycisk Utwórz blok w kategorii Moje bloki.  Dane wejściowe pozostaw puste.

2
Ćwiczenie 5

Uzupełnij blok oznacz_pole. Zastanów się, jak narysować czerwony ślad. Pamiętaj, że duszek po narysowaniu musi być na tej samej pozycji, co przed rysowaniem.

2
Ćwiczenie 6

Przygotuj skrypt, który po uruchomieniu spowoduje, że duszek dotrze do niebieskiego pola na planszy. Aby tego dokonać, umieść duszka na polu startowym i ustal, w którym kierunku ma się poruszać. Dodatkowo, użyj pisaka do oznaczenia odwiedzonych miejsc. Do znalezienia trasy wykorzystaj wcześniej przygotowany blok o nazwie szukaj_drogi.

Uruchom i przetestuj działanie aplikacji. Prawdopodobnie duszek bardzo szybko biega po labiryncie. Możesz go spowolnić, wstawiając klocek czekaj … si0E2ogPrm8_d479e210czekaj … s z odpowiednio dobranym czasem opóźnienia przed wywołaniem bloku szukaj_drogi. Możesz także przygotować drugi skrypt uruchamiany po kliknięciu zielonej flagi animujący duszka podczas chodzenia po labiryncie.

i0E2ogPrm8_d479e187
i0E2ogPrm8_d479e210

Zadania uzupełniające

2
Ćwiczenie 7

Popraw planszę tak, aby nie było przejścia z pola startowego do wyjścia lub usuń wyjście. Możesz w tym celu skopiować tło sceny i je nieznacznie poprawić w edytorze graficznym. Na którym polu duszek zakończy poszukiwanie drogi?  Jaki blok za to odpowiada? Popraw skrypt tak, aby duszek poinformował w dymku komiksowym, że nie ma przejścia do wyjścia.

2
Ćwiczenie 8

Zmodyfikuj tak aplikację, aby duszek próbę wykonania kolejnego ruchu rozpoczynał zawsze od tego samego kierunku (np. w prawo), a nie kierunku, w którym patrzy.

Ciekawostka

Do realizacji zadania została zastosowana technika projektowania algorytmów – rekurencja. Budując algorytm rekurencyjny należy pamiętać o podstawowych zasadach: wywołanie rekurencyjne zazwyczaj występuje dla zmienionych danych przy spełnieniu pewnego warunku. Ciąg wywołań rekurencyjnych powinien się zakończyć w momencie spełnienia warunku stopu. Warunek stopu oznacza, że program dla dowolnych danych musi ostatecznie zakończyć działanie. Ustalenie warunku stopu jest ważne w przypadku funkcji rekurencyjnych, bowiem definiując funkcje rekurencyjne nietrudno o warunku stopu zapomnieć. Najczęściej budujemy blok rekurencyjny z danymi wejściowymi, a warunek, kiedy następuje wywołanie rekurencyjne zależy od tych danych.
W omawianym algorytmie rekurencyjnym (wychodzenia z labiryntu) rolę danych wejściowych pełni pole, na którym stoi duszek, choć sam blok (w skryptach) jest zdefiniowany bez parametrów. Wywołanie rekurencyjne zależy od koloru pola, na które próbuje przejść duszek.
Porównaj działanie opracowanej przez siebie aplikacji z filmem, który znajduje się na początku tego materiału. Czy obie aplikacje stosują ten sam algorytm znajdowania drogi wyjścia z labiryntu? Łatwo zauważysz, że w twojej aplikacji duszek znajduje drogę do wyjścia, ale wcale nie najkrótszą, często wracając. Takie algorytmy nazywane są algorytmami z powrotami i często są zapisywane rekurencyjnie.
Natomiast w przedstawionej na filmie aplikacji duszek znajduje najkrótszą drogę do wyjścia. Rozwiązuje tak naprawdę bardziej ogólny problem znalezienia najlepszej drogi (w tym przypadku najkrótszej) pomiędzy dwoma miejscami (polami). Z problemem znalezienia najlepszej drogi (najkrótszej, najszybszej) możesz się zetknąć choćby korzystając z nawigacji satelitarnej lub map internetowych.
W przyszłości, jeśli będziesz kontynuować swoją przygodę z algorytmami, poznasz zapewne algorytm znajdowania najkrótszej drogi przejścia, a także inne problemy rozwiązywane algorytmami z powrotami.

2
Ćwiczenie 9

Zajrzyj do projektu Ułamki zwykłeD1AyvQpb3Ułamki zwykłe dotyczącego algorytmu Euklidesa. Własność NWD została zdefiniowana rekurencyjnie:

NWD(ab)=NWD(a-bb) przy założeniu, że a>b.

Spróbuj utworzyć własny blok obliczający NWD rekurencyjnie i stwórz prostą aplikację sprawdzającą, czy blok działa poprawnie.

Dodatkowe informacje znajdziesz w materiale Klasyczny Algorytm Euklidesa z resztą z dzieleniaPs7XRBNbCKlasyczny Algorytm Euklidesa z resztą z dzielenia. Odpowiednie zrozumienie algorytmu Euklidesa jest niezbędne, aby stworzyć poprawne rozwiązanie.

Ważne!

Dla wielu problemów można podać rozwiązanie zarówno rekurencyjne, jak i iteracyjne (czyli z wykorzystaniem pętli). W szczególności, jeśli w algorytmie rekurencyjnym następuje dokładnie jedno wywołanie rekurencyjne, zawsze można podać rozwiązanie z jedną pętlą powtarzaj ażi0E2ogPrm8_d479e272powtarzaj aż. W takiej sytuacji lepiej jest stosować rozwiązanie iteracyjne.

i0E2ogPrm8_d479e272