« poprzedni punkt | następny punkt » |
Trzeci algorytm rozwiązywania problemu min-max, to algorytm rekurencyjny. Z zasadą "dziel i zwyciężaj", którą tu wykorzystano spotkaliśmy w wcześniej w algorytmie binarnych poszukiwań. Warto ją zapamiętać, gdyż będzie jeszcze wielokrotnie wykorzystywana. Zasada ta głosi, że aby rozwiązać zadanie rozmiaru n należy najpierw podzielić je na podzadania o mniejszych rozmiarach ( np. na dwa podzadania rozmiaru n/2), rozwiązać je i z uzyskanych rozwiązań podzadań wywnioskować rozwiązanie końcowe.
W konkretnym przypadku problemu min-max, zastosowanie zasady "dziel i zwyciężaj" będzie polegało na podzieleniu danych na dwie, możliwie równe części, znalezienie elementów największego i najmniejszego w każdej z nich, a następnie ustalenie elementu najmniejszego całego ciągu, przez porównanie wybranych elementów najmniejszych i ustalenie elementu największego przez porównanie elementów największych każdej z dwóch części:
Algorytm zostanie zapisany jako funkcja min_max3, która zwraca obiekt typu para o dwóch atrybutach min i max. Funkcja ta ma trzy parametry, którymi są: dany ciąg oraz dwa indeksy wskazujące początek i koniec rozważanego fragmentu tego ciągu. W pierwszym wykonaniu funkcji te parametry powinny mieć odpowiednio wartości 1 i n. Dla uproszczenia rozważań, przyjmijmy, że n (tzn. liczba elementów w ciągu) jest potęgą 2.
wp = {($k)(n = 2k)}, wk ={result.min = min(e), result.max= max(e)},
gdzie min(e) oznacza element najmniejszy, a max(e) - element największy w ciągu e.
para | min_max3 (e: ciąg, i, j : int); | ||
if (i+1=j) then | |||
if e[i] £ e[j] then | |||
result.min := e[i]; result.max := e[j]; | |||
else | |||
result.min := e[j]; result.max := e[i]; | |||
fi | |||
else | |||
x := min_max3 (e, i, (i+j)div 2); | // e[k] £ x.max, dla k= i, i+1, ..., (i+j)div 2 oraz | ||
// x.min £ e[k] , dla k= i, i+1, ..., (i+j)div 2 (założenie) | |||
y := min_max3 (e, (i+j)div 2+1, j); | // e[k] £ y.max, dla k= (i+j)div 2, ..., j-1, j oraz | ||
// y.min £ e[k], dla k= (i+j)div 2, ..., j-1, j (założenie) | |||
if (x.min £ y.min) then | |||
result.min := x.min | |||
else | // result.min £ e[k], dla k= i, ..., j | ||
result.min := y.min fi; | |||
if (y.max £ x.max) then result.max := x.max | |||
else | //e[k] £ result.max , dla k= i, ..., j | ||
result.max := y.max fi; | |||
fi | |||
} | // result.min £ e[k], e[k] £ result.max dla k=i, ..., j |
Zanim przejdziemy do analizy poprawności, prześledźmy na przykładzie, jak działa ten algorytm. Omawiany algorytm jest rekurencyjny, tzn. w jego treści będziemy się ponownie do niego odwoływać. Rozważmy ciąg o 16 elementach. Wartości tych elementów nie są teraz istotne. Zanotujmy tylko parametry kolejnych wywołań. Zapiszemy je w postaci grafu (por. Rys. 3.1). Każde wywołanie, o ile dotyczy ciągu o więcej niż dwóch elementach, spowoduje dwukrotne wywołanie funkcji min_max3. W rozważanym przykładzie pierwsze wywołanie ma parametry (1,16). Ponieważ ciąg ma więcej niż dwa elementy, zatem zostaną wykonane instrukcje po "else", tzn. wywołamy dwukrotnie funkcję min_max3 z parametrami (1,8) i (9,16). Wywołanie min_max3 dla ciągu 8 elementowego znów spowoduje dwa rekurencyjne wywołania funkcji z parametrami (1,4) i (5,8) itd. Pierwsze porównanie zostanie wykonane dopiero, gdy rozważany fragment ciągu ma dwa elementy.
Pytanie 3: W jakiej kolejności będą zakończone wywołania rekurencyjne funkcji min_max3 dla n=8?
Analiza poprawności algorytmu min_max3.
Udowodnimy, że jeżeli przed wywołaniem funkcji min_max3(e,i,j) spełniony jest warunek a(i,j) = {j-i+1 = 2k dla k>0}, to po wykonaniu algorytmu spełniony jest warunek b(i,j) = {result.min = min(e[i],...,e[j]), result.max = max(e[i],...,e[j]}.
Dowód poprawności algorytmu min_max3 zaczniemy od przypadku, gdy j-i=1 tzn. ciąg składa się tylko z dwóch elementów. W takim przypadku zostanie wykonana instrukcja warunkowa:
if e[i] £ e[j] then |
result.min := e[i]; result.max := e[j]; |
else |
result.min := e[j]; result.max := e[i]; |
fi; |
Po jej wykonaniu result.min jest elementem najmniejszym we fragmencie ciągu od e[i] do e[j] , a result.max jest elementem największym, co oznacza, że algorytm działa poprawnie dla ciągów dwuelementowych.
Załóżmy, że algorytm działa poprawnie dla wszystkich ciągów o długości 2(k-1), gdzie k>1, i rozważmy ciąg dwa razy dłuższy j-i+1=2k . Zgodnie z treścią algorytmu, wykonujemy dwa wywołania funkcji min_max3. Ponieważ (i+j)div 2 - i+1 = j - (i+j)div 2 = 2(k-1), zatem na mocy założenia indukcyjnego, po wykonaniu wywołań
x := min_max3 (e, i, (i+j)div 2); y := min_max3(e, (i+j)div 2+1, j);
spełnione są własności:
x.min £ e[l] £ x.max dla l = i, i+1, ..., (i+j)div2 oraz y.min £ e[l] £ y.max dla l = (i+j)div2 +1, ..., j.
Mniejsza z wartości x.min i y.min jest, na mocy przechodniości relacji £, niewiększa od e[i], e[i+1],..., e[(i+j)div2] oraz e[(i+j)div2+1], e[(i+j)div2+2], ..., e[j], czyli jest najmniejszym elementem ciągu e[i],...,e[j]. Większa z wartości x.max i y.max jest elementem największym ciągu e[i],...,e[j]. Zatem po wykonaniu instrukcji warunkowej, rozważane wywołanie funkcji min_max3 zakończy się i spełniony będzie warunek
b(i,j) = {result.min = min(e[i],...,e[j]), result.max = max(e[i],...,e[j]}.
Wykazaliśmy w ten sposób, że algorytm działa poprawnie dla ciągów o długości 2k. Na mocy zasady indukcji matematycznej, jeżeli dane spełniają warunek {j-i+1 = 2k dla k>0}, to po wykonaniu min_max3(e,i,j) otrzymane wyniki spełniają warunek b(i,j) dla dowolnej liczby naturalnej k>0. W szczególności, gdy n jest potęgą dwójki, wykonanie min_max(e,1,n) daje, dla dowolnego ciągu e, wyniki spełniające warunek końcowy specyfikacji algorytmu.
Twierdzenie 3.1 Dla dowolnych danych spełniających warunek wp = {($k)(n = 2k)}, w dowolnej strukturze, która ma określony liniowy porządek elementów, algorytm rekurencyjny min_max3 zatrzymuje się, a otrzymany wynik spełnia warunek końcowy
wk ={result.min = min(e), result.max= max(e)}.
Analiza kosztu algorytmu min_max3.
Niech T(n) oznacza koszt wykonania (mierzony liczbą wykonanych porównań) algorytmu min_max3 dla danych o rozmiarze n i niech n= 2k. Jeśli ciąg ma tylko dwa elementy, to algorytm wykonuje jedno porównanie w celu ustalenia, który z elementów jest większy. Jeśli ciąg ma więcej niż dwa elementy, to dzielimy go na dwie części i dwukrotnie wywołujemy funkcję min_max3. Na koniec wykonujemy dodatkowo 2 porównania by ustalić wartość elementu najmniejszego i największego. Korzystając z przyjętego oznaczenia możemy zdefiniować funkcję T rekurencyjnie:
- T(2k) = 2 ´ T(2k-1) + 2 dla k>1,
- T(2) = 1.
Rozwiązaniem tego równania rekurencyjnego jest funkcja T(2k) = 3 ´ 2(k-1) -2.
Rzeczywiście dla k=1, T(2) = 3´20 - 2 = 3 - 2 = 1, co jest zgodne z definicją funkcji T(n). Załóżmy, że T(2i)= 3 ´ 2(i-1) - 2 dla pewnego i. Udowodnimy prawdziwość wzoru dla i+. Korzystając kolejno z definicji rekurencyjnej i z założenia indukcyjnego otrzymujemy
T(2i+1) = 2´T(2i)
+ 2 = 2´(3 ´ 2(i-1)
-2) + 2 = 3 ´ 2
i - 2,
co należało udowodnić.
Ostatecznie, T(n) = 3n/2 - 2 dla wszystkich liczb naturalnych n, będących potęgą 2.
Wadą omawianej tu metody jest użycie rekursji, co jest zawsze związane z koniecznością zapamiętywania parametrów kolejnych wywołań. Zwykle tworzy się do tego celu stos. To oczywiście zajmuje czas i wymaga dodatkowego miejsca. W następnym punkcie omówimy wersję iteracyjną tego algorytmu.
Pytanie 4: Ile razy zostanie wywołana funkcja min_max3, jeśli w pierwszym wywołaniu parametrem aktualnym funkcji jest ciąg 128 elementowy oraz i = 1, j = 128?
« poprzedni punkt | następny punkt » |