|
|
(Nie pokazano 5 pośrednich wersji utworzonych przez tego samego użytkownika) |
Linia 128: |
Linia 128: |
| Tak jak w poprzednim zadaniu poruszać się można tylko w czterech kierunkach podstawowych, nie po przekątnej. | | Tak jak w poprzednim zadaniu poruszać się można tylko w czterech kierunkach podstawowych, nie po przekątnej. |
|
| |
|
| {{wskazowka| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Wskazówka 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| Zadanie to należy rozwiązywać tak jak poprzednie. Jedyną różnicą jest to, jak należy decydować czy z danego pola można przejść do sąsiedniego: oprócz zaznaczenia trzeba wziąć pod uwagę różnicę wysokości. | | Zadanie to należy rozwiązywać tak jak poprzednie. Jedyną różnicą jest to, jak należy decydować czy z danego pola można przejść do sąsiedniego: oprócz zaznaczenia trzeba wziąć pod uwagę różnicę wysokości. |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| {{rozwiazanie| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Rozwiązanie 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| '''function''' Zjazd1(M,N:integer; '''var''' A:'''array'''[1..M,1..N] '''of''' integer; i1,j1,i2,j2:integer):boolean; | | '''function''' Zjazd1(M,N:integer; '''var''' A:'''array'''[1..M,1..N] '''of''' integer; i1,j1,i2,j2:integer):boolean; |
| // M,N >= 1 | | // M,N >= 1 |
Linia 183: |
Linia 187: |
| ''Dyskusja:'' Tak jak w rozwiązaniu poprzedniego zadania, istnienie sąsiada na planszy badamy przed wywołaniem rekurencyjnym, a poprawność przejścia na początku wywołania. Służy nam do tego dodatkowy parametr 'h', który oznacza wysokość poprzedniego pola. Dlatego pierwsze wywołanie funkcji 'szukaj' ma pierwszy parametr A[i1,j1]+1 (w wariancie I). '''Uwaga!''' Funkcja ta nie zadziała, jeśli A[i1,j1]=MaxInt (czyli maksymalnej możliwej wartości typu integer). Poniżej przedstawione jest rozwiązanie bez tego mankamentu. | | ''Dyskusja:'' Tak jak w rozwiązaniu poprzedniego zadania, istnienie sąsiada na planszy badamy przed wywołaniem rekurencyjnym, a poprawność przejścia na początku wywołania. Służy nam do tego dodatkowy parametr 'h', który oznacza wysokość poprzedniego pola. Dlatego pierwsze wywołanie funkcji 'szukaj' ma pierwszy parametr A[i1,j1]+1 (w wariancie I). '''Uwaga!''' Funkcja ta nie zadziała, jeśli A[i1,j1]=MaxInt (czyli maksymalnej możliwej wartości typu integer). Poniżej przedstawione jest rozwiązanie bez tego mankamentu. |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| {{rozwiazanie| 2||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Rozwiązanie 2</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| '''function''' Zjazd2(M,N:integer; '''var''' A:'''array'''[1..M,1..N] '''of''' integer; i1,j1,i2,j2:integer):boolean; | | '''function''' Zjazd2(M,N:integer; '''var''' A:'''array'''[1..M,1..N] '''of''' integer; i1,j1,i2,j2:integer):boolean; |
| // M,N >= 1 | | // M,N >= 1 |
Linia 239: |
Linia 245: |
| ''Opis:'' Rozwiązanie jest bardzo podobne do poprzedniego. Różnica polega na przeniesieniu kodu z początku funkcji 'szukaj' do osobnej funkcji 'wdol'. Funkcja ta jest używana jako test dopuszczający do rekurencyjnego wywołania funkcji 'szukaj'. | | ''Opis:'' Rozwiązanie jest bardzo podobne do poprzedniego. Różnica polega na przeniesieniu kodu z początku funkcji 'szukaj' do osobnej funkcji 'wdol'. Funkcja ta jest używana jako test dopuszczający do rekurencyjnego wywołania funkcji 'szukaj'. |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| ==Zadanie 3 (Wieże Hanoi z ograniczeniami)== | | ==Zadanie 3 (Wieże Hanoi z ograniczeniami)== |
Linia 245: |
Linia 251: |
| Na wykładzie omawiane były [[Wstęp do programowania/Rekursja#wieze Hanoi|wieże Hanoi]]. Ciekawa modyfikacja tego zadania polega na zabronieniu ruchów pomiędzy niektórymi pałeczkami, np. z pierwszej na drugą. Zapisać procedurę realizującą to zadanie przy zabronionych niektórych ruchach. | | Na wykładzie omawiane były [[Wstęp do programowania/Rekursja#wieze Hanoi|wieże Hanoi]]. Ciekawa modyfikacja tego zadania polega na zabronieniu ruchów pomiędzy niektórymi pałeczkami, np. z pierwszej na drugą. Zapisać procedurę realizującą to zadanie przy zabronionych niektórych ruchach. |
|
| |
|
| {{wskazowka| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Wskazówka 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| * Dozwolone ruchy najlepiej reprezentować w tablicy typu TDozwRuchow = array[1..3, 1..3] of boolean, (przekątna nie ma znaczenia). | | * Dozwolone ruchy najlepiej reprezentować w tablicy typu TDozwRuchow = array[1..3, 1..3] of boolean, (przekątna nie ma znaczenia). |
| * Zastanów się, jaki warunek musi być spełniony przez tablicę dozwolonych ruchów, aby zadanie miało rozwiązanie. | | * Zastanów się, jaki warunek musi być spełniony przez tablicę dozwolonych ruchów, aby zadanie miało rozwiązanie. |
| * Użyj funkcji-otoczki do wstępnego sprawdzenia czy istnieje rozwiązanie oraz do pamiętania tablicy dozwolonych ruchów w czasie działania funkcji rekurencyjnej. | | * Użyj funkcji-otoczki do wstępnego sprawdzenia czy istnieje rozwiązanie oraz do pamiętania tablicy dozwolonych ruchów w czasie działania funkcji rekurencyjnej. |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| <!-- | | <!-- |
Linia 258: |
Linia 266: |
| end; | | end; |
| --> | | --> |
| {{rozwiazanie| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Rozwiązanie 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| '''type''' Paleczki = 1..3; | | '''type''' Paleczki = 1..3; |
| | | |
Linia 306: |
Linia 316: |
| ''Omówienie:'' Zauważ, że dużo łatwiej jest rozwiązać to zadanie sformułowane w sposób ogólny, niż gdy powie się, że zabroniony jest konkretny ruch (np. 1 → 2). | | ''Omówienie:'' Zauważ, że dużo łatwiej jest rozwiązać to zadanie sformułowane w sposób ogólny, niż gdy powie się, że zabroniony jest konkretny ruch (np. 1 → 2). |
| </div> | | </div> |
| </div>}} | | </div> |
| | | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| {{cwiczenie| 1|pytanko 1|<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Ćwiczenie 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| Jaki przypadek jest najgorszy i ile trzeba wykonać wtedy ruchów? | | Jaki przypadek jest najgorszy i ile trzeba wykonać wtedy ruchów? |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| {{odpowiedz||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Odpowiedź</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| Najgorszy przypadek to taki, gdy przerwane jest połączenie Skąd-Dokąd w obie strony. Wtedy trzeba wykonać <math>3^{Ile}-1</math> ruchów) | | Najgorszy przypadek to taki, gdy przerwane jest połączenie Skąd-Dokąd w obie strony. Wtedy trzeba wykonać <math>3^{Ile}-1</math> ruchów) |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| ==Zadanie 4 (Ustawianie hetmanów)== | | ==Zadanie 4 (Ustawianie hetmanów)== |
| Napisz procedurę znajdująca wszystkie takie rozstawienia 8 hetmanów na szachownicy, by żadne dwa hetmany się nie atakowały. | | Napisz procedurę znajdująca wszystkie takie rozstawienia 8 hetmanów na szachownicy, by żadne dwa hetmany się nie atakowały. |
|
| |
|
| {{wskazowka| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Wskazówka 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| W tablicach logicznych pamiętamy zajętość kolumn i obu rodzajów przekątnych, kolejnego hetmana ustawiamy w kolejnym wierszu jeśli się da, jeśli nie wracamy. | | W tablicach logicznych pamiętamy zajętość kolumn i obu rodzajów przekątnych, kolejnego hetmana ustawiamy w kolejnym wierszu jeśli się da, jeśli nie wracamy. |
|
| |
|
Linia 328: |
Linia 343: |
| Oczywiście lepiej uogólnić na N hetmanów na szachownicy N×N. | | Oczywiście lepiej uogólnić na N hetmanów na szachownicy N×N. |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| {{rozwiazanie| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Rozwiązanie 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| '''procedure''' Hetmany(N:integer); | | '''procedure''' Hetmany(N:integer); |
| // N >= 1 | | // N >= 1 |
Linia 408: |
Linia 425: |
|
| |
|
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| ==Zadanie 5 (Mnożenie wielomianów)== | | ==Zadanie 5 (Mnożenie wielomianów)== |
| Dane są dwie tablice (array[0..N-1] of real) reprezentujące dwa wielomiany stopnia N-1. Należy obliczyć iloczyn tych wielomianów metodą dziel-i-zwyciężaj. Zakładamy, że N jest potęgą dwójki. | | Dane są dwie tablice (array[0..N-1] of real) reprezentujące dwa wielomiany stopnia N-1. Należy obliczyć iloczyn tych wielomianów metodą dziel-i-zwyciężaj. Zakładamy, że N jest potęgą dwójki. |
|
| |
|
| {{wskazowka| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Wskazówka 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| Zamiast rozwiązania naiwnego wymagającego <math>n^2</math> mnożeń zróbmy trochę lepsze (aczkolwiek nie najlepsze) polegające na podzieleniu wielomianów na dwie części. Cały pomysł polega na spostrzeżeniu, że jeśli zadane wielomiany <math>p(x)</math> i <math>q(x)</math> podzielimy na dolną i górną część<br> | | Zamiast rozwiązania naiwnego wymagającego <math>n^2</math> mnożeń zróbmy trochę lepsze (aczkolwiek nie najlepsze) polegające na podzieleniu wielomianów na dwie części. Cały pomysł polega na spostrzeżeniu, że jeśli zadane wielomiany <math>p(x)</math> i <math>q(x)</math> podzielimy na dolną i górną część<br> |
| <math>p(x) = p_l(x) + p_h(x)*x^{N/2}</math> i analogicznie | | <math>p(x) = p_l(x) + p_h(x)*x^{N/2}</math> i analogicznie |
Linia 423: |
Linia 442: |
| Czyli potrzebujemy 3 mnożeń o połowę mniejszych wielomianów. W sumie uzyskamy liczbę mnożeń rzędu <math>n^{lg 3}</math>. Szczegóły można znaleźć w [[Sedgewick|Sedgewick Algorithms in C++]], str. 527-530. | | Czyli potrzebujemy 3 mnożeń o połowę mniejszych wielomianów. W sumie uzyskamy liczbę mnożeń rzędu <math>n^{lg 3}</math>. Szczegóły można znaleźć w [[Sedgewick|Sedgewick Algorithms in C++]], str. 527-530. |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| ==Zadanie 6 (Suma składników)== | | ==Zadanie 6 (Suma składników)== |
Linia 432: |
Linia 451: |
| 3 = 1+1+1 | | 3 = 1+1+1 |
|
| |
|
| {{wskazowka| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Wskazówka 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| Użyj dodatkowej tablicy do przechowywania początków rozkładów. Uważaj, aby nie kopiować tej tablicy przy każdym wywołaniu rekurencyjnym! | | Użyj dodatkowej tablicy do przechowywania początków rozkładów. Uważaj, aby nie kopiować tej tablicy przy każdym wywołaniu rekurencyjnym! |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| {{wskazowka| 2||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Wskazówka 2</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| Funkcja rekurencyjna powinna mieć parametr wskazujący jak duże składniki mogą być użyte do rozkładu pozostałej liczby. </div> | | Funkcja rekurencyjna powinna mieć parametr wskazujący jak duże składniki mogą być użyte do rozkładu pozostałej liczby. </div> |
| </div>}} | | </div> |
|
| |
|
| {{rozwiazanie| 1||<div class="mw-collapsible mw-made=collapsible mw-collapsed"><div class="mw-collapsible-content" style="display:none">
| | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> |
| | <span class="mw-collapsible-toogle mw-collapsible-toogle-default style="font-variant:small-caps">Rozwiązanie 1</span> |
| | <div class="mw-collapsible-content" style="display:none"> |
| '''procedure''' Suma(n:integer); | | '''procedure''' Suma(n:integer); |
| // 1 <= n | | // 1 <= n |
Linia 491: |
Linia 516: |
| Poza wspomnianymi modyfikacjami całą procedurę 'rozkład' można by napisać w sposób ''rosnący'', tzn. zamiast generować coraz mniejsze składniki - generować coraz większe, aż osiągniemy bądź przekroczymy n. W takim wypadku składniki należałoby wypisywać w odwrotnej kolejności. | | Poza wspomnianymi modyfikacjami całą procedurę 'rozkład' można by napisać w sposób ''rosnący'', tzn. zamiast generować coraz mniejsze składniki - generować coraz większe, aż osiągniemy bądź przekroczymy n. W takim wypadku składniki należałoby wypisywać w odwrotnej kolejności. |
| </div> | | </div> |
| </div>}} | | </div> |
|
| |
|
| <!-- to zadanie tu nie pasuje, wymaga zdecydowanie rekordow z wariantami... | | <!-- to zadanie tu nie pasuje, wymaga zdecydowanie rekordow z wariantami... |
To są zadania na rekursję.
Oglądaj wskazówki i rozwiązania __SHOWALL__
Ukryj wskazówki i rozwiązania __HIDEALL__
Zadanie 1 (Labirynt)
Czy istnieje ścieżka miedzy wskazanymi punktami (i1,j1) i (i2,j2) w labiryncie reprezentowanym przez prostokątną tablicę liczb całkowitych o rozmiarze M×N, zawierającą zera (ściana) i jedynki (droga)? Zakładamy, że nie można przechodzić z pola na pole po skosie (np. z (2,5) na (3,6)), a tylko w czterech podstawowych kierunkach (np. z (2,5) na (3,5), (2,4) itd.)
Wskazówka 1
Właściwą funkcję rekurencyjną szukającą drogi zamknij w funkcji-otoczce, która poza wywołaniem funkcji rekurencyjnej sprawdzi poprawność argumentów, przygotuje i/lub posprząta tablicę z labiryntem, zinterpretuje wynik funkcji rekurencyjnej itp. oraz będzie przechowywać dane wspólne dla wszystkich wywołań funkcji rekurencyjnej.
Rozwiązanie 1
function Labirynt(M,N:integer; var A:array[1..M,1..N] of integer; i1,j1,i2,j2:integer):boolean;
// M,N >= 1
// A zawiera 0 (ściana) i 1 (droga)
function szukaj(i,j:integer):boolean;
// 1 <= i <= M
// 1 <= j <= N
var jest:boolean;
begin
if (i=i2) and (j=j2) then
jest:=true
else begin
jest:=false;
if A[i,j]>0 then begin
A[i,j]:=-1;
if i>1 then jest:=szukaj(i-1,j);
if not jest and (i<M) then jest:=szukaj(i+1,j);
if not jest and (j>1) then jest:=szukaj(i,j-1);
if not jest and (j<N) then jest:=szukaj(i,j+1);
end
end;
szukaj:=jest
end; // szukaj
var i,j:integer;
begin // Labirynt
if not((1<=i1) and (i1<=M) and (1<=j1) and (j1<=N) and
(1<=i2) and (i2<=M) and (1<=j2) and (j2<=N)) then
Labirynt:=false
else
begin
Labirynt:=szukaj(i1,j1);
for i:=1 to M do
for j:=1 to N do
if A[i,j]=-1 then A[i,j]:=1
end
end; // Labirynt
Koszt czasowy: liniowy względem rozmiaru A (czyli M×N)
Koszt pamięciowy: liniowy względem rozmiaru A
Opis: Przeszukujemy wgłąb tablicę zaznaczając odwiedzone pola poprzez ustawienie ich wartości na -1. Dzięki temu nie zapętlimy się ani nie będziemy przetwarzać wielokrotnie tych samych pól.
Dyskusja: Funkcja 'Labirynt' jest otoczką, która sprawdza poprawność parametrów, wywołuje właściwą funkcję rekurencyjną 'szukaj' oraz sprząta po niej zaznaczenia w tablicy A.
Wewnętrzna funkcja rekurencyjna 'szukaj' ma za zadanie znaleźć ścieżkę z punktu (i,j) do punktu (i2,j2) po niezaznaczonych polach w tablicy A. Korzysta ona z kilku nazw zadeklarowanych przez otoczkę (M, N, A, i2, j2), dlatego nie są one parametrami wywołania funkcji 'szukaj'.
Funkcja 'szukaj':
- sprawdza czy punkt (i,j) nie jest poszukiwanym końcem (w tym przypadku zwraca true),
- sprawdza czy punkt (i,j) nie jest ścianą lub punktem odwiedzonym wcześniej (w tym przypadku zwraca false),
- zaznacza punkt (i,j) jako odwiedzony
- próbuje szukać drogi z punktów sąsiednich dla punktu (i,j), o ile sąsiedzi Ci istnieją.
Zauważ, że testy 1 i 2, mogłyby być wykonane przed każdym wywołaniem funkcji 'szukaj' w punkcie 4 (oraz w otoczce) zamiast na jej początku, jednak prowadziłoby to do wielokrotnego pisania bardzo podobnych fragmentów programu, co łatwo prowadzi do błędów. Oczywiście nie zmniejszyłoby to złożoności czasowej i pamięciowej.
Podobnie jak testy 1 i 2, testy istnienia sąsiadów (i>1), (i<M) itp. w punkcie 4 mogłyby być wykonane na początku funkcji 'szukaj' (wtedy nie zakładalibyśmy, że (i,j) jest poprawnym punktem w tablicy), ale nie mając wiedzy w którą stronę ostatnio poszliśmy, musielibyśmy sprawdzić pełną poprawność obu współrzędnych (i,j), czyli w sumie sprawdzalibyśmy 4 warunki. W aktualnej wersji sprawdzamy tylko 1 warunek.
Poprawność rozwiązania: Oczywiste jest, że jeśli funkcja 'Labirynt' da wynik true, to ścieżka z (i1,j1) do (i2,j2) istnieje. Mniej oczywiste jest, że jeśli funkcja da wynik false, to ścieżka na pewno nie istnieje.
Aby przeprowadzić dowód przez sprzeczność, załóżmy, że funkcja 'szukaj' wywołana w funkcji 'Labirynt' dała wynik false, a ścieżka z (i1,j1) do (i2,j2) istnieje. W takim razie A[i1,j1]=-1 a A[i2,j2]=1. Wynika z tego, że ścieżka z (i1,j1) do (i2,j2) w którymś miejscu opuszcza zaznaczoną część tablicy, czyli istnieją dwa sąsiednie pola (i,j) i (i',j') na tej ścieżce, takie że A[i,j]=-1, A[i',j']=1. Z tego wynika, że funkcja szukaj została (w czasie działania programu) wywołana z parametrami (i,j), ale nie została wywołana z parametrami (i',j'). Jest to niemożliwe, bo pola te sąsiadują ze sobą i wywołanie dla (i,j) wywołałoby rekurencyjnie 'szukaj' dla (i',j').
Ćwiczenie 1
Ile razy maksymalnie może być wywołana funkcja 'szukaj' dla tego samego punktu?
Ćwiczenie 2
Jak przerobić przedstawione rozwiązanie na program wypisujący na ekran ścieżkę pomiędzy punktami (i1,j1) i (i2,j2), o ile taka ścieżka istnieje? Czy wypisana ścieżka będzie najkrótsza z możliwych ?
Ćwiczenie 3
Co by się stało, gdybyśmy usuwali zaznaczenie wychodząc z funkcji 'szukaj'? Czy program dalej by działał? Jeśli tak, to jaką by miał złożoność?
Odpowiedź
Program dalej by działał, ale miałby wykładniczą złożoność czasową zamiast liniowej (złożoność pamięciowa pozostałaby liniowa). W tej wersji program wielokrotnie przeszukiwałby sąsiadów pól, o których już z wcześniejszych obliczeń wiadomo, że nie da się z nich dojść do punktu końcowego.
Dla ciekawskich:
Rozwiązanie przedstawione powyżej polega na przeszukiwaniu wgłąb grafu reprezentującego labirynt. Inne, równie dobre rozwiązanie polega na przeszukiwaniu tego grafu wszerz, zwanego również metodą płonącego lasu. Wymaga ono użycia struktury danych -- kolejki. Rozwiązanie to łatwo przerobić na program wypisujący najkrótszą ścieżkę pomiędzy punktami (i1,j1) i (i2,j2), o ile taka ścieżka istnieje.
Zadanie 2 (Z górki na pazurki)
W tablicy liczb całkowitych o rozmiarze M×N zapisana jest mapa
gór (każdy punkt ma podaną dodatnią wysokość). Sprawdź, czy da się
przejść z punktu startowego (i1,j1) do docelowego (i2,j2) idąc:
- tylko w dół lub po płaskim
- tylko w dół
Tak jak w poprzednim zadaniu poruszać się można tylko w czterech kierunkach podstawowych, nie po przekątnej.
Wskazówka 1
Zadanie to należy rozwiązywać tak jak poprzednie. Jedyną różnicą jest to, jak należy decydować czy z danego pola można przejść do sąsiedniego: oprócz zaznaczenia trzeba wziąć pod uwagę różnicę wysokości.
Rozwiązanie 1
function Zjazd1(M,N:integer; var A:array[1..M,1..N] of integer; i1,j1,i2,j2:integer):boolean;
// M,N >= 1
// A zawiera liczby > 0
function szukaj(h,i,j:integer):boolean;
// 0 < h
// 1 <= i <= M
// 1 <= j <= N
var jest:boolean;
hpom:integer;
begin
jest:=false;
if (A[i,j]>0) and (h>A[i,j])then begin // h>=A[i,j] w wariancie II
if (i=i2) and (j=j2) then
jest:=true
else begin
hpom:=A[i,j];
A[i,j]:=-A[i,j];
if i>1 then jest:=szukaj(hpom,i-1,j);
if not jest and (i<M) then jest:=szukaj(hpom,i+1,j);
if not jest and (j>1) then jest:=szukaj(hpom,i,j-1);
if not jest and (j<N) then jest:=szukaj(hpom,i,j+1);
end
end;
szukaj:=jest
end; // szukaj
var i,j:integer;
begin // Zjazd1
if not((1<=i1) and (i1<=M) and (1<=j1) and (j1<=N) and
(1<=i2) and (i2<=M) and (1<=j2) and (j2<=N)) then
Zajzd:=false
else
begin
Zjazd:=szukaj(A[i1,j1]+1,i1,j1); // wystarczy szukaj(A[i1,j1],i1,j1) w wariancie II
for i:=1 to M do
for j:=1 to N do
if A[i,j]<0 then A[i,j]:=-A[i,j]
end
end; // Zjazd1
Koszt czasowy: liniowy względem rozmiaru A (czyli M×N)
Koszt pamięciowy: liniowy względem rozmiaru A
Opis: Podobnie jak w poprzednim zadaniu, przeszukujemy tablicę wgłąb, jednak przechodzimy tylko do tych pól sąsiednich, które mają mniejszą (odpowiednio: mniejszą lub równą) wysokość.
Dyskusja: Tak jak w rozwiązaniu poprzedniego zadania, istnienie sąsiada na planszy badamy przed wywołaniem rekurencyjnym, a poprawność przejścia na początku wywołania. Służy nam do tego dodatkowy parametr 'h', który oznacza wysokość poprzedniego pola. Dlatego pierwsze wywołanie funkcji 'szukaj' ma pierwszy parametr A[i1,j1]+1 (w wariancie I). Uwaga! Funkcja ta nie zadziała, jeśli A[i1,j1]=MaxInt (czyli maksymalnej możliwej wartości typu integer). Poniżej przedstawione jest rozwiązanie bez tego mankamentu.
Rozwiązanie 2
function Zjazd2(M,N:integer; var A:array[1..M,1..N] of integer; i1,j1,i2,j2:integer):boolean;
// M,N >= 1
// A zawiera liczby > 0
function wdol(h,i,j:integer):boolean;
// sprawdza czy pole (i,j) nie jest zaznaczone i
// czy przejscie z pola o wysokosci h na pole (i,j) jest rzeczywiscie w dół
// 1 <= i <= M
// 1 <= j <= N
begin
wdol:=(A[i,j]>0) and (h>A[i,j]) // h>=A[i,j] w wariancie II
end; //wdol
function szukaj(i,j:integer):boolean;
// 1 <= i <= M
// 1 <= j <= N
var jest:boolean;
hpom:integer;
begin
if (i=i2) and (j=j2) then
jest:=true
else begin
jest:=false;
hpom:=A[i,j];
A[i,j]:=-hpom;
if i>1 then if wdol(hpom,i-1,j) then jest:=szukaj(i-1,j);
if not jest and (i<M) then if wdol(hpom,i+1,j) then jest:=szukaj(i+1,j);
if not jest and (j>1) then if wdol(hpom,i,j-1) then jest:=szukaj(i,j-1);
if not jest and (j<N) then if wdol(hpom,i,j+1) then jest:=szukaj(i,j+1);
end;
szukaj:=jest
end; // szukaj
var i,j:integer;
begin // Zjazd2
if not((1<=i1) and (i1<=M) and (1<=j1) and (j1<=N) and
(1<=i2) and (i2<=M) and (1<=j2) and (j2<=N)) then
Zajzd:=false
else
begin
Zjazd:=szukaj(i1,j1);
for i:=1 to M do
for j:=1 to N do
if A[i,j]<0 then A[i,j]:=-A[i,j]
end
end; // Zjazd2
Koszt czasowy: liniowy względem rozmiaru A (czyli M×N)
Koszt pamięciowy: liniowy względem rozmiaru A
Opis: Rozwiązanie jest bardzo podobne do poprzedniego. Różnica polega na przeniesieniu kodu z początku funkcji 'szukaj' do osobnej funkcji 'wdol'. Funkcja ta jest używana jako test dopuszczający do rekurencyjnego wywołania funkcji 'szukaj'.
Zadanie 3 (Wieże Hanoi z ograniczeniami)
Na wykładzie omawiane były wieże Hanoi. Ciekawa modyfikacja tego zadania polega na zabronieniu ruchów pomiędzy niektórymi pałeczkami, np. z pierwszej na drugą. Zapisać procedurę realizującą to zadanie przy zabronionych niektórych ruchach.
Wskazówka 1
- Dozwolone ruchy najlepiej reprezentować w tablicy typu TDozwRuchow = array[1..3, 1..3] of boolean, (przekątna nie ma znaczenia).
- Zastanów się, jaki warunek musi być spełniony przez tablicę dozwolonych ruchów, aby zadanie miało rozwiązanie.
- Użyj funkcji-otoczki do wstępnego sprawdzenia czy istnieje rozwiązanie oraz do pamiętania tablicy dozwolonych ruchów w czasie działania funkcji rekurencyjnej.
Rozwiązanie 1
type Paleczki = 1..3;
procedure Hanoi(Ile: integer; Skad, Dokad: Paleczki; Dozw: TDozwRuchow);
procedure Rob(Skad, Dokad, Pom: Paleczki; Ile: Integer);
// Pom jest zawsze rowne 6-Skad-Dokad, ale z parametrem jest czytelniej
begin
if Ile > 0 then
if Dozw[Skad, Dokad] then begin
Rob(Skad, Pom, Dokad, Ile-1);
Przenies(Skad,Dokad);
Rob(Pom, Dokad, Skad, Ile-1);
end
else begin
Rob(Skad, Dokad, Pom, Ile-1);
Przenies(Skad, Pom);
Rob(Dokad, Skad, Pom, Ile-1);
Przenies(Pom, Dokad);
Rob(Skad, Dokad, Pom, Ile-1);
end
end; //Rob
function CzyDaSie(Skad, Dokad, Pom: Paleczki):boolean
begin
if not Dozw[Skad, Dokad] and not (Dozw[Skad, Pom] and Dozw[Pom, Dokad]) then begin
writeln('Nie da sie przełożyć krążka z ',Skad,' na ',Pom);
CzyDaSie:=false
end
else
CzyDaSie:=true
end // CzyDaSie
var ok:boolean;
i,j:integer;
begin // Hanoi
ok:=true;
for i:=1 to 2 do for j:=i+1 to 3 do
ok:=ok and CzyDaSie(i,j,6-i-j) and CzyDaSie(j,i,6-i-j);
if ok then Rob(Skad,Dokad,6-Skad-Dokad,Ile);
end; // Hanoi
Koszt czasowy: wykładniczy ze względu na Ile
Koszt pamięciowy: liniowy ze względu na Ile
Omówienie: Zauważ, że dużo łatwiej jest rozwiązać to zadanie sformułowane w sposób ogólny, niż gdy powie się, że zabroniony jest konkretny ruch (np. 1 → 2).
Ćwiczenie 1
Jaki przypadek jest najgorszy i ile trzeba wykonać wtedy ruchów?
Odpowiedź
Najgorszy przypadek to taki, gdy przerwane jest połączenie Skąd-Dokąd w obie strony. Wtedy trzeba wykonać ruchów)
Zadanie 4 (Ustawianie hetmanów)
Napisz procedurę znajdująca wszystkie takie rozstawienia 8 hetmanów na szachownicy, by żadne dwa hetmany się nie atakowały.
Wskazówka 1
W tablicach logicznych pamiętamy zajętość kolumn i obu rodzajów przekątnych, kolejnego hetmana ustawiamy w kolejnym wierszu jeśli się da, jeśli nie wracamy.
Rozwiązanie jest ładnie opisane w dużym Wirthcie, str. 155-160.
Oczywiście lepiej uogólnić na N hetmanów na szachownicy N×N.
Rozwiązanie 1
procedure Hetmany(N:integer);
// N >= 1
var
wiersze:array[1..N] of integer;
kolumny:array[1..N] of boolean;
przekgd:array[-(N-1)..N-1] of boolean; // przekątne \ o stałej różnicy
przekdg:array[2..2*N] of boolean; // przekątne / o stałej sumie
procedure ustaw(k,i:integer);
// 1 <= k,i <= N
begin
wiersze[k]:=i;
kolumny[i]:=true;
przekgd[k-i]:=true;
przekdg[k+i]:=true;
end; // ustaw
procedure odstaw(k,i:integer);
// 1 <= k,i <= N
begin
wiersze[k]:=0;
kolumny[i]:=false;
przekgd[k-i]:=false;
przekdg[k+i]:=false;
end; // odstaw
function wolne(k,i:integer):boolean;
// 1 <= k,i <= N
begin
wolne:=not kolumny[i] & not przekgd[k-i] & not przekdg[k+i];
end; // wolne
var jest:boolean;
procedure wypisz;
var i:integer;
begin
jest:=true;
write('Ustawienie: ');
for i:=1 to N do write(i,':',wiersze[i],' ');
writeln;
end; // wypisz
procedure rozstaw(k:integer);
var i:integer;
begin
if k=N+1 then
wypisz
else
for i:=1 to N do
if wolne(k,i) then begin
ustaw(k,i);
rozstaw(k+1);
odstaw(k,i);
end
end; // rozstaw
var i:integer;
begin // Hetmany
for i:=1 to N do wiersze[i]:=0;
for i:=1 to N do kolumny[i]:=false;
for i:=-(N-1) to N-1 do przekgd[i]:=false;
for i:=2 to 2*N do przekdg[i]:=false;
jest:=false;
rozstaw(1);
if not jest then writeln('Nie znaleziono żadnego ustawienia')
end // Hetmany
Koszt czasowy: wykładniczy względem N
Koszt pamięciowy: liniowy względem N
Opis: Aby ustawić N hetmanów, każdy z nich musi stać w osobnym wierszu. Dlatego główna procedura rekurencyjna 'rozstaw' próbuje ustawić k-tego hetmana w wierszu k w kolejnych kolumnach. Jeśli to się uda (wolne(k,i)=true), zaznacza zajętość odpowiedniej kolumny i przekątnych oraz wywołuje się rekurencyjnie, aby rozstawić pozostałe hetmany. Po powrocie z wywołania rekurencyjnego kolumna i przekątne są zwalniane i pętla for próbuje ustawić k-tego hetmana w kolejnej kolumnie.
Jeśli nie ma już żadnych hetmanów do rozstawiania (k=N+1) oznacza to, że N jest już ustawionych - pozostaje ich wypisać. Przy okazji zmienna 'jest' rejestruje czy jakiekolwiek rozwiązanie zostało wypisane. Jeśli nie, główna procedura 'Hetmany' wypisuje stosowny komunikat.
Zadanie 5 (Mnożenie wielomianów)
Dane są dwie tablice (array[0..N-1] of real) reprezentujące dwa wielomiany stopnia N-1. Należy obliczyć iloczyn tych wielomianów metodą dziel-i-zwyciężaj. Zakładamy, że N jest potęgą dwójki.
Wskazówka 1
Zamiast rozwiązania naiwnego wymagającego mnożeń zróbmy trochę lepsze (aczkolwiek nie najlepsze) polegające na podzieleniu wielomianów na dwie części. Cały pomysł polega na spostrzeżeniu, że jeśli zadane wielomiany i podzielimy na dolną i górną część
i analogicznie
, to ich iloczyn można zapisać tak:
, gdzie
Czyli potrzebujemy 3 mnożeń o połowę mniejszych wielomianów. W sumie uzyskamy liczbę mnożeń rzędu . Szczegóły można znaleźć w Sedgewick Algorithms in C++, str. 527-530.
Zadanie 6 (Suma składników)
Napisz procedurę, która wypisze dla zadanej liczby n jej wszystkie rozkłady na sumy liczb naturalnych większych od zera ustawionych w kolejności nierosnącej. Np. dla n = 3:
3 = 3
3 = 2+1
3 = 1+1+1
Wskazówka 1
Użyj dodatkowej tablicy do przechowywania początków rozkładów. Uważaj, aby nie kopiować tej tablicy przy każdym wywołaniu rekurencyjnym!
Wskazówka 2
Funkcja rekurencyjna powinna mieć parametr wskazujący jak duże składniki mogą być użyte do rozkładu pozostałej liczby.
Rozwiązanie 1
procedure Suma(n:integer);
// 1 <= n
var T:array[1..n] of integer;
procedure rozklad(var T:array[1..n] of integer; ti,co,maxSkł:integer)
// rozkładamy liczbę co używając składników nie większych niż maxSkł
// rozkłady wpisujemy do tablicy T; pierwsze wolne miejsce to ti+1
// 0 <= ti <= n
// ti > 0 --> maxSkł=T[ti]
// 0 <= co
// co=0 --> ti > 0
var i:integer
begin
if co=0 then begin
write(n,' = ');
for i:=1 to ti-1 do write(T[i],'+');
writeln(T[ti])
end
else begin
ti:=ti+1;
for i:=min(co,maxSkł) downto 1 do begin
T[ti]:=i;
rozklad(T,ti,co-i,i);
end
end
end //rozklad
begin
rozklad(T,0,n,n);
end
Koszt czasowy: wykładniczy względem n
Koszt pamięciowy: liniowy względem n
Wizualizacja
Opis: Funkcja rekurencyjna 'rozkład' przegląda wszystkie nierosnące rozkłady liczby co używające składników niewiększych niż maxSkł. Ponieważ w tablicy T[1..ti] jest już nierosnący ciąg składników o sumie n-co, wydłużenie go o dowolny taki rozkład co będzie dawać poprawny rozkład n.
Istotnie, jeśli co=0, w tablicy jest już gotowy rozkład n, więc wypisujemy go. W przeciwnym razie zwiekszamy ti, wszystkie liczby i, które mogą znaleźć się w tym miejscu rozkładu umieszczamy kolejno w T[ti] i wywołujemy rekurencyjnie funkcję 'rozklad', aby do wydłużonego rozkładu dopisała wszystkie rozkłady co-i o pierwszym składniku nieprzekraczającym i.
Zwróć uwagę, że tablica T przekazywana jest przez zmienną i dlatego jej zawartość nie jest kopiowana. Tablica T mogłaby też być zmienną globalną (dla procedury 'Suma'), ponieważ i tak wszystkie wywołania funkcji rozklad korzystają z tej samej tablicy zadeklarowanej w głównej części procedury 'Suma'.
Zauważ, że można by też tak napisać procedurę 'rozklad', aby nigdy nie wywoływała się rekurencyjnie dla co=0. Wypisywanie pełnego rozkładu miałoby miejsce wtedy, gdy min(maxSkł,co)=co (czyli maxSkł>=co) i oczywiście pętla 'for' zaczynałaby się w takim przypadku od co-1.
Druga możliwa modyfikacja polega na zrezygnowaniu z parametru maxSkł, ponieważ maxSkł albo jest równe T[ti], albo co jeśli ti=0. Można by też użyć strażnika T[0]=n i zawsze mielibyśmy maxSkł=T[ti].
Poza wspomnianymi modyfikacjami całą procedurę 'rozkład' można by napisać w sposób rosnący, tzn. zamiast generować coraz mniejsze składniki - generować coraz większe, aż osiągniemy bądź przekroczymy n. W takim wypadku składniki należałoby wypisywać w odwrotnej kolejności.