You are on page 1of 49

Kunia Talentw Informatycznych: Algorytmika i programowanie Struktury danych i ich zastosowania

Marcin Andrychowicz, Bolesaw Kulbabiski, Tomasz Kulczyski, Jakub cki, Baej Osiski, Wojciech mietanka

Struktury danych i ich zastosowania

Rodzaj zaj: Kunia Talentw Informatycznych Tytu: Struktury danych i ich zastosowania Autor: Marcin Andrychowicz, Bolesaw Kulbabiski, Tomasz Kulczyski, Jakub cki, Baej Osiski, Wojciech mietanka Redaktor merytoryczny: prof. dr hab. Maciej M Syso Zeszyt dydaktyczny opracowany w ramach projektu edukacyjnego Informatyka+ ponadregionalny program rozwijania kompetencji uczniw szk ponadgimnazjalnych w zakresie technologii informacyjno-komunikacyjnych (ICT). www.informatykaplus.edu.pl kontakt@informatykaplus.edu.pl Wydawca: Warszawska Wysza Szkoa Informatyki ul. Lewartowskiego 17, 00-169 Warszawa www.wwsi.edu.pl rektorat@wwsi.edu.pl Projekt graficzny okadki: FRYCZ I WICHA Warszawa 2010 Copyright Warszawska Wysza Szkoa Informatyki 2009 Publikacja nie jest przeznaczona do sprzeday.

Struktury danych i ich zastosowania

Marcin Andrychowicz, Bolesaw Kulbabiski, Tomasz Kulczyski, Jakub cki, Baej Osiski, Wojciech mietanka

<4>

Informatyka +

Streszczenie
Celem kursu jest zapoznanie uczestnika z szeregiem rnych struktur danych. Prezentowane jest szerokie spektrum zagadnie: od podstawowych struktur wskanikowych jak stosy i kolejki, poprzez zbiory rozczne, drzewa przedziaowe i wyszukiwa binarnych, a do masek bitowych. Przydatno wymienionych struktur danych ilustruj liczne przykady zastosowa w algorytmach optymalizacyjnych, grafowych czy te geometrycznych, a take w rozwizaniach zada olimpijskich. Uczestnik zapoznawany jest take pobienie z kontenerami z biblioteki STL, ktre s prost w uyciu implementacj niektrych spord omawianych struktur. Zakadana jest znajomo jakiego jzyka programowania, najlepiej C++, gdy w nim napisane s fragmenty przykadowych programw. Znajomo podstaw algorytmiki (wyniesiona choby z kursu Przegld podstawowych algorytmw) bdzie dla uczestnika spor pomoc.

Spis treci
Streszczenie 1 Stos 2 Kolejka 3 Lista 4 Kopiec 4.1 Zastosowanie kopca w implementacji algorytmu Dijkstry . . . . . . . . . . . . . . . . . . . . . . . 5 Drzewa rozpinajce 6 Zbiory rozczne 7 Drzewa wyszukiwa binarnych (BST) 7.1 Zrwnowaone drzewa poszukiwa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Drzewa przedziaowe 8.1 Drzewo potgowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.2 Drzewa przedziaowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 5 6 7 9 12 14 16 19 22 24 25 27

9 Technika zamiatania 32 9.1 Zamiatanie ktowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 9.2 Sortowanie ktowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 10 Drzewa TRIE 36 11 Algorytm Aho-Corasick 39 11.1 Algorytm Bakera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 12 Maski bitowe 41 12.1 Programowanie dynamiczne na maskach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 12.2 Meet in the middle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 Literatura 45

> Struktury danych i ich zastosowania

<5>

1
.

Stos
Denicja 1. Stos to struktura danych, w ktrej dokadamy nowe elementy na szczycie stosu i zdejmujemy elementy poczwszy od szczytu stosu. Bdziemy chcieli, eby stos w pamici wyglda jak nastpuje: stos stos po zdjciu elementu ze szczytu stos po dodaniu nowego elementu top 15 10 42 top 10 42 top 17 10 42

Przyjrzyjmy si teraz implementacji stosu w jzyku C++ za pomoc operatorw new i delete. struct stack_element { // klasa element stosu int val; // warto w biecym elemencie stack_element *prev; // wskanik na poprzedni element stack_element(int _val, stack_element *_prev) { // konstruktor val = _val; // ustawiamy warto prev = _prev; // ustawiamy wskanik na poprzedni } } ; struct my_stack { stack_element *_top; int _size; my_stack() { // konstruktor _top = NULL; // ustawiamy szczyt stosu na NULL _size = 0; // rozmiar na 0 } void push(int a) { // dodawanie elementu do stosu _top = new stack_element(a, _top); // tworzymy nowy element, ktrego // poprzednikiem bdzie _top stosu ++_size; // zwikszamy rozmiar } void pop() { // usuwanie elementu ze stosu stack_element *tmp = _top; // w zmiennej pomocniczej pamitamy szczyt _top = tmp->prev; // obniamy szczyt delete tmp; // usuwamy stary szczyt --_size; // zmniejszamy rozmiar } } ;

<6>
wiczenie 1. Zaimplementuj dodatkowe metody: front zwracajc pierwszy element ze stosu size zwracajc liczb elementw na stosie empty zwracajc warto logiczn, czy stos jest pusty

Informatyka +

wiczenie 2. Uzasadnij, dlaczego stos jest odpowiedni do implementacji przeszukiwania metod DFS, ale nie nadaje si do implementacji przeszukiwania metod BFS. wiczenie 3. Przetestuj na komputerze dziaanie procedury pop, gdy wywouje si j na pustym stosie. wiczenie 4. Za pomoc kontenera stack z biblioteki STL zaimplementuj przeszukiwanie grafu metod DFS.

2
.

Kolejka
Denicja 2. Kolejka to struktura danych, w ktrej dokadamy elementy na koniec i pobieramy elementy z przodu kolejki. W realnym wiecie wystpuje jako kolejka w sklepie: najpierw obsugiwany jest klient z przodu kolejki, za nowi klienci ustawiaj si na samym kocu kolejki. W pamici komputera chcielibymy reprezentowa kolejk w taki sposb: head 17 10 42 tail

Przyjrzyjmy si teraz implementacji kolejki w jzyku C++ za pomoc operatorw new i delete. struct queue_element { // element kolejki int val; queue_element *next; queue_element(int _val, queue_element *_next) { // konstruktor val = _val; next = _next; } } ; struct my_queue { queue_element *head, *tail; int _size; my_queue() { head = tail = NULL; _size = 0; } void push(int a) { ++_size; if (head == NULL) { head = new queue_element(a, NULL); tail = head; // ustawienie } else {

// gowa i ogon kolejki

// dodawanie elementu na koniec // zwikszamy rozmiar kolejki // jeli kolejka jest pusta // ustawienie wartoci gowy wartoci ogona // jeli kolejka ma // ju jakie elementy

> Struktury danych i ich zastosowania


tail->next = new queue_element(a, NULL); // // tail = tail->next; // // } } } ;

<7>
nastpnik ogona, to nowy element przechodzimy do ostatniego elementu

Idea dziaanie tego kodu jest prosta: cay czas trzymamy wskaniki na gow i ogon kolejki. Kady element z kolejki ma wskanika na swojego nastpnika. Wyjmujemy elementy wskazywane przez gow. Dodajemy elementy na koniec, zaraz za ogonem. wiczenie 5. Uzupenij struktur kolejki o nastpujce metody: front zwracajc pierwszy element z kolejki size zwracajc liczb elementw w kolejce empty zwracajc warto logiczn, czy kolejka jest pusta pop zdejmujca pierwszy element z przodu kolejki, warto tutaj poszuka podobiestw z pop ze stosu wiczenie 6. Uzasadnij, dlaczego kolejka jest odpowiedni struktur do przeszukiwania metod BFS, a nie nadaje si do przeszukiwania metod DFS. wiczenie 7. Zaimplementuj przeszukiwanie grafu metod BFS korzystajc z kontenera queue z biblioteki STL.

Lista

Lista dwukierunkowa to struktura danych, w ktrej kady element ma swojego nastpnika i poprzednika. head 17 10 42 tail

Taka lista umoliwia wstawianie i usuwanie elementw z dowolnego miejsca. Sprbujmy napisa szkielet struktury danych lista. Zacznijmy od pojedynczego elementu listy: struct list_element { // element listy int val; list_element *prev, *next; list_element(int _val, list_element *_prev, list_element *_next) { // konstruktor, ustawia wartoci pocztkowe val = _val; // warto elementu prev = _prev; // poprzednik next = _next; // nastpnik } } ; Jak wida, potrzebujemy wskaniki na poprzedni i nastpny element listy. Poza tym mamy pole val, w ktrym jest przechowywana warto elementu. Pojedynczy element struktury my list ma dwa wskaniki, head i tail wskazujce na pierwszy i ostatni element listy. Porednie elementy s poczone ze sob. struct my_list { list_element *head, *tail; // wskaniki na gow i ogon listy

<8>

Informatyka +
int _size; // rozmiar my_list() { // konstruktor head = tail = NULL; // pocztkowo gowa = ogon = nic _size = 0; } void push_back(int a) { // dodanie elementu na koniec ++_size; if (head == NULL) { // jeli kolejka jest pusta head = tail = new list_element(a, NULL, NULL); } else { // kolejka ma ju jakie elementy tail->next = new list_element(a, tail, NULL); tail->next->prev = tail; tail = tail->next; } } void pop_back() { // usunicie elementu z koca kolejki --_size; list_element *tmp = tail; // zapamitujemy stary ogon // w zmiennej tymczasowej tail = tail->prev; // poprawienie ogona tail->next = NULL; // poprawienie nastpnika ogona delete tmp; // usunicie starego ogona } list_element* search(int x) { // znalezienie pierwszego elementu // o podanym kluczu list_element* e = head; while (e != NULL) { // idziemy po licie a znajdziemy if (e->val == x) return e; // Hurra! Znalelimy! e = e->next; // przechodzimy dalej } return NULL; // nie znalelimy } void remove(list_element *ptr) { // usuwanie elementu --_size; if (ptr->prev == NULL) head = ptr->next; // jeli nie ma poprzednika else ptr->prev->next = ptr->next; // jeli jest if (ptr->next == NULL) tail = ptr->prev; // jeli nie ma nastpnika else ptr->next->prev = ptr->prev; // jeli on jest delete ptr; // usunicie wskanika } void write() { // wypisanie elementw listy list_element *e = head; while (e != NULL) { cout<<e->val<<" "; e = e->next; } cout<<endl; }

} ; Obserwujc ten kod naley zwrci uwag, e nie wszystkie metody wykonywane s w czasie staym. Metoda search dziaa w czasie liniowym ze wzgldu na rozmiar listy.

> Struktury danych i ich zastosowania


wiczenie 8. Jako wiczenie warto zaimplementowa dodatkowe metody w strukturze my list. Np.: pop front usunicie elementu z przodu listy push front dodanie elementu z przodu listy insert dodanie elementu do listy za wskazanym elementem wiczenie 9. Sprbuj przerobi struktur my queue, eby dziaaa jak lista jednokierunkowa. W tym celu naley doda funkcjonalnoci: wyszukiwanie elementw, usuwanie elementw ze rodka, wstawianie elementw w rodek listy.

<9>

wiczenie 10. Korzystajc z listy dwukierunkowej zaimplementuj struktur pozwalajc na edycj tekstu. W edycji tekstu dozwolone s nastpujce operacje: napisanie jednej litery zaraz za kursorem usunicie jednej litery z miejsca bezporednio za kursorem przesunicie kursora o jedno pole w lewo/prawo wypisanie caego tekstu na ekran

Podsumowanie struktur
Warto uwanie przeledzi dziaanie tych trzech struktur danych. Szczliwie s one wszystkie zaimplementowane w bibliotece standardowej C++ STL. S to odpowiednio kontenery stack, queue, list. Autorzy tego dokumentu starali si zachowa oryginalne nazwy metod tak, aby przesiadka na kontenery z biblioteki STL bya moliwie bezbolesna. Drobne rnice s jedynie w implementacji listy.

Kopiec

Wstp
Kolejny fragment jest powicony strukturze danych, zwanej kopcem. W poniszej tabeli porwnano zoonoci operacji na kopcu oraz tablicy (nie-)posortowanej. wstawienie usunicie minimum tablica O(1) O(1) O(n) posortowana tablica O(n) O(n) O(1) kopiec O(log n) O(log n) O(log n) Oznaczenia: wstawienie wstawienie elementu usunicie usunicie elementu na podanej pozycji (a nie o danej wartoci) minimum znalezienie minimum

Implementacja penego drzewa binarnego


.

< 10 >

Informatyka +

Denicja 3. Pene drzewo binarne to drzewo, ktrego wierzchoki maj co najwyej dwch synw (binarne) a liciea znajduj si tylko na 2 poziomach, przy czym te na niszym poziomie s z jednej strony (patrz rys. 7.1).
1

10

11

12

Rys. 1: Numeracja wierzchokw penego drzewa binarnego


a li

wierzchoek nie posiadajcy synw

Wierzchoki penego drzewa binarnego moemy ponumerowa tak, jak to zostao przedstawione na rys. 7.1. Taka numeracja jest wygodna, gdy: lewy syn x to 2x prawy syn x to 2x + 1 ojciec x to x/2 W wierzchokach penego drzewa binarnego bdziemy przechowywali pewne wartoci (np. liczby). Drzewo binarne bdziemy reprezentowali w postaci tablicy heap, przy czym heap[i] oznacza warto w i-tym wierzchoku.

Wasno kopca
. Denicja 4. Kopcem nazywamy pene drzewo binarne z wartociami w wierzchokach, ktre ma wasno kopca, tzn. kady wierzchoek ma przypisan warto nie wiksz ni wartoci jego synw. Innymi sowy, dla wszystkich x zachodzi heap[ x/2 ] heap[x].
2

Rys.2: Przykadowy kopiec

Operacje na kopcu
Niektre operacje na kopcu oprcz opisu bd zawieray implementacj. Na potrzeby implementacji zakadamy, e nasz kopiec ma nastpujc deklaracj: #define MAXN 1000000 int heap[MAXN+1];

> Struktury danych i ich zastosowania


int size=0; //liczba elementw w kopcu //elementy w kopcu to heap[1],heap[2],...,heap[size] Wstawianie elementu Aby wstawi element do kopca:

< 11 >

1. dodajemy nowy element na koniec tablicy (wasno kopca moe by zaburzona) 2. dopki ojciec nowego elementu jest od niego wikszy, to zamieniamy wartoci w obu wierzchokach (proces ten nazywamy kopcowaniem w gr (HeapUp)) void heapUp(int x){ //kopcuje w gr element na pozycji x while(heap[x/2] > heap[x]){ swap(heap[x], heap[x/2]); //zamienia wartoci w obu wzach x/=2; } } void insert(int x){ //dodaje x do kopca heap[++size]=x; heapUp(size); } Znajdowanie minimum Minimum znajduje si oczywicie z korzeniu, czyli w heap[1].

int minimum(){ //zwraca minimum return heap[1]; } Usuwanie korzenia Aby usun korze z kopca: 1. w miejsce korzenia wstawiamy ostatni element z tablicy (wasno kopca moe by zaburzona) 2. dopki wstawiony element jest wikszy od ktrego ze swoich synw to zamieniamy go z synem o mniejszej wartoci (proces ten nazywamy kopcowaniem w d (HeapDown)) Usuwanie korzenia dobrze wizualizuje prezentacja doczona do tego tematu. void heapDown(int x){ //kopcuje w d element na pozycji x while(2*x <= size){ //dopki x ma chocia jednego syna int son=2*x; if(2*x+1 <= size && heap[2*x+1] < heap[2*x]) son=2*x+1; //zmienna son zawiera teraz indeks syna x o mniejszej wartoci if(heap[son] >= heap[x]) break; swap(heap[x], heap[son]); //zamienia wartoci w obu wzach x=son; } } void eraseRoot(){ //usuwa korze (czyli minimum) heap[1]=heap[size--]; heapDown(1); }

< 12 >
Usuwanie dowolnego elementu

Informatyka +
Aby usun dowolny element (niekoniecznie korze) z kopca:

1. w miejsce usuwanego elementu wstawiamy ostatni element 2. kopcujemy nowy element w gr 3. kopcujemy nowy element w d Analiza zoonoci Wszystkie opisane operacje dziaaj w czasie proporcjonalnym do wysokoci kopca, ktra jest logarytmiczna wzgldem liczby elementw w kopcu.

Sortowanie przez kopcowanie HeapSort


Kopca mona uy do szybkiego O(n log n) sortowania. Pomys jest do prosty elementy tablicy, ktr chcemy posortowa wrzucamy do kopca a nastpnie dopki kopiec nie jest pusty, to wypisujemy minimum i je usuwamy.

4.1

Zastosowanie kopca w implementacji algorytmu Dijkstry

Przypomnienie algorytmu
Przypomnijmy, e algorytm Dijkstry suy do znajdowania najkrtszych cieek ze rda w sieciach, w ktrych wagi krawdzi s nieujemne.1 Oto schemat tego algorytmu: 1. oznacz wszystkie wierzchoki jako nieodwiedzone 2. dla kadego v V przyjmij dis[v] = 3. przyjmij dis[s] = 0 4. dopki istnieje nieodwiedzony wierzchoek o skoczonej odlegoci: (a) niech v bdzie wierzchokiem nieodwiedzonym o najmniejszej odlegoci (b) oznacz v jako odwiedzony (c) zrelaksuj wszystkie krawdzie wychodzce z v Kluczowa jest tutaj operacja 4.a, ktr wykonujemy O(|V |) razy a dotychczas zabieraa ona czas O(|V |), co dawao cakowit zoono algorytmu O(|V |2 )

Zastosowanie kopca
Bdziemy przechowywa w kopcu numery wierzchokw nieodwiedzonych. Chcielibymy znajdowa szybko w kopcu wierzchoek o najmniejszej wartoci w tablicy dis. Przy kopcowaniu musimy wic porwnywa odpowiednie wartoci w tablicy dis, a nie bezporednio wartoci trzymane w kopcu (dis[x] bdziemy nazywa priorytetem wartoci x). Priorytety mog jednak ulega zmianie w trakcie dziaania algorytmu (na skutek relaksacji krawdzi), co moe zaburzy wasno kopca. Zauwamy jednak, e mog one tylko male.
1 Dokadniejszy

opis znajduje si w notatkach do kursu Przegld podstawowych algorytmw

> Struktury danych i ich zastosowania


Rozwizanie problemu I metoda

< 13 >

Za kadym razem, gdy priorytet jakiego elementu si zmniejszy, musimy wykona kopcowanie tego elementu w gr. Umiemy jednak wykonywa kopcowanie jedynie elementu na danej pozycji (a nie o danej wartoci). Problem ten da si rozwiza rozbudowujc nieznacznie struktur naszego kopca. Oprcz tablicy heap, bdziemy trzymali tablic where, gdzie where[x] oznacza pozycj w kopcu wartoci x, tzn. heap[where[x]] = x. Przyjmujemy where[x] = 0, jeli w kopcu nie ma elementu x. Tablic t musimy uaktualnia za kadym razem, gdy przemieszczamy elementy w kopcu. Rozwizanie to nie jest perfekcyjne. Musimy zaimplementowa kopiec z operacj zmiany priorytetu, co moe zaj do duo czasu, nie s bowiem dostpne adne gotowe implementacje kopca dysponujce tak operacj.

Rozwizanie problemu II metoda


Przedstawimy teraz rozwizanie, ktre korzysta ze zwykego kopca (bez operacji zmiany priorytetu). Do kopca bdziemy wrzuca pary postaci (dis[x], x) a nie pojedyncze liczby jak dotychczas, przy czym przyjmujemy porzdek leksykograczny na parach, tzn. (x, y) < (a, b) x < a (x = a y < b) Najmniejszy element w kopcu, odpowiada wwczas wierzchokowi o najmniejszej odlegoci. Co zrobi, jeli warto dis[x] si zmieni? Wrzucamy wwczas do kopca now par (dis[x], x). Przy takim podejciu w kopcu bd znajdoway si mieci nieaktualne pary postaci (d, x), gdzie d > dis[x]. Pary takie po prostu pomijamy przy wyciganiu ich z kopca. A oto dokadniejszy schemat takiego algorytmu: 1. dla kadego v V ustaw dis[v] = 2. ustaw dis[s] = 0 3. wrzu do kopca par (0, s) 4. dopki kopiec nie jest pusty (a) wycignij najmniejsz par z kopca, oznaczmy j przez (d, v) (b) jeli d > dis[v], to powr do pkt. 4 (c) zrelaksuj wszystkie krawdzie wychodzce z v, jeli poprawia to odlego do wierzchoka x, to dodaj par (dis[x], x) do kopca

Zoono i implementacja
Oba przedstawione rozwizania maj zoono O(|E| log |V |), czyli duo lepsz ni algorytm Dijkstry nie korzystajcy z kopca, o ile tylko ilo krawdzi w grae jest duo mniejsza od |V |2 (grafy takie nazywamy rzadkimi). W zadaniach olimpijskich czsto mamy do czynienia z takimi grafami a nawet z grafami dla ktrych maksymalne liczby krawdzi i wierzchokw s tego samego rzdu. Implementacj algorytmu Dijkstry z uyciem kopca pozostawiam jako wartociowe wiczenia. By moe zastanawiasz si, jak reprezentowa w programie pary. Najwygodniejsz opcj jest skorzystanie z klasy pair. Poniszy kod prezentuje sposb jej uywania: #include <stdio.h> //ponisze dwa wiersze s koniczne do korzystania z klasy pair #include <iostream> using namespace std; int main(){

< 14 >

Informatyka +

pair<int, int> para; //tworzy zmienn para, ktrej obie skadowe s typu int para.first=7; //ustawia pierwsz skadow na 7 para.second=5; //ustawia drug skadow na 5 printf("%d %d\n", para.first, para.second); //wypisze "7 5" para=make_pair(2, 3); //szybsza opcja na przypisanie obu wartoci pair<int, int> cos(2, 3); //przypisanie wartoci przy tworzeniu if(para < cos){ //pary s porwnywane w porzdku leksykograficznym! cos = para; } return 0; }

Drzewa rozpinajce

Rozwamy nastpujcy problem. Jestemy na obozie informatycznym i mamy za zadanie utworzy sie przewodow pomidzy n hubami. Dodatkowo chcemy zuy do tego moliwie mao kabla, ktry w kocu te kosztuje. Nie wszystkie pary huby daj si poczy, gdy np. s za daleko od siebie. Pomidzy parami hubw, ktre moemy poczy, znamy wymagane dugoci kabla. Nie mona tworzy nowych rozgazie przy tworzeniu hubw. W jaki sposb znale sie czc huby tak, aby moliwy by transport danych midzy wszystkimi parami (by moe z uyciem hubw poredniczcych)? Proponujemy zastanowi si chwil nad tym problemem.

Rozwizanie
Zacznijmy od obserwacji, e szukana sie jest drzewem. Gdyby w optymalnej sieci byy cykle, to mona by wyrzuci dowoln krawd z cyklu otrzymujc sie, ktra nadal czy wszystkie huby, ale jest krtsza o jedno poczenie (zuywamy mniej kabla). Kolejna obserwacja jest taka: zawsze istnieje optymalne drzewo rozpinajce (czyli takie, ktre czy wszystkie wierzchoki do jednej spjnej skadowej), ktre zawiera najkrtsz krawd. Powd jest prosty: gdybymy znaleli drzewo o najmniejszej sumarycznej dugoci, ktre nie zawieraoby tej najkrtszej krawdzi, moglibymy doda t krawd do tego drzewa i uzyskalibymy jaki cykl. Jak wiemy, moemy wyrzuci z tego cyklu dowoln krawd (inn od tej dopiero co dodanej) i nadal mie spjn sie. Poniewa pewn krawd zastpilimy inn, o nie wikszej dugoci, to w sumie nowa dugo kabli jest nie wiksza ni poprzednia.

Przykad
Popatrzmy na tak sie pocze: 1 2 5 3 3 1 5 2 2 4 7 4 1 7 6 1 1 6

Kto wybra pogrubione krawdzie, uzyskujc wedug siebie najkrtsze drzewo rozpinajce. czna dugo: 13. Sprbuj samemu znale bd w wyborze krawdzi!

> Struktury danych i ich zastosowania

< 15 >

Rozwizanie: Jest cykl 2354. W tym cyklu nie zostaa wybrana tylko krawd 35, mimo, e ta krawd jest najkrtsza. Podmieniamy j za najdusz krawd z tego, czyli 45. Nowa dugo to 10. Nowe drzewo wyglda tak: 1 2 5 3 3 1 5 2 2 4 7 4 1 7 6 1 1 6

Poniewa nas interesuje dowolne drzewo rozpinajce, ktrego dugo jest minimalna, to moemy z gry wzi najkrtsz krawd i mie pewno, e nie zamkniemy sobie drogi do optymalnego rozwizania. Pomys 1 Teraz w do naturalny sposb nasuwa si pomys, aby powtrzy sposb wyboru kolejnej krawdzi. Wybierajc now krawd omijamy te, ktrych dodanie spowodowaoby powstanie cyklu. Zastanwmy si, dlaczego to rozwizanie dziaa. Krok algorytmu Popatrzmy troch inaczej na ten problem. Znajdujemy najpierw najkrtsz krawd spord wszystkich dozwolonych. Dodajemy j do wynikowego drzewa. Przypumy, e czya ona wierzchoki v i u. Moemy zczy te wierzchoki do jednego wierzchoka. W kocu, od momentu dodania krawdzi v u nie rozrniamy ju tych wierzchokw. Jest nam obojtne, czy jaka nowa krawd poczy w z v, czy w z u. Z poczonego wierzchoka uv wychodz wszystkie te same krawdzie, ktre wychodziy wczeniej. Po poczeniu moe si pojawi jaki wierzchoek, do ktrego id dwie krawdzie z wierzchoka uv. Oczywiste jest, e t o wikszej dugoci usuwamy. Nie bdzie ju ona potrzebna. Krok i co dalej? Po jednokrotnym wykonaniu operacji wyboru krawdzi i scaleniu dwch wierzchokw dostajemy nowy problem. Uzyskalimy graf o n 1 wierzchokach, ktre trzeba poczy drzewem rozpinajcym. Postpujemy znw tak samo. Czyli wykonujemy krok algorytmu. Znajdujemy najkrtsz krawd i czymy. Robimy tak, a nie osigniemy grafu z jednym wierzchokiem. Stan z jednym wierzchokiem oznacza, e wszystkie wierzchoki s w jednej spjnej skadowej. Poprawno Poprawno tego algorytmu wynika std, e przed kadym krokiem mamy problem znalezienia drzewa rozpinajcego w zwyczajnym grae, bez adnych wybranych wczeniej krawdzi. Wiemy natomiast z wczeniejszych rozwaa, e w takim grae zawsze istnieje rozwizanie, w ktrym bierzemy najkrtsz krawd. Wynika std, e algorytm, ktry za kadym razem wybiera najkrtsz krawd i scala koce tej krawdzi, poprawnie znajdzie drzewo rozpinajce o minimalnej sumarycznej dugoci.

< 16 >
A jednak Pomys 1 dziaa!

Informatyka +

Zauwamy teraz, e pocztkowy algorytm robi dokadnie to samo. Zawsze bierzemy najkrtsz niewykorzystan jeszcze krawd i dodajemy j do szukanego drzewa, jeli jej koce w drzewie le w rnych skadowych. Wniosek? Ten algorytm take jest poprawny. Ile to nas kosztuje? Pozostaje kwestia, jak szybko mona zaimplementowa ten algorytm. Niech n oznacza ilo wierzchokw grafu, za m oznacza liczb krawdzi. Na pocztku warto posortowa krawdzie wzgldem rosncej dugoci, aby potem wybr kolejnych najkrtszych by prosty. Ta faza kosztuje nas O(m log m). Trudniejsza jest kwestia, jak szybko sprawdza, czy dwa koce krawdzi le w budowanym drzewie rozpinajcym w jednej spjnej skadowej. Najprostsze rozwizanie, to sprawdzanie przy kadej dodawanej krawdzi algorytmem przeszukiwania grafu, np. metod DFS, czy koce krawdzi s w tej samej spjnej skadowej. Takie rozwizanie prowadzi do zoonoci tej fazy wynoszcej O(n m). Jest to bardzo duo i sprbujemy zredukowa ten koszt. Mona to nieco ulepszy spostrzeeniem, e jeli nie dodalimy nowej krawdzi, to podzia na skadowe jest cay czas ten sam. Oznacza to, e wystarczy, ebymy po kadym dodaniu krawdzi policzyli podzia na skadowe. Moemy to zrobi przypisujc wszystkim wierzchokom w danej skadowej jak liczb charakterystyczn. Pniej atwo ju odpowiada, czy dwa wierzchoki s w jednej spjnej skadowej. Wystarczy spojrzenie, czy przyporzdkowane im liczby charakterystyczne s sobie rwne. Takie rozwizanie w tej fazie kosztuje O(n2 ) czasu, co jest zauwaalnie lepsze ni rozwizanie poprzednie, szczeglnie, gdy graf jest gsty. Znany jest jednak znacznie szybszy sposb sprawdzania czy wierzchoki s poczone. Metoda ta korzysta ze struktury zbiorw rozcznych.

Zbiory rozczne

Dana jest pewna rodzina zbiorw. Kade dwa zbiory w tej rodzinie s parami rozczne. Na tej rodzinie zbiorw chcemy wykonywa dwie operacje. Jedna - to zapytanie: czy element a i element b nale do tego samego zbioru. Druga - to operacja poczenia dwch podzbiorw w jeden. Interesuje nas przede wszystkim to, jak szybko dziaa nasza struktura, jeli na n elementach wykonamy n operacji zczenia zbiorw i m operacji sprawdzenia, czy jakie dwa elementy s w tym samym zbiorze. We wszystkich prezentowanych poniej podejciach bdziemy wykonywa dwie operacje: FIND(x) znalezienie reprezentanta zbioru, do ktrego naley x. Jeli x i y nale do tego samego zbioru, to FIND(x) zwraca to samo, co FIND(y). UNION(x,y) zczenie zbioru zawierajcego x ze zbiorem zawierajcym y.

Podejcie pierwsze
Kady zbir reprezentujemy w postaci listy elementw. Dodatkowo kady element ma wskanik do pierwszego elementu na licie. Wtedy: Operacj FIND wykonujemy w czasie O(1), wystarczy odwoa si do pierwszego elementu. Jeli chcemy zczy dwie listy, to moemy jedn zostawi tak jak jest, a drug doczy na koniec. Na razie kosztowao nas to czas O(1). Niestety musimy jeszcze poprawi wartoci wskanikw drugiej listy tak, aby pokazyway na pierwszy element pierwszej listy. To kosztuje znacznie wicej, bo O(d), gdzie d jest dugoci drugiej listy. Rozwamy najgorszy scenariusz: mamy jeden duy zbir, ktry za kadym razem doczamy do listy jednoelementowej na

> Struktury danych i ich zastosowania

< 17 >

koniec. Wtedy i - ta taka operacja kosztuje i modykacji wskanika. Oznacza to, e wykonanie n takich operacji wykonamy bdzie trwao:
n

i=
i=1

n(n + 1) 2

czyli O(n2 ). Wszystkie zapytania i operacje poczenia zajmuj razem O(n2 + m).

Podejcie drugie
Zamiast dokleja drug list na koniec pierwszej, mona wybra kolejno doklejania tak, aby byo potem moliwie mao operacji modykowania wskanika na pierwszy element. W tym celu zawsze do duszej listy doklejamy krtsz. Powoduje to, e kady element, gdy zmieniamy mu wskanik do reprezentanta, przynajmniej podwaja dugo listy, w ktrej si znajduje. Wniosek jest taki, e kady element bdziemy poprawia maksymalnie O(log n) razy. Oznacza to, e wszystkie operacje poczenia kosztuj nas n O(log n) = O(n log n) plus O(n) operacji czenia samych list. Razem daje to O(n log n). Zapytania FIND kosztuj nas razem O(m). W sumie dziaanie naszej struktury bdzie kosztowa O(m + n log n). Niby niewielka zmiana, a zoono znacznie lepsza.

Do trzech razy sztuka


Ostanie podejcie bdzie podobne. Tym razem reprezentujemy zbir jako drzewo. W takim drzewie kady element bdzie mia wskanik na swojego ojca. Dodatkowo kady bdzie pamita wysoko, na ktrej si aktualnie znajduje. Zasadniczo bdzie to wyglda tak:

d Jeli wywoujemy FIND, to idziemy po wskanikach do gry. Jeli chcemy poczy zbir zawierajcy x ze zbiorem zawierajcym y, to musimy najpierw znale reprezentantw, czyli rx =FIND(x), ry =FIND(y), a potem do elementu o wyszej randze spord rx i ry podpinamy ten o niszej randze. Jeli przypadkiem rangi byy rwne, to zwikszamy rang temu elementowi, do ktrego zosta podpity ten drugi. Efekt przykadowej operacji UNION(b, f):

< 18 >

Informatyka +

wiczenie 11. Wyka, e wysoko tak skonstruowanego drzewa jest co najwyej logarytmiczna ze wzgldu na liczb elementw tego poddrzewa. wiczenie 12. Napisz procedur UNION i funkcj FIND. Przyjmij, e elementy maj numery od 1 do n, tablica father reprezentuje ojca w drzewie reprezentujcym zbir. Tablica rank reprezentuje rang/wysoko zbioru podczepionego w danym elemencie. Na razie zoono czasowa jest bardzo podobna do tej z podejcia drugiego. Moe troch lepsza staa, ale nic poza tym. Teraz posuymy si kolejn sztuczk, aby znaczco przypieszy algorytm. Sztuczka Od tej pory nazywajmy pamitan wysoko drzewa rang. Na razie napiszmy funkcj FIND nastpujco: int FIND(int x) { if (father[x] == x) return x; return FIND(father[x]); } Ten kod jest poprawny, ale nieoptymalny. Przy kadym zapytaniu o element x musimy pokona dug ciek do korzenia. Z pomoc przychodzi nam sztuczka: kady odwiedzany element podpinamy do reprezentanta. Zatem jeli przechodzimy ciek, to podepnijmy wszystkie odwiedzane elementy do reprezentanta. Popatrzmy na nowy kod: int FIND(int x) { if (father[x] == x) return x; father[x] = FIND(father[x]); return father[x]; } Oszacowanie zoonoci czasowej tego algorytmu jest do trudne. Zostao to opisane w wyczerpujcy sposb we Wprowadzeniu do algorytmw. Warto zacytowa oszacowanie zoonoci za t ksik: n operacji czenia i m operacji FIND kosztuj O(m log n), gdzie log n oznacza wy2 soko stosu potg dwjek potrzebnych do zbudowania n. Np. log 2 = 1, log 22 = 2, log 22 = 3, log 22 = 4, itd. Warto 22 = 265536 wielokrotnie przewysza rozmiarem dane, ktre s przetwarzane przez dzisiejsze komputery. Mona wic przyj, e czas dziaania tego algorytmu jest liniowy. Caa ta sztuczka nosi nazw kompresji cieek.
22 22 2

> Struktury danych i ich zastosowania


Podsumowanie

< 19 >

Korzystajc z przedstawionego algorytmu na znajdowanie drzewa rozpinajcego i szybkiej struktury danych rozwizujcej problem FIND-UNION moemy zbudowa drzewo rozpinajce o najmniejszej sumarycznej dugoci w czasie O(m log m). Cay przedstawiony algorytm nosi nazw algorytmu Kruskala z uyciem struktury danych dla zbiorw rozcznych.

Drzewa wyszukiwa binarnych (BST)

Przy rozwizywaniu problemw algorytmicznych natraamy czsto na konieczno przechowywania zbioru (czy raczej multizbioru), ktrego zawarto bdzie si czsto zmieniaa. Zwykle chcemy wtedy mc stwierdza, czy element o okrelonej wartoci w nim wystpuje. Jaka jest odpowiednia struktura danych do przechowywania takiego multizbioru? Pierwsze, co przychodzi do gowy, to zwyka lista. Wwczas jednak szukanie okrelonej wartoci, moe wymaga przejrzenia wszystkich elementw multizbioru, co nie jest zbyt wydajne. Innym rozwizaniem jest trzymanie wartoci w posortowanej tablicy. Wwczas wyszukiwanie binarne elementu bdzie dziaao szybko, ale wstawianie nowego elementu wymaga bdzie bd ponownego sortowania, bd kopiowania pewnej liczby elementw, by moe duej. Istniej jednak struktury, ktre wszystkie te operacje mog wykonywa szybko.

Drzewo wyszukiwa binarnych


Struktura ta suy do przechowywania elementw uporzdkowanych (np. rosnco), czym rni si od innych struktur, jak choby stosu. Po angielsku nazywa si ona binary search tree, std powszechnie uywany skrt BST. Drzewa BST s ukorzenione (tzn. maj jeden wybrany wierzchoek bdcy korzeniem caego drzewa) oraz binarne (czyli kady wierzchoek ma co najwyej dwch synw). W kadym wierzchoku przechowywane s wartoci, nazywane rwnie kluczami. Tym, co odrnia drzewa BST od, na przykad, kopca jest to, e klucze wierzchokw speniaj dodatkowy warunek: Porzdek symetryczny: dla kadego wierzchoka o kluczu v wierzchoki w jego lewym poddrzewie maj wartoci mniejsze bd rwne v, a w prawym wiksze bd rwne.
7

11

12

Rys. 1: Przykadowe drzewo BST

Warunek porzdku symetrycznego pokazuje ju, jak bdziemy wyszukiwali elementw w drzewach BST: jeeli szukany element jest mniejszy od klucza aktualnego wierzchoka to szukamy w lewym poddrzewie, w przeciwnym przypadku w prawym.

Struktura implementacji
Implementacja drzew BST jest troch bardziej skomplikowana od kopca, gdy nie mona uywa po prostu jednej tablicy. Zamiast niej, podobnie jak w przypadku stosw, kolejek i list, bdziemy korzysta ze struktur dla wierzchokw i czy je wskanikami.

< 20 >
struct Node { // Wskaniki do lewego i prawego syna, oraz ojca. // warto NULL oznacza brak odpowiedniego wierzchoka. Node *left, *right, *parent; int key; // klucz, czyli warto przechowywana w wierzchoku }; Cae drzewo bdziemy przechowywali w programie jako obiekt: struct BST { Node * root; // wskanik na korze drzewa BST() // konstruktor pustego drzewa { root = NULL; } /* tu naley umieszcza kolejne funkcje */ };

Informatyka +

Wyszukiwanie elementu
Skoro znamy ju struktur drzewa BST moemy zaimplementowa wyszukiwanie elementu w drzewie. Algorytm oparty na metodzie dziel i zwyciaj, zosta ju pobienie opisany: poczynajc od caego drzewa sprawdzamy korzenie kolejnych poddrzew: jeeli klucz takiego korzenia jest wikszy od poszukiwanego to kontynuujemy w lewym poddrzewie, w przeciwnym przypadku w prawym.
7

11

12

Rys. 2: Poszukiwanie elementu o kluczu 4

Node * search(int v) { Node *s = root; // gdy s == NULL, oznacza to, e wyszlimy ,,poza drzewo while (s != NULL && s->key != v){ if (s->key > v) s = s->left; else s = s->right; } return s; // funkcja zwraca NULL, gdy brak wierzchoka o kluczu v } Nie trudno jest zauway, e metoda ta tak na prawd przechodzi po jakiej ciece w drzewie, odwiedzajc kady wierzchoek raz. Jej zoono czasowa to zatem O(h), gdzie h jest wysokoci drzewa (czyli dugoci najduszej cieki z korzenia do licia).

> Struktury danych i ich zastosowania

< 21 >

wiczenie 13. Wymyl i zaimplementuj algorytm znajdujcy najmniejszy klucz w drzewie BST. wiczenie 14. W jakiej kolejnoci naleaoby odwiedza, wierzchoki drzewa, by ich klucze przeglda w kolejnoci niemalejcej? Napisz metod klasy BST wypisujc wszystkie klucze z drzewa w kolejnoci niemalejcej. wiczenie 15. Nastpnikiem wierzchoka s nazywamy wierzchoek, ktry zostanie wypisany tu po s, w powyszym algorytmie przegldania drzewa. Wyka, e jeeli s ma prawego syna, to nastpnikiem s jest wierzchokiem o najmniejszym kluczu w jego prawym poddrzewie. Zaimplementuj funkcj znajdujc nastpnik w takim przypadku. Ktry wierzchoek jest nastpnikiem s, gdy nie ma on prawego poddrzewa? Czy nastpnik bdzie zawsze istnia?

Dodawanie elementu
Jeeli chcemy by nasze drzewa okazay si uyteczne musimy nauczy si dodawa do nich nowe wierzchoki tak, by zosta zachowany porzdek symetryczny. W tym celu naley, naladujc algorytm wyszukiwania elementu, przej przez drzewo, a do wierzchoka, ktry nie ma odpowiedniego (prawego lub lewego) syna i doda tam nowy wierzchoek. void insert(int v) { Node *n = new Node(), *s = root, *prev = NULL; // inicjalizacja nowego wierzchoka n->left = NULL; n->right = NULL; n->key = v; while (s != NULL){ // naladowanie wyszukiwania prev = s; if (s->key >= v) s = s->left; else s = s->right; // Niezmiennik: zmienna prev wskazuje // na poprzednio odwiedzony wierzchoek } // pod wierzchoek prev dowizujemy n z odpowiedniej strony if (prev->key >= v) prev->left = n; else prev->right = n; n->parent = prev; } wiczenie 16. Powyszy kod dziaa jedynie dla drzewa o co najmniej jednym elemencie. Dlaczego? Co si stanie gdy zostanie uruchomiona na pustym drzewie? Dopisz do metody obsug tego specjalnego przypadku.

Usuwanie wierzchoka
Usuwanie wierzchoka jest troch bardziej skomplikowane od dodawania, gdy trzeba uwaa na wicej przypadkw szczeglnych. Jeeli chcemy usun wierzchoek s, to musimy zadba o poprawne dziaanie w nastpujcych przypadkach:

< 22 >
1. s nie ma synw moemy po prostu skasowa ten wierzchoek;

Informatyka +

2. s ma jednego syna (lewego lub prawego). Wwczas ojcu s jako bezporedniego potomka (i to po odpowiedniej stronie!) ustawiamy jedynego syna s; 3. s ma dwch synw. Naley wwczas znale nastpnika s, jego klucz zapisa w wierzchoku s i usun nastpnika. Zauwamy, e operacja ta nie zaburza porzdku symetrycznego w drzewie, gdy nastpnik ma najmniejszy klucz, nie wikszy ni ten usuwany. Napisanie bezbdnie metody z tak liczb moliwych przypadkw stanowi pewne wyzwanie, do ktrego podjcia zachcamy! wiczenie 17. Uzupenij ponisz metod tak, by poprawnie dziaaa we wszystkich przypadkach. Nie zapomnij o poprawieniu wskanikw parent oraz o tym, e usuwany wierzchoek moe by korzeniem drzewa! void erase(Node * s) { Node *next; if (s->left == NULL || s->right==NULL) { // przypadki 1 i 2: s nie ma co najmniej jednego syna // poprawnie rozpatrz te dwa przypadki! delete s; } else { // przypadek 3, szukanie nastpnika next = s->right; while (next->left != NULL) next = next->left; s->key = next->key; erase(next); // dziki powyszemu odwoaniu trzeba sprawdzi mniej przypadkw } }

7.1

Zrwnowaone drzewa poszukiwa

Problem z efektywnoci drzew BST


Operacje dodawania, usuwania i wyszukania na drzewach BST dziaaj w czasie proporcjonalnym do wysokoci drzewa. W przypadku danych losowych, wysoko ta jest rzdu O(log n), co jest zupenie zadowalajce. Niestety, moe si zdarzy, e cig elementw dodawanych do drzewa spowoduje, e bdzie miao ono posta jednej dugiej cieki bez rozgazie, jak na rysunku 3.
1 2 3 ... 1000

Rys. 3: Zoliwy przypadek drzewa BST

Wwczas czas dziaania wszystkich operacji na drzewie bdzie liniowo zaleny od liczby elementw, a zatem nic nie zyskujemy na korzystaniu z drzew BST w porwnaniu do zwykej listy!

> Struktury danych i ich zastosowania


Drzewa AVL

< 23 >

Aby upora si z powyszym problemem stosuje si rne techniki. Chyba najprostsz do zrozumienia s drzewa AVL, zaproponowane przez Gieorgija Adelson-Wielskija i Jewgienija andisa. Pomys polega na dodaniu drzewom BST nastpujcego warunku: dla kadego wierzchoka, wysokoci jego prawego i lewego poddrzewa mog rni si o co najwyej 1 Mona udowodni, e wysoko drzewa o n wierzchokach speniajcego ten warunek jest O(log n). Operacje na drzewach AVL s podobne do tych na BST. Rnica jest tylko taka, e po operacjach dodawania, bd usuwania wierzchokw, ktre mog zmieni struktur drzewa, naley przywrci warunek zrwnowaania. Mona to zrobi za pomoc tzw. rotacji, ktrym przyjrzymy si na przykadzie z rys. 4.
y x

Rys 4. Pojedyncza rotacja

Zamy, e poddrzewo A ma wysoko h+1, a poddrzewa B i C maj wysokoci h. Wwczas w wierzchoku x poddrzewa rni si wysokoci o 1, co jest dopuszczalne, ale ju w wierzchoku y wysokoci poddrzew rni si o 2. Pojedyncza rotacja, przedstawiona na obrazku, moe to jednak poprawi: po jej wykonaniu poddrzewa bd miay rwne wysokoci, zarwno x jak i y. Jest to oczywicie bardzo oglny opis dziaania drzew AVL. Zainteresowanych odsyamy do pozycji [2] i [3]. Istniej te inne sposoby rwnowaenia drzew binarnych. Jako szczeglnie proste do implementacji polecamy samoorganizujce si drzewa BST (nazywane niekiedy drzewami splay), o ktrych te mona poczyta w wymienionych materiaach.

Kontenery z biblioteki standardowej


Jak si okazuje, w standardowej bibliotece szablonw (STL) jzyka C + + znajduj si struktury danych oparte na zrwnowaonych drzewach binarnych. Mona z nich korzysta w bardzo wielu przypadkach, ale niestety nie wszystkich: czasami trzeba zaimplementowa swoje wasne drzewa. Dwa czsto stosowane kontenery tego typu to set (reprezentuje zbir, zatem nie mog si w nim powtarza wartoci) oraz multiset. Kilka wskazwek, jak si nimi posugiwa: W programie trzeba umieci: #include<set> using namespace std; Deklaracja zbioru i multizbioru elementw typu int: set<int> zb; multiset<int> mzb; Dodawanie, usuwanie i szukanie elementu:

< 24 >
zb.insert(6); zb.erase(7); if (zb.find(4) != zb.end()) { // 4 jest w zbiorze zb } Wypisanie elementw zbioru w kolejnoci rosncej:

Informatyka +

for(set<int>::iterator it = zb.begin(); it != zb.end(); ++it) printf("%d\n", *it);

Wicej o korzystaniu z tych oraz innych kontenerw biblioteki STL bdzie mona dowiedzie si na innych kursach. Mona te korzysta z dokumentacji (w jzyku angielskim) dostpnej pod adresem: http://www.sgi.com/tech/stl/

Zadanie
A. Przedziay Napisz program przechowujcy informacje o zbiorze przedziaw na prostej. Bdzie on otrzymywa kolejne polecenia do wykonania, w jednej z poniszych postaci: 1 p k dodanie do zbioru przedziau o pocztku w p i kocu w k (p i k bd liczbami cakowitymi, 109 p < k 109 ). 0 zapytanie o liczb rozcznych przedziaw w zbiorze, 1 koniec listy polece. Przykadowe wejcie: 112 134 0 123 0 1 Przykadowe wyjcie: 2 1

Drzewa przedziaowe

Czym s drzewa przedziaowe?


Drzewami przedziaowymi nazywamy struktury danych, umoliwiajce szybkie wykonywanie operacji na zbiorze przedziaw, takich jak: wstawienie przedziau do zbioru (by moe z pewn wag), usunicie przedziau, sprawdzenie w ilu przedziaach zawiera si dany punkt, odczytanie sumarycznej wagi punktw z danego przedziau, etc. Na wykadzie przedstawiamy dwa podejcia do implementacji drzewa przedziaowego. Pierwsze z nich nazywane jest rwnie drzewem potgowym.

> Struktury danych i ich zastosowania


Ustalenia wstpne

< 25 >

Zakadamy, e wszystkie rozwaane przedziay maj koce w punktach cakowitoliczbowych z ustalonego zakresu bdcego potg dwjki (zakres ten oznacza bdziemy przez N ). Inaczej ni w geometrii, przez dugo przedziau rozumiemy liczb punktw o wsprzdnych cakowitych w nim zawartych. Przykadowo przedzia [3, 3] ma dugo 1 gdy zawiera dokadnie jeden punkt. atwiej jest zatem myle o punktach jak o komrkach tablicy. Zakres wsprzdnych przedziaw przechowywanych w drzewie nie moe by zbyt duy, w szczeglnoci tablica liczb cakowitych o rozmiarze 2N (a najczciej kilka takich tablic) musi swobodnie mieci si w limicie pamiciowym. W niektrych przypadkach moemy poradzi sobie z bardzo duym zakresem przedziaw stosujc pewn sztuczk. Jeli interesuje nas jedynie uoenie przedziaw wzgldem siebie (a nie ich faktyczna dugo) moemy wstpnie wczyta dane wejciowe, posortowa wszystkie wystpujce w nich punkty i nada im nowe wsprzdne bdce kolejnymi liczbami naturalnymi. Po takim zabiegu zakres wsprzdnych bdzie wielkoci danych wejciowych. Kada z omawianych struktur danych bdzie udostpnia dwie operacje: wstawienie (funkcja insert()) wykonanie pewnej akcji (dodanie obcienia lub wycignicie maksimum) na przedziale lub w punkcie, zapytanie (funkcja query()) odczytanie aktualnego stanu (sumy lub maksimum) z przedziau lub punktu, bdcego wynikiem wykonanych operacji insert.

8.1

Drzewo potgowe

Zamy, e potrzebujemy struktury danych umoliwiajcej wykonywanie nastpujcych operacji: insert(x,v) dodanie wartoci (inaczej obcienia) v do punktu x query(a,b) zsumowanie obcie punktw zawartych w przedziale [a, b] Trywialn realizacj powyszych wymaga moe by tablica, w ktrej po prostu zapisujemy wstawione wartoci. Niestety, pomimo tego, i funkcja insert dziaa w czasie staym, zliczenie sumy liczb w przedziale wie si z kosztem proporcjonalnym do jego dugoci, co w przypadku duej liczby wywoa query dla dugich przedziaw jest zdecydowanie zbyt wolne. Drzewo potgowe opiera si na pomyle, aby w tablicy (nazwijmy j load) nie przechowywa obcienia jednego punktu, lecz sum obcie troch wikszego obszaru. Dokadniej, w polu load[x] bdziemy pamita sum wszystkich operacji insert dla punktw z przedziau [x p + 1, x], gdzie p jest najwiksz potg dwjki dzielc x. 1 2 3 4 5 6 7 8 9 ...

Co nam daje taka modykacja? Na pewno komplikuje operacj insert, gdy dodajc warto do pewnego punktu musimy uaktualni potencjalnie wiele pl tablicy. Przykadowo, wykonujc insert w punkcie nr 3 musimy uaktualni pola load[3], load[4], load[8], load[16], itd. Okazuje si jednak, e liczba niezbdnych zmian jest niewielka.

< 26 >

Informatyka +

wiczenie 18. Uzasadnij, e liczba pl tablicy wymagajcych aktualizacji przy operacji insert jest rzdu O(log N ). Skd wiadomo, ktre pola w tablicy uaktualni? Zauwamy, e jeli uaktualnilimy pole o indeksie x, a p jest najwiksz potg dwjki dzielc x, to nastpnym polem, ktre obejmie swoim zasigiem pole x, jest x + p. Wystarczy zatem zacz od ustawienia wartoci pola load[x], a nastpnie przesuwa si do kolejnych pl, za kadym razem zwikszajc indeks o najwiksz potg dwjki dzielc indeks pola aktualnego. No dobrze, a jak w takim razie wyznaczy t potg dwjki? Mona to zrobi atwo w zoonoci logarytmicznej, my jednak chcielibymy umie wykona to w czasie staym. Z pomoc przychodz operatory bitowe, dziki ktrym szukan liczb p wyznaczymy w sposb nastpujcy: p = ( (x ^ (x - 1)) + 1 ) / 2 Aby lepiej zrozumie powyszy wiersz, przeanalizujmy jego dziaanie na przykadzie. Niech x = 20, czyli w zapisie binarnym 10100(2) : x = 10100(2) x - 1 = 10011(2) x ^ (x - 1) = 00111(2) (x ^ (x - 1)) + 1 = 01000(2) ((x ^ (x - 1)) + 1) / 2 = 00100(2) Skoro umiemy wyznaczy najwiksz potg dwjki dzielc dan liczb w czasie staym, to umiemy te zaimplementowa operacj insert tak, aby dziaaa w czasie O(log n): void insert(int x, int v) { while(x < N) { load[x] += v; x += ( (x ^ (x - 1)) + 1) / 2; } } Jak dotd udao nam si jedynie pogorszy zoono operacji insert. Okazuje si jednak, e dziki poczynionym modykacjom uda nam si zredukowa zoono operacji query do O(log N )! Aby uproci sobie troch implementacj, zauwamy, e zamiast pyta o sumaryczne obcienie na przedziale [a, b] wystarczy, e poznamy sumaryczne obcienia na przedziaach [1, a 1] i [1, b]. Oto kod funkcji query wraz z pomocnicz funkcj sum: int sum(int x) { int res = 0; while(x > 0) { res += load[x]; x -= ( (x ^ (x - 1)) + 1) / 2; } return res; } int query(int a, int b) { return sum(b) - sum(a-1); }

> Struktury danych i ich zastosowania


8.2 Drzewa przedziaowe

< 27 >

Drzewo potgowe sprawdza si bardzo dobrze w przypadku gdy operacja insert dotyczy punktw. Jeli jednak musimy dodawa obcienia na cae przedziay, potrzebujemy struktury nieco bardziej rozbudowanej. Jako e zapytanie o punkt moemy traktowa jak zapytanie o przedzia dugoci 1, skupimy si od razu na najoglniejszym przypadku, w ktrym wszystkie operacje dotycz przedziaw.

Rozkad na przedziay bazowe


. Denicja 5. Przedziaem bazowym nazywamy przedzia o dugoci bdcej potg dwjki i pocztku w punkcie bdcym wielokrotnoci swojej dugoci. Interesuje nas rozkad danego przedziau na minimaln liczb rozcznych przedziaw bazowych, ktre pokrywaj dany przedzia. Np. przedzia [3, 9] ma rozkad na sum przedziaw [3, 3], [4, 7] i [8, 9]. Kluczowy tutaj jest nastpujcy fakt: wiczenie 19. Uzasadnij, e liczba przedziaw bazowych w rozkadzie jest rzdu O(log k), gdzie k jest dugoci rozkadanego przedziau. Tym razem nasze drzewo przedziaowe bdzie penym drzewem binarnym, ktrego wzy odpowiada bd przedziaom bazowym. [0, 3] [0, 1] [2, 3]

[0, 0] [1, 1] [2, 2] [3, 3] Obcienia poszczeglnych przedziaw bazowych zapisywa bdziemy w tablicy load[] o rozmiarze 2N , numerujc wzy drzewa w nastpujcy sposb: 1 2 4 5 6 3 7

Drzewo z powysz numeracj wzw ma nastpujce wasnoci: ojcem wza x jest x/2, lewy syn wza x ma indeks 2x a prawy 2x + 1, wzy parzyste s zawsze lewymi synami swoich ojcw, natomiast nieparzyste prawymi, drzewo przedziaowe o zakresie od 0 do N 1 ma 2N 1 wzw a jego wysoko to log N +1.

< 28 >
8.2.1 Drzewo typu (+,+)

Informatyka +

Zacznijmy od omwienia struktury udostpniajcej nastpujce operacje: insert(a,b,v) dodanie obcienia v do punktw z przedziau [a, b] query(a,b) zsumowanie obcie punktw zawartych w przedziale [a, b] Jak zrealizowa metod insert? Wiemy, e kady przedzia dugoci k mona rozoy na sum O(log k) podprzedziaw bazowych. Wystarczy wic, e znajdziemy rozkad przedziau [a, b] a nastpnie dodamy warto v do kadego wza drzewa odpowiadajcego podprzedziaowi z rozkadu. Aby uproci implementacj, zaczniemy od znalezienia lici odpowiadajcych przedziaom [a, a] i [b, b], a nastpnie, startujc od nich, bdziemy porusza si w gr drzewa, uaktualniajc napotkane wzy odpowiadajce przedziaom zawartym w [a, b]. [0, 7] [0, 3] [0, 1] [2, 3] [4,5] [4, 7] [6, 7]

[0, 0] [1, 1] [2,2] [3,3] [4, 4] [5, 5] [6,6] [7,7] Przedziay pogrubione tworz rozkad przedziau [2, 7]. Warto zauway, e nie zawsze otrzymamy rozkad na minimaln liczb podprzedziaw. atwo jednak uzasadni, e liczba odwiedzonych wzw bdzie rzdu O(log N ). Jeeli bdziemy jednoczenie przechodzi ciekami od lewego i od prawego koca przedziau to nasze cieki w pewnym momencie si spotkaj (by moe dopiero w korzeniu). Jeli nasze cieki jeszcze si nie spotkay i w pewnym momencie znajdziemy si w wle lecym na prawej ciece, ktry jest prawym synem swojego ojca, to kandydatem na przedzia nalecy do szukanego rozkadu jest brat obecnego wza. Analogicznie w sytuacji gdy znajdziemy si w wle lecym na lewej ciece, ktry jest lewym synem swojego ojca. Czy to wystarczy aby mc zaimplementowa operacj query? Niestety nie, poniewa moe si zdarzy, e rozkady dwch zachodzcych na siebie przedziaw nie bd mie wsplnych elementw. wiczenie 20. Podaj przykad takich przedziaw. Musimy zatem przechowywa w wzach drzewa dodatkowe informacje. W dodatkowej tablicy sub[x] pamita bdziemy sum obcie wszystkich wzw poddrzewa o korzeniu x, ktre s zapisane w tym poddrzewie. Przechodzc ciekami w gr drzewa musimy pamita o aktualizowaniu wartoci sub we wszystkich napotkanych wzach, korzystajc z zalenoci rekurencyjnej: sub[x] = sub[left(x)] + sub[right(x)] + load[x] * length(x) gdzie left(x), right(x) to potomkowie wza x, a length(x) to dugo przedziau reprezentowanego przez wze x. Oto kod funkcji insert: void insert(int a, int b, int v) { int l = N + a, r = N + b; int length = 1; // dugo przedziaw na aktualnie odwiedzanym poziomie

> Struktury danych i ich zastosowania


load[l] += v; sub[l] += v; // jeli a==b to nie dodajemy obcienia dwukrotnie if(r != l) { load[r] += v; sub[r] += v; } while(l >= 1) { // jeli l i r nie s ssiadami w drzewie, to sprawdzamy czy // nie trzeba uaktualni wzw wewntrznych if(l < r - 1) { if(l % 2 == 0) // l jest lewym synem swego ojca { load[l + 1] += v; sub[l + 1] += v * length; } if(r % 2 == 1) // r jest prawym synem swego ojca { load[r - 1] += v; sub[r - 1] += v * length; } } // jeli l i r nie s limi, to uaktualniamy ich wartoci sub if(r < N) { sub[l] = sub[2 * l] + sub[2 * l + 1] + load[l] * length; sub[r] = sub[2 * r] + sub[2 * r + 1] + load[r] * length; } // przechodzimy poziom wyej l /= 2; r /= 2; length *= 2; } } Implementacja funkcji query jest podobna:

< 29 >

int query(int a, int b) { int l = N + a, r = N + b; int length = 1; // dugo przedziaw na aktualnie odwiedzanym poziomie // w llen i rlen pamitamy ile punktw przedziau [a,b] zawiera // si w poddrzewie o korzeniu l i r odpowiednio int llen = 1, rlen = (a != b ? 1 : 0); int res = 0; while(l >= 1) {

< 30 >
// sumujemy obcienia z wzw l i r res += llen * load[l] + rlen * load[r];

Informatyka +

// jeli l i r nie s ssiadami w drzewie to sprawdzamy czy // istniej wzy wewntrzne z obcieniem if(l < r - 1) { if(l % 2 == 0) // l jest lewym synem swego ojca { res += sub[l + 1]; llen += length; } if(r % 2 == 1) // r jest prawym synem swego ojca { res += sub[r - 1]; rlen += length; } } // przechodzimy poziom wyej l /= 2; r /= 2; length *= 2; } return res; } 8.2.2 Drzewo typu (+,max)

Drzewo typu (+,max) udostpnia nastpujce metody: insert(a,b,v) dodanie obcienia v do punktw z przedziau [a, b] query(a,b) znalezienie maksymalnego obcienia punktu nalecego do [a, b] Konstrukcja drzewa jest niemal identyczna jak w przypadku (+,+), z tym, e w tablicy sub[x] bdziemy przechowywa maksymalne obcienie wierzchoka z poddrzewa o korzeniu w x. Poniej znajduje si kod metody insert. Implementacja query jest nietrudnym wiczeniem. void insert(int a, int b, int v) { int l = N + a, r = N + b; load[l] += v; sub[l] += v; // jeli a==b to nie dodajemy obcienia dwukrotnie if(r != l) { load[r] += v; sub[r] += v; } while(l >= 1) { // jeli l i r nie s ssiadami w drzewie to sprawdzamy czy // nie trzeba uaktualni wzw wewntrznych

> Struktury danych i ich zastosowania


if(l < r - 1) { if(l % 2 == 0) // l jest lewym synem swego ojca { load[l + 1] += v; sub[l + 1] += v; } if(r % 2 == 1) // r jest prawym synem swego ojca { load[r - 1] += v; sub[r - 1] += v; } } // jeli l i r nie s limi to uaktualniamy ich wartoci sub if(r < N) { sub[l] = max(sub[2 * l], sub[2 * l + 1]) + load[l]; sub[r] = max(sub[2 * r], sub[2 * r + 1]) + load[r]; } // przechodzimy poziom wyej l /= 2; r /= 2; } } 8.2.3 Drzewo typu (max,max)

< 31 >

Tym razem nasze drzewo udostpnia nastpujce metody: insert(a,b,v) dla kadego punktu x nalecego do przedziau [a, b] wykonanie podstawienia load[x] = max(load[x],v) query(a,b) znalezienie maksymalnego obcienia punktu nalecego do [a, b] Implementacja nieznacznie rni si od implementacji drzewa (+,max) pozostawiamy j jako wiczenie.

Zadania
1. Zaimplementuj rekurencyjne odpowiedniki metod insert i query dla dowolnego z omawianych drzew przedziaowych. Porwnaj czasy dziaania obu wersji dla duych danych wejciowych. 2. Jeeli x jest zmienn typu int, to wyraenie ((x ^ (x - 1)) + 1) / 2 moemy zastpi przez (x & -x). Upewnij si, e rozumiesz dlaczego wyraenia te zwracaj t sam warto. 3. Do ktrych drzew mona wstawia za pomoc insert wartoci ujemne? Ktre wymagaj modykacji? 4. Zadanie Koleje z IX Olimpiady Informatycznej (dostpne w [5]).

< 32 >

Informatyka +

Technika zamiatania

Wprowadzenie
Technika zamiatania jest jednym z podstawowych podej do rozwizywania problemw geometrycznych. Na tych zajciach pokaemy przykady kilku problemw, ktre mona rozwiza tym sposobem. Aby przeprowadzi zamiatanie potrzebujemy mioty. Zwykle jest to prosta, ktra przesuwa si nad ca paszczyzn i przeglda kolejno napotykane obiekty (np. punkty, czy te gury). Umwmy si na potrzeby tych notatek, e miota jest pionowa i przesuwa si od lewej do prawej, czyli zgodnie ze wzrostem wsprzdnej x. W miar napotykania kolejnych obiektw miota zapamituje informacje potrzebne do rozwizania problemu. W typowym przypadku wykorzystuje do tego struktur danych tak, jak na przykad drzewo przedziaowe, o ktrym mwilimy na poprzednich zajciach.

Pary przecinajcych si odcinkw


Zamiatanie najlepiej pokaza na konkretnym przykadzie: Na paszczynie znajduje si n pionowych i poziomych odcinkw. Chcemy policzy wszystkie przecicia odcinkw pionowych z poziomymi. Zadanie to rozwiza mona prostym algorytmem w czasie O(n2 ) wystarczy dla kadej pary prostopadych odcinkw stwierdzi, czy przecinaj si ze sob. wiczenie 21. Jak sprawdzaby, czy dwa odcinki s prostopade? Jak mona uy w tym celu iloczynu skalarnego wektorw? My jednak pokaemy znacznie szybsze rozwizanie, ktre dziaa w czasie O(n log n), oparte na technice zamiatania. Nasza miota, czyli pionowa prosta przesuwajca si w prawo, bdzie si zatrzymywa, gdy napotka jedno z nastpujcych zdarze: pocztek poziomego odcinka, koniec poziomego odcinka, odcinek pionowy. Chcemy, by w kadym momencie swojej wdrwki miota znaa odcinki poziome, ktre si pod ni znajduj (przecinaj si z ni). W rzeczywistoci wystarczy przechowywa wsprzdne igrekowe tych odcinkw. W tym celu, gdy miota napotka pocztek poziomego odcinka, musi zapamita jego wsprzdne, a do momentu osignicia jego koca. W momencie, gdy pod miot pojawi si odcinek pionowy, bdziemy zliczali ile odcinkw poziomych si z nim przecina. Zamy, e wsprzdne igrekowe jego dolnego i grnego koca to odpowiednio y1 i y2 . Zapytamy wic miot ile odcinkw poziomych, ktre aktualnie znajduj si w miotle, ma wsprzdne z przedziau [y1 , y2 ]. Po zsumowaniu wynikw dla wszystkich napotkanych pionowych odcinkw otrzymamy ostateczny rezultat. Pozostaje jeszcze powiedzie, jak zrealizowa miot, by mona byo szybko dodawa i usuwa wsprzdne odcinkw, a take zlicza wsprzdne z okrelonych przedziaw. Wykorzystamy w tym celu drzewo potgowe! Kady jego punkt bdzie reprezentowa jedn wsprzdn igrekow, za obcieniem punktu jest liczba odcinkw, ktre s aktualnie pod miot na tej wsprzdnej. Operacje, ktre wykonuje miota odpowiadaj dokadnie operacjom udostpnianym przez drzewo potgowe dokonuje ona zmiany obcienia w danym punkcie oraz sumowania obcie z okrelonego przedziau. Przyjrzyjmy si jeszcze raz strukturze caego rozwizania. Na pocztku musimy stworzy i posortowa po pierwszej wsprzdnej wszystkie zdarzenia, ktre napotka miota. Mona to zrobi

> Struktury danych i ich zastosowania

< 33 >

w czasie O(n log n). Pniej, kade zdarzenie obsugujemy w czasie O(log n). W ten sposb udao nam si rozwiza pierwszy problem, tak jak obiecywalimy, w czasie O(n log n). wiczenie 22. Co naley zrobi gdy nie mona po prostu zastosowa drzewa potgowego, gdy przedzia, w jakim znajduj si wsprzdne kocw odcinkw jest zbyt duy?

Implementacja
Na pocztku napiszmy struktur opisujc zdarzenie. struct event { int x; /* wsprzdna, na ktrej wystpuje zdarzenie */ /* Typ zdarzenia: * /* 1 - pocztek odcinka poziomego * * -1 - koniec odcinka poziomego * * 0 - odcinek pionowy */ int type; int y1, y2; /* dolna i grna wsprzdna y * * (dla odcinkw poziomych y1 = y2) */ int operator<(const event& e) const { if(x == e.x) return type > e.type; else return x < e.x; } }; Funkcja operator< deniuje operator porwnujcy obiekty danej struktury. Powinna ona zwraca warto true wtedy i tylko wtedy, gdy obiekt lokalny jest mniejszy od obiektu przesanego jako parametr. Dziki niej moemy porwnywa dwie zmienne typu event jak zwyke liczby. Gdy mamy zdeniowany taki operator, moemy te posortowa tablic, lub wektor obiektw klasy event przez wywoanie funkcji sort z biblioteki STL. W wyniku tego sortowania chcemy mie zdarzenia uoone w kolejnoci, w jakiej ma je napotyka miota. Dlatego operator ten porzdkuje zdarzenia w pierwszej kolejnoci po wsprzdnej x, a w przypadku remisu porwnuje rwnie typy zdarze. Kolejno, w jakiej naley przetwarza zdarzenia o tej samej pierwszej wsprzdnej, naley dokadnie przemyle. Wyobramy sobie, e na pewnej wsprzdnej x zachodz zdarzenia wszystkich trzech typw: jeden odcinek poziomy si rozpoczyna, inny si koczy, a pewien odcinek pionowy dotyka obydwch odcinkw poziomych. Sprawdzenie, ilu odcinkw poziomych dotyka ten pionowy trzeba wic wykona po wstawieniu odcinkw, ktre zaczynaj si na danej wsprzdnej, ale przed usuniciem tych, ktre wanie si kocz. Std, poszczeglne typy zdarze, w ramach tej samej pierwszej wsprzdnej, s sortowane wanie w tej kolejnoci. Pamitaj, e za kadym razem gdy projektujemy algorytm uywajcy techniki zamiatania, trzeba zastanowi si w jakiej kolejnoci naley przeglda zdarzenia o tej samej pierwszej wsprzdnej. Czasami odpowiednia kolejno jest jednoznacznie wyznaczona (jak w powyszym przykadzie), ale zdarza si te, e pewne obliczenia chcemy wykona dopiero po obsueniu wszystkich zdarze na danej wsprzdnej. Przejdmy teraz do kodu realizujcego zamiatanie: // events - kontener typu vector<event> przechowujcy zdarzenia // sortujemy zdarzenia zgodnie ze zdefiniowanym porzdkiem sort(events.begin(), events.end());

< 34 >
// zmienna zliczajca znalezione przecicia long long inter = 0; for(int i=0; i<(int)events.size(); i++) { if(events[i].type == 0) //odcinek pionowy inter += query(events[i].y1, events[i].y2); else insert(events[i].y1, events[i].type); }

Informatyka +

Korzystamy z drzewa potgowego, omawianego na poprzednich zajciach. Przypomnijmy, e insert(x, v) dodaje obcienie v do punktu x, za query(a,b) zwraca sum obcie na przedziale [a, b].

Co jeszcze potra zamiatanie?


Wachlarz problemw, ktre potramy rozwiza przez zamiatanie, zaley w duej mierze od znanych nam struktur danych. Na przykad, korzystajc z drzewa przedziaowego typu (+, max), rozwiza moemy nastpujcy problem: na paszczynie znajduj si prostokty o bokach rwnolegych do osi ukadu wsprzdnych; znajd punkt, ktry jest przykryty najwiksz liczb prostoktw. Jako zdarzenia rozpatrujemy tu lewe i prawe boki prostokta, a miota pamita, ile prostoktw znajduje si pod ni na kadej wsprzdnej. Gdy napotykamy lewy bok prostokta dodajemy jedynk na przedziale wyznaczonym przez wsprzdne igrekowe jego poziomych bokw. Po kadym dodaniu prostokta, szukamy punktu w miotle, ktry jest przykryty najwiksz liczb prostoktw. wiczenie 23. W tym problemie te naley uwaa na sytuacj, w ktrej wiele zdarze ma t sam pierwsz wsprzdn. W jakiej kolejnoci naley je przeglda, by algorytm dziaa poprawnie?

9.1

Zamiatanie ktowe

Zajmiemy si teraz troch innym sposobem przegldania obiektw na paszczynie. Nie ma on dobrze rozpowszechnionej nazwy, jednak z powodu pewnych podobiestw do zamiatania, bdziemy go nazywa zamiataniem ktowym. W tym przypadku, miota jest pprost zaczepion w ustalonym punkcie. Pprosta ta przeglda paszczyzn wykonujc obrt o 360 . Tym razem umwmy si, e pprosta obraca si bdzie przeciwnie do ruchu wskazwek zegara. Rozwamy nastpujcy przykadowy problem: Na paszczynie znajduje si n punktw, spord ktrych adne trzy nie le na jednej prostej. Z kadym punktem skojarzona jest waga, okrelona dodatni liczb cakowit. Chcemy narysowa na paszczynie prost, tak, by sumy wag punktw po obydwch stronach prostej byy sobie jak najblisze. Zastanwmy si najpierw jak napisa rozwizanie, ktre rozpatrzy wszystkie moliwe proste. Po pierwsze, zauwamy, e moemy ograniczy si do rozwaania takich prostych, ktre przechodz przez dokadnie dwa punkty. Nawet jeli pewna optymalna prosta nie przechodzi przez aden punkt, moemy j tak przesun i obrci, by opara si na dwch punktach. Z drugiej strony, poniewa nie ma trzech punktw wspliniowych, jeli mamy prost przechodzc przez dwa punkty, to moemy j minimalnie przesun lub obrci, tak by kady z tych dwch punktw znalaz si po jednej lub drugiej stronie prostej (patrz rysunek). Rozwizanie naszego problemu wyglda zatem tak: dla kadej pary punktw (takich par jest O(n2 )) prowadzimy przez nie prost, liczymy sumy wag punktw po jednej i drugiej stronie (w czasie O(n)) a nastpnie rozpatrujemy 4 moliwoci przydzielenia punktw z prostej do jednej lub drugiej czci paszczyzny. W ten sposb dostajemy algorytm w zoonoci O(n3 ).

> Struktury danych i ich zastosowania

< 35 >

Rys. 1: Prosta przechodzca przez dwa punkty i cztery moliwoci jej przesunicia i obrotu

Korzystajc z zamiatania ktowego poprawimy czas dziaania do O(n2 log n). Dla kadego punktu z paszczyzny, bdziemy wykonywali dookoa niego zamiatanie ktowe. Chcemy w kadym momencie wykonania algorytmu zna sum wag punktw na ppaszczynie po lewej stronie mioty (tj. po stronie przeciwnej do ruchu wskazwek zegara). Gdy miota napotka punkt, naley go z mioty usun. Jeli za punkt znajdzie si na przedueniu mioty, jest on do niej dodawany. Sytuacj wyjaniaj rysunki.

Rys. 2: Moment dodania punktu do mioty (po lewej) oraz usunicia punktu (po prawej).

W momencie, gdy napotykamy punkt, sprawdzamy cztery moliwoci narysowania prostej, odpowiadajce moliwociom przydzielenia punktw do jednej z dwch ppaszczyzn. Mona sobie zaoszczdzi nieco pracy i zauway, e wystarczy sprawdza jedynie dwie opcje: punkt wok ktrego zamiatamy przydzielamy do jednej lub drugiej strony paszczyzny. W ten sposb rwnie rozwaymy wszystkie dostpne podziay paszczyzny.

9.2

Sortowanie ktowe

Implementacj zamiatania ktowego pozostawiamy jako wiczenie, opowiemy jedynie pokrtce, jak w prosty sposb posortowa punkty ktowo. Z pomoc przychodzi nam funkcja atan2 z biblioteki standardowej C++. double atan2(double y, double x); Tak, to nie pomyka, pierwszym argumentem tej funkcji jest wsprzdna y, za drugim wsprzdna x. Funkcja ta zwraca kt, jaki tworzy odcinek czcy punkt (x, y) z dodatni posi X wyraony w radianach (zwracana warto jest liczb z przedziau [, ]). Jeeli zamiast na

< 36 >

Informatyka +

radianach, wolimy operowa na stopniach z przedziau [180, 180] to moemy otrzyman warto pomnoy przez 180 . Aby uy funkcji atan2, naley do programu doczy plik nagwkowy cmath. Trzeba by jednak ostronym uywajc funkcji atan2, poniewa zapisuje ona wynik w liczbie zmiennoprzecinkowej (pamitanej w komputerze z ograniczon dokadnoci). Jeli kty wyznaczone przez dwa punkty rni si bardzo nieznacznie, atan2 moe zwrci dla nich t sam warto. Zwykle uwaa si, e jeeli wsprzdne punktw maj wartoci bezwzgldne wiksze ni 106 , bezpieczniej jest uy funkcji long double atan2l(long double y, long double x); i zmiennych typu long double. Jeli wsprzdne mog by wiksze ni 109 , nawet wyniki atan2l mog by niewystarczajco dokadne. wiczenie 24. Ambitne: Jak wykorzysta iloczyn wektorowy do ktowego sortowania? Czy te trzeba si wtedy martwic o dokadno wyniku?

Zadania
1. Zadanie Wyspy z XI Olimpiady Informatycznej (dostpne w [5]). 2. Na paszczynie zamalowujemy na czarno wntrza n prostoktw o bokach rwnolegych do osi ukadu wsprzdnych. Jakie jest pole zamalowanej czci paszczyzny? 3. Zaimplementuj rozwizanie zadania z rozdziau o zamiataniu ktowym. 4. Na paszczynie dane s punkty. Znajd takie trzy spord nich, ktre tworz kt o jak najwikszej mierze.

10

Drzewa TRIE

Problem
Wiemy ju, e za pomoc algorytmu KMP jestemy w stanie znale wszystkie wystpienia wzorca w tekcie w czasie liniowym. Problem wyszukiwania wzorca mona nieco uoglni i zapyta jak szybko jestemy w stanie znale wszystkie wystpienia wielu wzorcw w zadanym tekcie. Moemy uy algorytmu KMP do kadego wzorca z osobna a nastpnie scali wyniki. Jeeli wszystkich wzorcw jest n a tekst ma dugo m to rozwizanie to ma zoono O(nm). Algorytm Aho-Corasick pozwala znacznie polepszy ten wynik.

Drzewo TRIE
Bdziemy korzystali ze struktury danych o nazwie TRIE. Jest to drzewo ukorzenione, ktrego krawdzie s etykietowane symbolami danego alfabetu. W dalszej czci bdziemy zakadali, e nasz alfabet to zbir liter alfabetu angielskiego. Drzewo TRIE reprezentuje pewien zbir sw nad ustalonym alfabetem. Inaczej ni w dotychczas omawianych drzewach, elementy zbioru nie s przechowywane w wzach lecz mona je odtworzy na podstawie pozycji wzw w drzewie. Sowo reprezentowane przez wze x otrzymamy przechodzc po ciece od korzenia do x i odczytujc litery na kolejnych krawdziach. Nie wszystkie wzy bd reprezentowa elementy znajdujcy si w zbiorze, dlatego w kadym wle przechowujemy dodatkowo warto logiczn, wskazujc czy wze ten odpowiada istniejcemu elementowi.

> Struktury danych i ich zastosowania


a a c b a a c b

< 37 >

Na rysunku powyej zamalowane wzy odpowiadaj sowom przechowywanym w drzewie. Korze (oznaczony symbolem ) odpowiada sowu pustemu. Powysze drzewo reprezentuje zbir sw {aa, ab, abc, caa, cb}, natomiast sowa a, c czy ca to tego zbioru nie nale. Drzewo TRIE udostpnia bdzie nastpujce metody: insert wstawienie sowa do zbioru, search sprawdzenie, czy sowo naley do zbioru, erase usunicie sowa ze zbioru. W kadym wle musimy przechowywa zbir wskanikw do potomkw. Jeli nasz alfabet nie jest zbyt duy (a najczciej bdzie to alfabet angielski, czyli 26 liter) moemy pokusi si o dodanie do kadego wza 26-elementowej tablicy wskanikw, co nieco uproci implementacj. Wie si to jednak z troch wikszym narzutem pamiciowym oraz koniecznoci zerowania tablic w konstruktorze. Alternatywnym podejciem jest zastosowanie kontenera map z biblioteki STL, w ktrym przechowywane s pary (litera, odpowiadajcy jej wskanik do potomka). Aby nie komplikowa nadmiernie implementacji, zastosujemy pierwsz metod. Oto (niepena) implementacja drzewa TRIE:

#define ALPH 26

// rozmiar alfabetu

struct TrieNode // wze drzewa TRIE { // konstruktor ustawiajcy ojca i znak na krawdzi prowadzcej od ojca TrieNode(TrieNode *_parent, char _last) { for(int i = 0; i < ALPH; ++i) links[i] = NULL; parent = _parent; last = _last; in_dict = false; } TrieNode *links[ALPH]; TrieNode *parent; char last; bool in_dict; }; // // // // tablica potomkw wskanik do ojca znak na krawdzi prowadzcej od ojca czy sowo jest w drzewie

< 38 >
struct Trie { Trie() // konstruktor tworzcy korze drzewa { root = new TrieNode(NULL, -); }

Informatyka +

void insert(string &word) { TrieNode *v = root; // wskanik na aktualny wze for(int i = 0; i < (int)word.size(); ++i) { if(v->links[word[i] - a] == NULL) // jeli nie ma jeszcze krawdzi z dan liter // to utwrz nowego potomka v->links[word[i] - a] = new TrieNode(v, word[i]); v = v->links[word[i] - a]; } v->in_dict = true; } bool search(string &word) { TrieNode *v = root; // wskanik na aktualny wze for(int i = 0; i < (int)word.size(); ++i) { if(v->links[word[i] - a] == NULL) // jeli nie ma krawdzi z dan liter to zwr fasz return false; else // jeli jest to przejd po niej v = v->links[word[i] - a]; } return v->in_dict; } TrieNode *root; }; wiczenie 25. Uzupenij powysz implementacj o brakujc metod erase oraz destruktory zwalniajce zaalokowan pami. Przy zaoeniu, e nasz alfabet jest 26-elementowy, moemy przyj, e wszystkie operacje na zbiorze links wykonywane s w czasie staym. Przy takim zaoeniu, zoono wszystkich operacji na drzewie TRIE jest liniowa wzgldem dugoci wstawianego (lub usuwanego czy szukanego) sowa. Jeeli nasz alfabet byby duy i mia rozmiar k to zoonoci poszczeglnych operacji na sowie o dugoci n zaleayby od implementacji: TRIE z mapami TRIE z tablicami utworzenie wza O(1) O(k) insert O(n log k) O(n k) search O(n log k) O(n) erase O(n log k) O(n)

> Struktury danych i ich zastosowania

< 39 >

11

Algorytm Aho-Corasick

Dziaanie algorytmu Aho-Corasick (w skrcie AC) jest analogiczne do algorytmu KMP. Dlatego te potrzebowa bdziemy odpowiednika tablicy preksowej w postaci drzewa TRIE, do ktrego pocztkowo wstawimy wszystkie wzorce. W kadym wle naszego drzewa chcemy pamita dodatkow warto suf link. Jeli wze x reprezentuje sowo s to chcemy aby x.suf link by wskanikiem do wza reprezentujcego najduszy waciwy suks sowa s, ktry jest jednoczenie preksem pewnego wzorca. Rnica wzgldem algorytmu KMP jest taka, e suks ten nie musi by preksem tego samego wzorca (nie musi by jego prekso-suksem). Wartoci pl suf link obliczymy podobnie jak w algorytmie KMP, przy czym drzewo przechodzi bdziemy algorytmem BFS (mona tutaj rwnie dobrze uy algorytmu DFS):

void create_links(Trie &trie) { // przechodzimy drzewo algorytmem BFS queue<TrieNode*> Q; // wstawiamy korze do kolejki Q.push(trie.root); trie.root->suf_link = trie.root->dict_link = NULL; while(!Q.empty()) { TrieNode *v = Q.front(); Q.pop(); if(v != trie.root) // wze nie jest korzeniem { // postpujemy tak jak w KMP - cofamy si po kolejnych suf_linkach // dopki nie natrafimy na krawd o odpowiedniej etykiecie TrieNode *j = v->parent->suf_link; while(j && j->links[v->last - a] == NULL) j = j->suf_link; if(j == NULL) v->suf_link = trie.root; else v->suf_link = j->links[v->last - a]; if(v->suf_link->in_dict) // ustawiamy dict_link v->dict_link = v->suf_link; else v->dict_link = v->suf_link->dict_link; } // wstawiamy wszystkich potomkw v do kolejki for(int i = 0; i < ALPH; ++i) if(v->links[i] != NULL) Q.push(v->links[i]); } } W powyszym kodzie, dla kadego wza v ustawiana jest dodatkowo warto v->dict link, ktra jest wskanikiem do najduszego waciwego suksu wza v, ktry jest jednym z wzorcw.

< 40 >

Informatyka +

Moe si bowiem zdarzy, e w jednym miejscu tekstu koczy si kilka dopasowa wzorcw (na przykad wyszukiwanie wzorcw ze zbioru {abc, bc, c} w tekcie abcd). W takim przypadku, wskanik dict link bdzie potrzebny do odtworzenia wszystkich dopasowanych wzorcw. Zasada dziaania algorytmu AC jest taka, jak algorytmu KMP: void aho_corasick(Trie &trie, string &text) { TrieNode *v = trie.root, *tmp; for(int i = 0; i < (int)text.size(); ++i) { while(v && v->links[text[i] - a] == NULL) v = v->suf_link; if(v == NULL) v = trie.root; else v = v->links[text[i] - a]; // sprawdzamy, czy s jakie dopasowania if(v->in_dict) tmp = v; else tmp = v->dict_link; while(tmp) { printf("Znaleziono wystapienie wzorca numer %d.\n", tmp->id); tmp = tmp->dict_link; } } } Jeeli po przetworzeniu kolejnej litery z tekstu znajdziemy si w wle drzewa TRIE rnym od korzenia, musimy sprawdzi, czy ten wze nie reprezentuje wzorca, a take wypisa wszystkie dopasowania osigalne poprzez dict link. wiczenie 26. Wanym przypadkiem szczeglnym jest sytuacja, w ktrej wszystkie wzorce s jednakowej dugoci. Zakadajc, e takie wanie s dane wejciowe, zaimplementuj alternatywn wersj algorytmu AC, nie korzystajc z dict linkw. Jak zoono ma algorytm Aho-Corasick? Wstawienie wzorcw do drzewa odbywa si w czasie liniowym wzgldem sumy ich dugoci. Funkcja create links rwnie dziaa w czasie liniowym wzgldem sumy dugoci wzorcw, co mona uzasadni analogicznie jak w oszacowaniu zoonoci algorytmu KMP. Wreszcie funkcja aho corasick, ktra, pomijajc przegldanie wskanikw dict link, dziaa liniowo wzgldem sumy dugoci wzorcw i tekstu. Moe si jednak zdarzy, e ilo dopasowa wzorcw bdzie rzdu kwadratowego (na przykad wyszukiwanie wzorcw ze zbioru {a, aa, aaa, aaaa} w tekcie aaaa. A zatem zoono algorytmu AC to O(n + Sw + d), gdzie n jest dugoci tekstu, Sw sum dugoci wzorcw a d sum wystpie wzorcw w tekcie.

11.1

Algorytm Bakera

Wanym zastosowaniem algorytmu AC jest algorytm Bakera, ktry suy do wyszukiwania dwuwymiarowego wzorca w dwuwymiarowym tekcie. Zamy, e nasz wzorzec jest macierz o wy wierszach i wx kolumnach oraz elementach nalecych do alfabetu. Analogicznie tekst jest macierz o ty wierszach i tx kolumnach.

> Struktury danych i ich zastosowania

< 41 >

Algorytm Bakera dziaa w nastpujcy sposb: Wyszukujemy kolumny wzorca w kolumnach tekstu za pomoc algorytmu AC. Znalezione dopasowania zapisujemy w dodatkowej tablicy dwuwymiarowej hits[tx ][ty ]. Kolumnom wzorca nadajemy identykatory bdce dodatnimi liczbami naturalnymi, przy czym istotne jest, aby przystajce kolumny otrzymay ten sam identykator. Przykad:
wzorzec: identykatory kolumn:

aba bab 121

tekst:

abad baba cbab

tablica hits:

1210 0121 0000

Liczba znajdujca si na pozycji hits[i][j] jest identykatorem kolumny wzorca, ktra wystpuje w tekcie w kolumnie i-tej, w wierszach od j do j +wy 1. Liczba 0 oznacza brak dopasowania. Majc wyznaczon tablic hits wystarczy, e za pomoc algorytmu KMP znajdziemy wszystkie wystpienia sowa zoonego z kolejnych identykatorw kolumn wzorca (na powyszym przykadzie 121) w wierszach tablicy hits. Warto zauway, e ostatnie wy 1 wierszy tablicy hits jest zawsze wypenionych zerami i mona je zignorowa. wiczenie 27. Zaimplementuj algorytm Bakera. Jako, e wszystkie wzorce wyszukiwane algorytmem AC s rwnej dugoci, zoono algorytmu Bakera jest liniowa wzgldem sumy wielkoci tekstu i wzorca.

12

Maski bitowe

Binarna reprezentacja zbiorw


Wiemy, e dane w pamici komputera zapisane s w postaci kodu binarnego. Okazuje si, e moemy to wykorzysta w celu wygodnego przechowywania i przetwarzania informacji o zbiorach. Zajmiemy si 32-bitowym typami cakowitymi dodatnimi np. unsigned int. Liczba 75 w pamici ma posta: numer bitu warto warto bitu ... ... ... 7 27 0 6 26 1 5 25 0 4 24 0 3 23 1 2 22 0 1 21 1 0 20 1

Denicja 6. Maska bitowa to forma kodowania informacji o caym zbiorze w jednej zmiennej. Ustawienie bitu o numerze i na 1 lub 0 (mwimy czasem o zapaleniu bitu) informuje o wystpowaniu lub braku elementu i w zbiorze. Przykadem moe by przechowywanie informacji o podzbiorach zbioru liter alfabetu angielskiego. Literom od a do h nadajemy numerki od 0 do 7. Zbiorowi {a, b, d, h} odpowiada zatem maska 100010112 = 27 + 23 + 21 + 20 = 139. wiczenie 28. Znajd mask dla zbioru {a, d, f, g} i zbir reprezentowany przez mask 83. Jak w prosty sposb budowa maski bitowe? Su do tego operatory bitowe, m.in: a << b przesuwa bity zmiennej a o b bitw w lewo (z prawej dopeniajc zerami). Maska reprezentujca {a, g, h} to zatem (1<<0) + (1<<6) + (1<<7) (nawiasy s istotne, operacje bitowe maj niski priorytet). Przykad: 100110112 << 2 = 011011002

< 42 >

Informatyka +

a >> b przesuwa bity zmiennej a o b bitw w prawo (z lewej dopeniajc zerami). Zachowuje si tak samo jak dzielenie (cakowitoliczbowe) przez 2b . Przykad: 100110112 >> 2 = 001001102 ~a neguje, zamienia wszystkich bitw na przeciwne. Ze zbioru robi jego dopenienie. Przykad: 100110112 = 011001002

Operatory bitowe
Dziaanie operatorw bitowych na pojedynczych bitach (i odpowiadajce im nazwy z logiki): Zmienne b a 0 0 1 0 0 1 1 1 Koniunkcja a & b 0 0 0 1 Alternatywa a | b 0 1 1 1 Alternatywa wykluczajca a ^ b 0 1 1 0

Wszelkie operatory bitowe s wykonywane niezwykle szybko, mniej wicej tak, jak dodawanie dwch liczb. Ich dziaanie na caych zmiennych polega na wykonaniu powyszych operacji na kadej parze bitw. Moemy wic szybko wykonywa operacje na zbiorach, dziki nastpujcym operatorom: Koniunkcja: a & b Przykad: 100110112 & 110011112 = 100010112 Koniunkcja jest prawdziwa tylko jeeli oba argumenty s prawdziwe. W zwizku z tym zastosowana do dwch zbiorw daje ich cz wspln, bo zachowane s te bity, ktre byy zapalone w obu maskach. Trik: (a & (1<<nr)) != 0 sprawdza czy bit o numerze nr jest jedynk Alternatywa: a | b Przykad: 100110112 | 110011112 = 110111112 Alternatywa sprawdza czy cho jeden z jej argumentw jest rwny 1. Zatem wynikiem alternatywy dwch masek jest ich suma (teoriomnogociowa). Alternatywa wykluczajca: a ^ b Przykad: 100110112 ^ 110011112 = 010101002 Operacj t czsto nazywamy zaczerpnitym z angielskiego skrtem xor. Sprawdza ona, czy dokadnie jeden z jej argumentw jest rwny 1. Trik: (a ^ b) & a to maska bdca rnic zbiorw reprezentowanych przez maski a i b. wiczenie 29. Majc dane maski reprezentujce zbiory a, b i c znajd za pomoc powyszych operatorw: cz wspln tych trzech zbiorw zbir elementw, ktre wystpuj w dokadnie dwch spord zbiorw a, b, c rnic zbiorw a i b, bez uycia operatora ^

Wszystkie podzbiory
Maski bitowe bardzo zgrabnie wykorzystujemy w rozwizaniach polegajcych na przejrzeniu wszyst kich podzbiorw. Przykadowy problem: Mamy dane liczby a1 , a2 , . . . , an . Czy da si z nich wybra podzbir o sumie S?

> Struktury danych i ich zastosowania


bool odp = false; /* Maski odpowiadajce wszystkim podzbiorom zbioru * * n-elementowego to po prostu liczby od 0 do (1 << n)-1 */ int ogr = 1 << n; for (int maska = 0; maska < ogr; maska++) { int suma = 0; /* sprawdzamy, ktre elementy s w sprawdzanym podzbiorze */ for (int i = 0; i < n; i++) if ((maska & (1<<i)) != 0) suma += a[i]; if (suma == S) { odp = true; break; } } /* Wynik: w zmiennej odp */ wiczenie 30. Okrel zoono powyszego rozwizania.

< 43 >

wiczenie 31. (Ambitne) Masz dan mask bitow reprezentujc pewien zbir. W jaki prosty sposb moesz wygenerowa wszystkie podzbiory tego zbioru? Wskazwka: Naley je generowa w odwrotnej kolejnoci ni w powyszym programie.

12.1

Programowanie dynamiczne na maskach

Czasami maski bitowe staj si stanami w rozwizaniach opartych na programowaniu dynamicznym. Dzieje si tak gdy musimy zapamita nie tylko ile obiektw ju wykorzystalimy, ale te dokadnie ktre z nich. Przyjrzyjmy si nastpujcemu problemowi: Chcemy uoy w szereg n przedszkolakw. Henio i Kazio uwielbiaj sta koo siebie, ale niestety Ada i Micha s wyranie wrogo do siebie nastawieni. Oglniej, dla kadej pary przedszkolakw wiemy, jak bardzo bd zadowoleni z bycia ssiadami (dla dzieci o numerach i i j liczb t oznaczmy przez wij 0). Naley wyznaczy maksymaln sum zadowolenia, jaka moe by osignita. Mona ten problem rozwiza sprawdzajc wszystkie moliwe permutacje przedszkolakw, w czasie O(nn!). Istnieje jednak szybszy algorytm z wykorzystaniem programowania dynamicznego. Polega na rozwizaniu wszystkich moliwych podproblemw, tzn. znalezienia maksymalnego zadowolenia kadego podzbioru dzieci, z ustalonym pewnym przedszkolakiem na kocu szeregu. Wyniki tych podproblemw bdziemy przechowywa w tablicy: t[mask][a] - maksymalne zadowolenie zbioru przedszkolakw (przechowywanego w masce mask) w ustawieniu gdzie dziecko o numerze a jest na kocu. Zauwamy, e jeeli maski bdziemy przeglda po prostu w kolejnoci rosncej, to w momencie obliczania wyniku dla pewnego zbioru, wszystkie jego podzbiory bd ju przetworzone (bdziemy znali ich wyniki). for (int maska = 1; maska < (1 << n); maska++) for (int i = 0, a = 1; i < n; i++, a <<= 1) /* ustalamy ostatnie dziecko z danego zbioru */ if (a & maska) { if ((a ^ maska) == 0) { /* przypadek samotnego dziecka */

< 44 >
t[maska][i] = 0; continue;

Informatyka +

} int nw = 0; for (int j = 0, b = 1; j < n; j++, b <<= 1) { /* sprawdzamy wszystkie moliwe przedostatnie przedszkolaki, * * rne od ostatniego i bdce w przetwarzanym zbiorze */ if (j != i && (b & maska)) /* t[maska ^ a][j] to wynik dla zbioru bez ostatniego dziecka * * z j-tym dzieckiem na kocu */ if (t[maska ^ a][j] + w[i][j] > nw) nw = t[maska ^ a][j] + w[i][j]; } t[maska][i] = nw; } wiczenie 32. Jak z wypenionej tablicy t uzyska wynik dla caego zadania? Zoono tego algorytmu to O(2n n2 ). Jest to wynik istotnie lepszy ni O(nn!). Przykadowo, dla n = 15 nasz algorytm potrzebuje okoo 7 milionw operacji dominujcych, a rozwizanie brutalne ponad 10 bilionw ! wiczenie 33. Rozszerz powyszy algorytm by otrzyma nie tylko warto, ale take optymalne rozmieszczenie dzieci.

12.2

Meet in the middle

Meet in the middle, dosownie spotkajmy si w poowie, nie ma pki co oglnie przyjtej polskiej nazwy. Zachcam do wymylenia czego! Technika ta to prba przypieszenia rozwiza brutalnych: dzielimy wejciowy zbir na dwa, w kadym z nich robimy pene przeszukiwanie, a nastpnie staramy si zczy wyniki. Sprbujemy teraz ponownie zmierzy si z problemem szukania podzbioru o okrelonej sumie. Zastosowanie meet in the middle wyglda nastpujco: dzielimy zbir na dwa, w kadym z nich generujemy wszystkie moliwe sumy podzbiorw, sortujemy i prbujemy dobra pary liczb po jednej z kadej posortowanej listy, ktre maj odpowiedni sum. wiczenie 34. Jak zrealizowa wyszukiwanie binarne w czasie liniowym wzgldem dugoci list? n Jak wyglda kwestia zoonoci? Zamiast wyjciowego O(2n n) otrzymujemy O(2 2 n). Zatem asymptotycznie udao nam si zmniejszy sta w wykadniku dwa razy, co jest niezym wynikiem. wiczenie 35. Dokonaj wasnorcznie szacowa zoonoci tego algorytmu. Nie zapomnij, e konieczne jest sortowanie!

Zadania
A. Suma podzbioru Zaimplementuj program ilustrujcy technik meet in the middle: masz stwierdzi czy ze zbioru a1 , a2 , . . . , an da si wybra podzbir o sumie S. Wejcie: nS a1 a2 . . . an Wyjcie: TAK, lub NIE.

> Struktury danych i ich zastosowania

< 45 >

B. Szczeglna wycieczka Mamy dany waony, skierowany graf G o n wierzchokach i m krawdziach. Podrujemy w nim midzy wierzchokami o numerze 1 i n. W grae jest te wyrnione k wierzchokw (o numerach rnych od 1 i n), przez ktre musimy przejecha dokadnie jeden raz. Znajd minimalny czas potrzebny na przebycie trasy! Wejcie: nm p1 k1 w1 p2 k2 w2 ... pn kn wn Trjka pi , ki , wi oznacza, e midzy wierzchokami pi oraz ki istnieje krawd o wadze wi . Wyjcie: Pojedyncza liczba - dugo najkrtszej moliwej trasy.

Literatura
1. Cormen T.H., Leiserson C.E., Rivest R.L., Stein C., Wprowadzenie do algorytmw, WNT, Warszawa 2004 2. Banachowski L., Diks K., Rytter W., Algorytmy i struktury danych, WNT, Warszawa 2003 3. Diks K., Malinowski A., Rytter W. Wale T., Algorytmy i struktury danych, http://wazniak.mimuw.edu.pl/index.php?title=Algorytmy_i_struktury_danych 4. Standard Template Library Programmers Guide, http://www.sgi.com/tech/stl/ 5. Modzieowa Akademia Informatyki, http://main.edu.pl/

< 46 > Notatki

Informatyka +

Notatki

< 47 >

W projekcie Informatyka +, poza wykadami i warsztatami, przewidziano nastpujce dziaania:

24-godzinne kursy dla uczniw w ramach moduw tematycznych

24-godzinne kursy metodyczne dla nauczycieli, przygotowujce do pracy z uczniem zdolnym

nagrania 60 wykadw informatycznych, prowadzonych przez wybitnych specjalistw i nauczycieli akademickich

konkursy dla uczniw, trzy w cigu roku udzia uczniw w pracach k naukowych

udzia uczniw w konferencjach naukowych

obozy wypoczynkowo-naukowe.

Szczegowe informacje znajduj si na stronie projektu www.informatykaplus.edu.pl

You might also like