Wyszukiwanie binarne

Transkrypt

Wyszukiwanie binarne
Wyszukiwanie binarne
Wyszukiwanie binarne to technika pozwalająca na przeszukanie jakiegoś
posortowanego zbioru danych w czasie logarytmicznie zależnym od jego
wielkości (co to dokładnie znaczy dowiecie się prawdopodobnie na najbliższym
kółku). Jak mieliście się okazję przekonać na zajęciach jest to też dobra
strategia w grze, w której ktoś wymyśla liczbę np. z przedziału od 1 do 1000, a
my staramy się ją zgadnąć.
Wyszukiwanie w posortowanej tablicy
Jest to jedno z najczęstszych i najprostszych zastosowań wyszukiwania
binarnego. Mając daną posortowaną niemalejąco tablicę T o n elementach
(indeksowanych od 0) chcemy w niej znaleźć pierwszy element, którego
wartość jest równa x. Zdefiniujmy sobie teraz przedział poszukiwania jako ten,
w którym znajduje się poszukiwany element, jeżeli jest w tablicy T. Indeks
odpowiadający początkowi tego przedziału będziemy oznaczali przez p,
a koniec przez k. Oczywiście początkowo prawdziwe jest, że p=0 i k=n-1
czyli, że przedział poszukiwania jest równy całej tablicy. W kolejnych krokach
wyszukiwania binarnego będziemy porównywali medianę (czyli element
o indeksie
[ ] , gdzie przez [a] oznaczam część całkowitą z a) z x. Jeżeli
m=
pk
2
mediana jest mniejsza niż x to z pewnością pierwszy element o wartości
równej x nie może się pojawić w przedziale od p do m, zatem możemy
przypisać p wartość m+1. Podobnie gdy mediana jest większa bądź równa x
to szukany element nie wystąpi w przedziale od m+1 do k, więc k przyjmie
wartość m. Algorytm kończy się, gdy przedział poszukiwania zawiera jeden
element, czyli p=k. W każdy krok przedział poszukiwania jest zawężany o
połowę, a więc wykonamy mniej więcej log2n kroków. Np. dla n = 1000
wykonamy 10 kroków, a dla n = 1000 000 jedynie 20. Widać więc, że algorytm
będzie działał szybko.
Przyjrzyjmy się teraz działaniu tego algorytmu na przykładowej tablicy.
W tabeli w kolejnych krokach wypisano tylko elementy z przedziału
poszukiwania, a mediany są zaznaczone żółtym kolorem.
krok 1
krok 2
1
1
2
3
3
4
5
6
8
8
8
9
9
10
6
8
8
8
9
9
10
krok 3
6
8
krok 4
6
8
8
8
8
wynik
Oto zapis tego algorytmu w języku C++.
Odpowiedni argument funkcji
Oczywiście
wyszukiwanie
binarne
można
używać
nie
tylko
na
elementach tablicy. Znając pewną funkcję możemy skorzystać z tej metody do
znalezienia przykładowo pierwszego argumentu większego niż dana wartość w
pewnym przedziale jej dziedziny. Musimy być jednak pewni, że funkcja jest w
tym przedziale monotoniczna. Warto dodać, że może być to funkcja określona
zarówno dla argumentów całkowitych, jak i rzeczywistych, jednak w drugim
przypadku będziemy musieli tolerować niedokładność otrzymanego wyniku.
Załóżmy, że mamy do rozwiązania problem znalezienia największej
możliwej liczby g (wśród liczb rzeczywistych nieujemnych), która spełnia
pewien warunek. Wiadomo, że jeżeli ten warunek jest spełniony dla jakiejś
wartości to jest też spełniony dla wartości od niej mniejszych i jeżeli warunek
nie jest spełniony dla jakiejś wartości to nie jest spełniony dla wartości
większych. Innymi słowami możemy zdefiniować funkcję F : ℝ+∪{0}{0,1}
przyjmującą wartości odpowiednio 1 i 0, w zależności od tego, czy argument
spełnia podany warunek, czy nie. Wiemy zatem, że funkcja ta jest nierosnąca
dla liczb rzeczywistych nieujemnych, a naszym zadaniem jest znaleźć
największe takie g, że F(g)=1.
W takim przypadku wyszukiwanie binarne będzie wyglądało trochę
inaczej niż do tej pory. Przedział poszukiwania będziemy także reprezentować
przy użyciu dwóch zmiennych p (równej początkowo 0) i k, której przypiszemy
odpowiednio dużą wartość, taką by F(k)=0. Najistotniejszą różnicą pomiędzy
wyszukiwaniem binarnym w funkcjach określonych dla liczb całkowitych
(przykładem może być po prostu tablica), a tych na liczbach rzeczywistych jest
warunek przerywający wyszukiwanie. W pierwszym przypadku kolejne kroki
są
wykonywane
dopóki
p≠k.
Gdy
jednak
wyszukujemy
wśród
liczb
rzeczywistych taki warunek mógłby doprowadzić do niekończącej się pętli,
ze względu na niedokładność obliczeń zmiennoprzecinkowych. Aby pozbyć się
tego problemu najlepiej zawczasu obliczyć potrzebną nam dokładność i ustalić
liczbę iteracji algorytmu. Przykładowo, gdy nasz przedział poszukiwania jest
wielkości 1018 to po wykonaniu 100 kroków otrzymany błąd będzie mniejszy
niż 10-12. Taka dokładność powinna w zupełności wystarczyć.
Pozostaje już tylko zaimplementować ten algorytm:
Wyszukiwanie po wyniku
Kolejną metodę związaną z wyszukiwaniem binarnym przedstawię na
przykładzie następującego zadania pochodzącego z półfinałów konkursu ACM,
który odbył się w Sankt Petersburgu w 2001 roku:
Mamy n desek o długościach d1, d2, d3,...,dn. Chcemy je pociąć w ten
sposób, żeby otrzymać co najmniej z (z>0) sztachet o jednakowej długości g.
Pociętych fragmentów nie możemy w żaden sposób ze sobą łączyć. Jakie
najdłuższe sztachety możemy otrzymać?
Zadanie to przypomina przed chwilą rozważany problem odpowiedniego
argumentu funkcji. W łatwy sposób możemy stwierdzić, czy dla danej długości
g da się otrzymać z sztachet. Wystarczy sprawdzić ile można ich otrzymać z
każdej z n desek (a jest to oczywiście
[ ] ), otrzymane wartości zsumować
di
g
i przyrównać do z. Możemy zatem ponownie zdefiniować funkcję F, która
zwracać będzie 1 gdy suma jest nie mniejsza niż z i 0 w przeciwnym
przypadku. Łatwo zauważyć, że funkcja F będzie niemalejąca.
W dalszej części przyjmiemy dodatkowe założenie, że występujące dane
zadania i jego wynik mają być liczbami całkowitymi. Pozwoli nam to przyjrzeć
się trochę innej implementacji wyszukiwania binarnego, a także zwrócić
uwagę na bardzo często pojawiające się błędy.
Na początek przedstawiam implementację funkcji F, która mając dane
wartości n i z oraz długości d1, d2, d3,...,dn w tablicy d, stwierdza, czy możliwe
jest otrzymanie z sztachet długości g:
Funkcja jest oczywiście bardzo prosta. Na uwagę zasługuje jedynie
rozpatrzenie przypadku szczególnego, gdy g jest równe 0. Wszak zawsze da
się otrzymać z sztachet o długości 0, a dzięki temu nie musimy się martwić
o wykonywalność późniejszego dzielenia przez g.
Zajmijmy się teraz już samym wyszukiwaniem binarnym. Po pierwsze
musimy odpowiednio dobrać początkową wartość dla zmiennej k. Zgodnie ze
wcześniejszymi spostrzeżeniami powinna ona być taka, że F(k)=0. Nie trudno
zauważyć, że ten warunek spełnia chociażby długość największej z desek
powiększona o 1.
Po drugie tym razem nie wyszukujemy w funkcji określonej na liczbach
całkowitych pierwszego miejsca spełniające dany warunek, lecz ostatnie, które
spełnia. W związku z tym jeżeli dla mediany m F(m)=1, to nową wartość p
będzie m. Natomiast gdy F(m)=0 to możemy przyjąć k=m-1. Aby taki
algorytm się kończył musimy za medianę uznać
[
pk1
2
] , a nie [ ] . Wynika to
pk
2
z rozpatrzenia skrajnego przypadku, gdy p+1=k i F(p)=1, lecz F(k)=0.
Po trzecie wartości p i k mogą być na tyle duże, że ich suma przekroczy
zakres używanego typu zmiennych całkowitych. Aby ominąć ten problem
formułę
należy zastąpić równoważną
.
Po ominięciu tych pułapek uzyskujemy poprawnie działający program:
Algorytm ten wykonuje liczbę kroków proporcjonalną do logarytmu
zakresu danych wejściowych, każdy zaś z kroków w czasie proporcjonalnym
do n. Jeżeli najdłuższa z desek będzie miała długość max to wykona on mniej
więcej nlog2max kroków.
Niniejszy artykuł jest pierwszym z serii materiałów pomocniczych
przygotowywanych do kółek „Mazowieckie talenty”. Mam nadzieję, że pozwoli
nie tylko lepiej zrozumieć metody krótko omawiane na zajęciach, ale także
poznać szerokie możliwości jakie daje wyszukiwanie binarne. Będę bardzo
wdzięczny za wszelkie uwagi i komentarze do tego tekstu. Można o nich pisać
na
forum
na
stronie:
http://mtalenty.wikidot.com
lub
też
na
e-mail:
[email protected].
Błażej Osiński