• Nie Znaleziono Wyników

Zadanie: Pomniki

3.8. Para najbliższych punktów

W rozdziale dotycz¸acym wyznaczania wypukłej otoczki zbioru Literatura [WDA] - 35.4 [ASD] - 8.4.1 punktów wspomnieliśmy o algorytmie wyznaczania pary

najdal-szych punktów. W niniejszym rozdziale zajmiemy się podobnym problemem — dla danego zbioru punktów S na płaszczyźnie należy wyznaczyć parę najbliższych punktów.

Nasuwaj¸acym się rozwi¸azaniem tego problemu jest sprawdzenie każdej pary punktów oraz wybranie tych dwóch, które s¸a najbliżej siebie. Rozwi¸azanie takie jest bardzo proste w implementacji, niestety jego złożoność czasowa nie jest zachwycaj¸aca i wynosi O(n2).

Istnieje algorytm pozwalaj¸acy na znalezienie pary najbliższych punktów w czasie O(n ∗ log(n)) i działaj¸acy w oparciu o technikę dziel i zwyciężaj. Na samym pocz¸atku tworzone s¸a dwie kopie zbioru punktów. Jedna z nich sortowana jest w kolejności niemalej¸acych odciętych (zbiór ten nazwiemy Sx), druga w kolejności niemalej¸acych rzędnych (zbiór Sy). Po wykonaniu tego kroku następuje przejście do fazy dzielenia zbioru punktów. Wyznaczana jest prosta l, równoległa do osi y, taka że po obu jej stronach znajduje się tyle samo punktów ze zbioru S (patrz rysunek 3.12.b). W ten sposób powstaj¸a dwa zbiory punktów X oraz Y . X zawiera punkty położone na lewo od prostej l, natomiast Y — punkty położone na prawo od prostej l.

Rekurencyjnie rozwi¸azane zostaj¸a podproblemy dla obu części X i Y , na skutek czego wyznaczone zostaj¸a dwie odległości d oraz e, oznaczaj¸ace odpowiednio odległość najbliższej pary punktów w zbiorze X oraz Y . Załóżmy, bez straty ogólności, że d < e.

Kolejnym krokiem algorytmu jest scalanie podrozwi¸azań. Oczywistym jest, że para na-jbliższych punktów dla całego zbioru S nie może być oddalona o więcej niż d. Możliwe s¸a dwa przypadki — albo najbliższa para należy do zbioru X (wtedy jest to wyznaczona para punktów oddalonych o d), albo jeden z punktów należy do zbioru X, a drugi do zbioru Y . W przypadku tej drugiej sytuacji, punkty s¸a oddalone od siebie o odległość mniejsz¸a niż d, więc każdy z nich musi leżeć nie dalej niż o d od prostej l — podzbiory X i Y punktów znajduj¸acych się nie dalej niż o d oznaczamy odpowiednio przez X0 i Y0 (rysunek 3.12.c).

To, co pozostaje do zrobienia, to wyznaczenie wszystkich par punktów u ∈ X0oraz v ∈ Y0 takich, że |u 7→ v| < d. Ponieważ odległość dowolnej pary punktów, zarówno w zbiorze X0, jak

i Y0wynosi co najmniej d, zatem liczba punktów ze zbiorów X0oraz Y0w dowolnym kwadracie o boku długości d jest ograniczona przez liczbę 5. Aby wyznaczyć wszystkie poż¸adane pary punktów u i v, wystarczy dla każdego punktu ze zbioru X0 sprawdzić 5 najbliższych pod względem współrzędnej y punktów ze zbioru Y0. Można tego dokonać w czasie liniowym ze względu na sumaryczn¸a moc zbiorów X oraz Y , dzięki wcześniejszemu posortowaniu punktów względem współrzędnej y.

Implementacja opisanej wyżej metody jest realizowana przez strukturę NearestPoints.

Do jej konstruktora przekazywany jest wektor punktów, dla których chcemy znaleźć parę punktów najbliższych. Wynik — najmniejsz¸a odległość — można odczytać z pola double dist, natomiast para najbliższych punktów jest wskazywana przez wskaźniki —POINT *p1,

*p2. Implementacja przedstawiona jest na listingu 3.46.

Listing 3.46: Implementacja strukturyNearestPoints 01 struct NearestPoints {

02 vector<POINT*> l;

// Wskaźniki na dwa punkty, stanowiące znalezioną parę najbliższych punktów 03 POINT *p1, *p2;

// Odległość między punktami p1 i p2 04 double dist;

// Funkcja usuwa z listy l wszystkie punkty, których odległość // od prostej x=p jest większa od odległości między parą aktualnie // znalezionych najbliższych punktów

05 void Filter(vector<POINT*> &l, double p) { 06 ints = 0;

07 REP(x, SIZE(l))

08 if (sqr(l[x]->x - p) <= dist) l[s++] = l[x];

09 l.resize(s);

10 }

// Funkcja realizuje fazę dziel i zwyciężaj dla zbioru punktów z wektora l // od pozycji p do k. Wektor ys zawiera punkty

// z przetwarzanego zbioru posortowane w kolejności niemalejących współrzędnych y 11 void Calc(intp, int k, vector<POINT*> &ys) {

// Jeśli zbiór zawiera więcej niż jeden punkt, to następuje faza podziału 12 if (k - p > 1) {

13 vector<POINT*> lp, rp;

// Wyznacz punkt podziału zbioru 14 intc = (k + p - 1) / 2;

// Podziel wektor ys na dwa zawierające odpowiednio punkty // na lewo oraz na prawo od punktu l[c]

15 FOREACH(it, ys)

16 if (OrdXY(l[c], *it)) rp.PB(*it); else lp.PB(*it);

// Wykonaj fazę podziałów 17 Calc(p, c + 1, lp);

18 Calc(c + 1, k, rp);

// Następuje faza scalania. Najpierw z wektorów l i r usuwane

// są punkty położone zbyt daleko od prostej wyznaczającej podział zbiorów 19 Filter(lp, l[c]->x);

20 Filter(rp, l[c]->x);

21 intp = 0; double k;

Listing 3.46: (c.d. listingu z poprzedniej strony)

// Następuje faza wyznaczania odległości pomiędzy kolejnymi parami punktów, // które mogą polepszyć aktualny wynik

22 FOREACH(it, lp) {

23 while(p < SIZE(rp) - 1 && rp[p + 1]->y < (*it)->y) p++;

24 FOR(x, max(0, p - 2), min(SIZE(rp) - 1, p + 1))

// Jeśli odległość między parą przetwarzanych punktów jest mniejsza od aktualnego wyniku,

// to zaktualizuj wynik

25 if(dist > (k = sqr((*it)->x - rp[x]->x) + 26 sqr((*it)->y - rp[x]->y))) {

27 dist = k;

28 p1 = *it;

29 p2 = rp[x];

30 }

31 }

32 }

33 }

// Konstruktor struktury NearestPoints wyznaczający parę najbliższych punktów 34 NearestPoints(vector<POINT> &p) {

// Wypełnij wektor l wskaźnikami do punktów z wektora p

// oraz posortuj te wskaźniki w kolejności niemalejących współrzędnych x 35 FOREACH(it, p) l.PB(&(*it));

36 sort(ALL(l), OrdXY);

// Jeśli w zbiorze istnieją dwa punkty o tych samych współrzędnych, // to punkty te są poszukiwanym wynikiem

37 FOR(x, 1, SIZE(l) - 1)

38 if (l[x - 1]->x == l[x]->x && l[x - 1]->y == l[x]->y) {

39 dist = 0;

40 p1 = l[x - 1]; p2 = l[x]; return;

41 }

42 dist = double(INF) * double(INF);

// Skonstruuj kopię wektora wskaźników do punktów i posortuj go w kolejności // niemalejących współrzędnych y

43 vector<POINT*> v = l;

44 sort(ALL(v), OrdYX);

// Wykonaj fazę dziel i rządź dla wszystkich punktów ze zbioru 45 Calc(0, SIZE(l), v);

46 dist = sqrt(dist);

47 } 48};

Listing 3.47: Przykład wykorzystania struktury NearestPointsna zbiorze punktów z rysunku 3.12

Wyznaczona odleglosc: 4.12311

Znaleziona para najblizszych punktow:

(-7, 5) (-3, 4)

Listing 3.48: Kod źródłowy programu użytego do wyznaczenia wyniku z listingu 3.47. Pełny kod źródłowy programu znajduje się w pliku nearestpoints str.cpp

01 intmain() { 02 int n;

03 vector<POINT> l;

04 POINT p;

// Wczytaj liczbę punktów

05 cin >> n;

// Wczytaj kolejne punkty 06 REP(x, n) {

07 cin >> p.x >> p.y;

08 l.PB(p);

09 }

// Wyznacz parę najbliższych punktów oraz wypisz wynik 10 NearestPoints str(l);

11 cout << "Wyznaczona odleglosc: " << str.dist << endl;

12 cout << "Znaleziona para najblizszych punktow:" << endl;

13 cout << *(str.p1) << " " << *(str.p2) << endl;

14 return 0;

15}

Ćwiczenia

Proste Średnie Trudne

acm.uva.es - zadanie 10245 acm.sgu.ru - zadanie 120 acm.uva.es - zadanie 10750

Rozdział 4

Kombinatoryka

Wiele zadań, które zawodnik musi rozwi¸azać podczas zawodów, Literatura [KDP] - 1 [SZP] - 1.2.5

[ASP] - 3 należy do klasy problemów NP-trudnych. Stosunkowo dużo tego

ty-pu zadań pojawia się na konkursach ACM czy TopCoder. Rozpoz-nanie zadania tego typu jest zazwyczaj stosunkowo łatwe — wszys-tkie one charakteryzuj¸a się bardzo ścisłymi limitami na wielkość danych wejściowych (umiejętność szacowania złożoności

wymagane-go alwymagane-gorytmu na podstawie ograniczeń na wielkość danych wejściowych jest bardzo istotna).

Jak wiadomo, nie s¸a znane algorytmy pozwalaj¸ace na rozwi¸azywanie zadań NP-trudnych w czasie wielomianowym — często stosowane techniki bazuj¸a na programowaniu dynamicznym, które zasadniczo polega na wyznaczaniu wyników dla pewnej klasy podproblemów, na pod-stawie których można obliczyć wynik dla postawionego problemu. Liczba podproblemów, które należy rozpatrzeć w przypadku zadań NP-trudnych jest wykładnicza, co nie tylko wi¸aże się z długim czasem ich generowania, ale również wymaga dużej ilości pamięci potrzebnej na spamiętywanie wyników częściowych. Programowanie dynamiczne w przypadku wielu zadań nie jest również proste z punktu widzenia implementacyjnego.

Czasem rozs¸adniejszym podejściem okazuje się wygenerowanie wszystkich możliwych roz-wi¸azań do postawionego problemu, a następnie wybranie spośród nich najlepszego. Rozwi¸aza-niem może być przykładowo odpowiednie uporz¸adkowanie pewnych obiektów (wyznaczany porz¸adek można reprezentować przy użyciu permutacji), czy też wybranie pewnego ich pod-zbioru.

W przypadku wielu zadań NP-trudnych okazuje się, że umiejętność efektywnego generowa-nia rozmaitych obiektów kombinatorycznych jest podstaw¸a do szybkiego rozwi¸azagenerowa-nia zada-nia. W aktualnym rozdziale przedstawimy kilka algorytmów pozwalaj¸acych na generowanie wszystkich permutacji oraz podzbiorów danego zbioru, jak również podziałów liczby.

Świetnym źródłem informacji, na bazie którego powstała zawartość tego rozdziału jest ksi¸ażka Witolda Lipskiego „Kombinatoryka dla programistów”, do której lektury gor¸aco zachęcamy.