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= pk 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ć [ pk1 2 ] , a nie [ ] . Wynika to pk 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