Autor: @Sztakler # AiSD Egzamin Poprawkowy 2017 ### Zad. 1 ![](https://i.imgur.com/sQSfwSc.png) Mamy dwa ciągi A i B, o długości odpowiednio n i m. Niech LCS(A,B) to najdłuższy wspólny podciąg obu tych ciągów, a $A_x$ oznacza x-literowy prefiks ciągu A. Korzystamy z następującej zależności rekurencyjnej: * $LCS(A_i, B_j) = 1 + LCS(A_{i-1}, B_{j-1})$, jeśli $A_i$ oraz $B_j$ kończą się na tą samą literę, * $LCS(A_i, B_j) = max(LCS(A_i, B_{j-1}), LCS(A_{i-1}, B_j))$, jeśli kończą się na różne litery. ```python= M[n][m] = 0 // wyzeruj tablicę for i = 0 to n: for j = 0 to m: if A[i] == B[j]: M[i][j] = 1 + M[i-1][j-1] else: M[i][j] = max(M[i][j-1], M[i-1][j]) return M[n][m] ``` Powyższy algorytm wyznacza długość najdłuższego wspólnego podciągu A i B. Sam LCS wyznaczamy, cofając się po tablicy, stosując tę samą zależność rekurencyjną, co wcześniej. ### Zad. 2 ![](https://i.imgur.com/vZg9wBL.png) Operacja decreasekey zmniejsza wartość klucza i odcina go, jeśli przestaje spełniać porządek kopcowy. Jeśli ojciec tego wierzchołka wcześniej utracił syna, teraz należy odciąć także jego samego i sprawdzać rekurencyjnie kolejne wierzchołki wyżej na ścieżce do korzenia. Może się zdarzyć, że operacja przejdzie w ten sposób całą długość ścieżki do korzenia, która wynosi O(logn), ponieważ liczba wierzchołków w drzewie Fibonacciego jest wykładnicza względem rzędu. ### Zad. 3 ![](https://i.imgur.com/3g2NkWQ.png) Tak, wystarczy rozważyć drzewo o korzeniu $y$ z dwoma poddrzewami $l$ i $r$ o wysokościach $h_l$ i $h_r$. Jeśli $h_l \leq h_r$, wtedy splay na korzeniu drzewa $l$, tj. splay($x_l$) sprawi, że $x_l$ stanie się ojcem $y$, przez co wysokości $h_r$ wzrośnie o 1. ![](https://i.imgur.com/D5v0ApN.png) Po wykonaniu find(1): ![](https://i.imgur.com/zHD829a.png) ### Zad. 4 ![](https://i.imgur.com/Yrfvjrd.png) Pamiętamy z wykładu, że dla zbioru $n$ kluczy istnieje drzewiec dla dowolnego przydziału priorytetów, co więcej, jeśli są to **różne** priorytety, wtedy taki drzewiec jest jedyny. ![](https://i.imgur.com/0Oivl4h.png) ![](https://i.imgur.com/bDBJTGg.png) Oznacza to, że możemy zbudować dokładnie jeden taki drzewiec. ### Zad. 5 ![](https://i.imgur.com/1DnAOxY.png) *Element Uniqueness* to problem polegający na określeniu, czy dany zbiór składa się z parami różnych elementów. Model liniowych drzew decyzyjnych pozwala pokazać, że dla danego zbioru danych istnieje $n!$ ścieżek, prowadzących do różnych liści, gdzie liście są odpowiedzią na pytanie zadane w problemie, tzn. "TAK" lub "NIE", a zatem długość ścieżek jest $\Omega (n\ log\ n)$. Model zwykłych drzew decyzyjnych nie pozwala nam na przeprowadzenie takiego dowodu, ponieważ wymaga on, by liście drzewa decyzyjnego były różne, np. by każdy z nich zawierał inną permutację elementów na wejściu, a problem *Element Uniqueness* zwraca jedynie dwie możliwe odpowiedzi. ### Zad. 6 ![](https://i.imgur.com/SC90qH7.png) Do wyznaczania położenia elementu $x$ stosujemy funkcję $h(x, i) = (h_1(x) + ih_2(x))\ mod\ m$. Sprawia ona, że w przypadku i-tej kolizji przesuniemy wyznaczoną pozycję o wartość wylosowaną przez drugą funkcję haszującą $h_2$. ### Zad. 7 ![](https://i.imgur.com/AqelXB1.png) Pamiętamy, że Quicksort najpierw wybiera pivot, następnie wykonuje podział tablicy na elementy mniejsze i większe od pivota, a na końcu wywołuje się rekurencyjnie na obu tych podtablicach. Oznacza to, że złożoność $T(n)$ Quicksorta dla ciągu długości n to: $$ T(n) = 2T(n/2) + T(partition) + T(pivot) $$ Stosując algorytm magicznych piątek jesteśmy w stanie znaleźć pivot w czasie liniowym. Partition oczywiście jest wykonywany w O(n). Stąd: $$ T(n) = 2T(n/2) + O(n) + O(n)\\ T(n) = 2T(n/2) + O(n)\\ T(n) = O(n\ log\ n) $$ ### Zad. 8 ![](https://i.imgur.com/J2Kemg6.png) ```python= def find_prefix_function(P): m = length(P) pi[1] = 0 k = 0 for i = 2 to m: while k > 0 && P[k+1] != P[i]: k = pi[i] if P[k+1] == P[i]: k = k + 1 pi[i] = k return pi ``` Czas działania tej procedury to O(m). Zauważmy, że pętla for wykona się m-1 razy. W każdym z jej obrotów wykonuje się pętla while, jednak zauważmy, że wykona się ona co najwyżej tyle razy, ile wynosi k, ponieważ w każdej jej iteracji możemy zmniejszyć k o co najmniej 1. Zauważmy jednak, że w każdej iteracji for k moze wzrosnąć o co najwyżej 1 (podstawienie za niego pi[i] może je tylko zmniejszyć, bo pi[i] < k). Oznacza to, że łączna liczba obrotów pętli while to suma spadków wartości k, która jest ograniczona przez sumę wzrostów k, czyli wynosi co najwyżej n. W takim razie cała procedura musi mieć złożoność O(n). ### Zad. 9 ![](https://i.imgur.com/jZPaNrO.png) ![](https://i.imgur.com/Np8RViB.png) ### Zad. 10 ![](https://i.imgur.com/Zh7OKEs.png) Mamy dany zbiór $P$ $n$ różnych elementów $p$, gdzie $p_i$ ma wagę $w_i$ i wartość $v_i$, oraz plecak $K(W)$, gdzie $W$ to pojemność plecaka. Szukamy takiego zestawu przedmiotów z $P$, że mieszczą się w plecaku i mają łącznie największą wartość. Każdy przedmiot możemy umieścić w plecaku tylko raz. Zauważamy, że mając plecak o zerowej pojemności nie możemy dodać do niego więcej przedmiotów. Mając plecak o pojemności $W>0$ i chcąc dodać do niego przedmiot $p_i$ możemy wrzucić go do plecaka i wtedy wartość plecaka jest sumą wartości plecaka o pojemności $W-w_i$ oraz wartości $v_i$, tzn. $K(W) = K(W-w_i)+v_i$. Możemy też nie wrzucać go do plecaka i wtedy wartość plecaka jest równa wartości plecaka, do któego nie będziemy już próowali wrzucić przedmiotu $p_{i}$, czyli ograniczamy rozmiar zbioru przedmiotów do $i-1$. Interesuje nas maksymalna wartość plecaka, czyli większa z tych wartości. Możemy to zapisać jako zależność rekurencyjną: $K(W,i) = 0$, jeśli $W=0$ lub $i=0$, $K(W, i) = max(K(W-w_i, i-1) + v_i, K(W, i-1))$, w p.p. Algorytm zachłanny polegałby na zbudowaniu tablicy indeksowanej po pojemnościach plecaka, tzn. od 0 do W, stosując powyższą zależność rekurencyjną. W ukończonej tablicy znajdziemy odpowiedź na pytanie o największą wartość plecaka. Możemy również zapamiętywać indeksy elementów wybieranych przez algorytm do plecaka, czyli poznamy też jego zawartość. ```python= v[n] // wektor wartości w[n] // wektor wag K[n][W] // tablica for i = 0 to n: for w = 0 to W: if i == 0 || w == 0: K[i][w] = 0 else: K[i][w] = max(K[i-1][W-w] + v[i], K[i-1][W]) return K[n][W] ``` Zauważamy, że algorytm można zoptymalizować, pamiętając jedynie dwa wiersze lub dwie kolumny tej tablicy -- pozostałe nie są potrzebne. Złożoność algorytmu to $O(Wn)$, czyli jest pseudowielomianowa, bo zależy wielomianowo od **wartości danych** -- $W$. ### Zad. 11 ![](https://i.imgur.com/2E4qpq9.png) a) Shift-And dla długich wzorców nie jest optymalny, ponieważ operuje na wektorach bitowych o długości zależnej od długości wzorca. Oznacza to, że działa najszybciej dla krótkich wzorców, które mogą być pamiętane, np. na jednym słowie maszynowym. b) użycie go dla wzorców w długich tekstach jest sensowne, ponieważ czas działania tego algorytmu jest liniowy, o ile wzorzec nie jest zbyt długi. Również w przypadku bardzo dużego alfabetu czas preprocessingu może być zbyt duży. ### Zad. 12 ![](https://i.imgur.com/4YkQK1l.png) Rozważmy sytuację, gdzie lewe poddrzewo zawiera maksymalną liczbę wierzchołków, a prawe minimalną. Niech całe drzewo o korzeniu $v$ ma wysokość $h$. **Lewe** Jest ono wysokości $h-1$. Oczywiście liczba wierzchołków w tym poddrzewie jest nie większa niż $2^h-1$. **Prawe** Jest wysokości $h-1$. Zauważmy, że jest to drzewo minimalne, tzn. jego prawe i lewe poddrzewo również są minimalnymi drzewami AVL. Oznacza to, że liczba wierzchołków w tym poddrzewie $n(h-1) = 1+ n(h-2) + n(h-3)$, gdzie $n(0) = 1$, $n(1)=2$. n(2) = 1 + 2 + 1 = 4 n(3) = 1 + 2 + 4 = 7 n(4) = 1 + 4 + 7 = 12 Indukcyjnie załóżmy, że $n(k)=F(k+2)-1$ dla $k < i$. Wtedy: $n(i) = 1 + n(i-1) + n(i-2) = 1 + F(i+1) - 1 + F(i) - 1$ $n(i) = F(i+2) - 1$, czyli zależność jest spełniona. Stąd maksymalna różnica między liczbą wierzchołków w lewym i prawym poddrzewie $v$ wynosi $2^{h}-1 - n(h-1) = 2^{h}-1 - F(h+1)-1$. Stąd różnica tych wysokości może być $\Omega(n)$. ### Zad. 13 ![](https://i.imgur.com/XJZpHyz.png) Usuwany jest korzeń drzewa dwumianowego, na który wskazuje wskaźnik $min$ w kopcu $H$. Następnie wszystkie poddrzewa tego korzenia są dodawane na listę drzew nowego kopca $H'$. Uruchamiana jest procedura $meld(H, H')$, która łączy oba kopce. W jej wyniku otrzymamy kopiec $H$ zawierajacy co najwyżej log(n) różnych drzew dwumianowych, gdzie n to liczba wierzchołków w kopcu. Koszt tej operacji to potencjalnie O(n), jednak jej złożoność zamortyzowana to jedynie O(logn) -- rozpatrując niezmiennik kredytowy, w którym korzeń każdego drzewa w kopcu zawiera jedną jednostkę kredytową, wtedy operacja deletemin wymaga jedynie 2logn jednostek, by zachować ten niezmiennik. ### Zad. 14 ![](https://i.imgur.com/0NOerCp.png) Poznaliśmy na zajęciach wielomianowy algorytm, rozwiązujący ten problem, czyli jest on klasy P, zatem także klasy NP. Nie jest ani NP-zupełny, ani NP-trudny, o ile P i NP są różne. ### Zad. 15 ![](https://i.imgur.com/3pnF8Yz.png) $4n$ na samą tablicę oraz dodatkowe $3n+3$ na zapamiętanie parametrów funkcji haszującej na pierwszym poziomie oraz funkcji haszujących dla wszystkich podtablic. ### Zad. 16 ![](https://i.imgur.com/BHtrA8Y.png) Mamy spełnione założenie, że prawdopodobieństwo kolizji to $1/m$, gdzie $m$ to rozmiar tablicy. Stąd oczekiwana liczba kolizji to: $$ \sum_{i=0}^{n-1}i\cdot \frac{1}{m} $$ W naszym przypadku $n=100$, $m=1000$, czyli: $$ \sum_{i=0}^{99}i\cdot \frac{1}{1000} = \frac{1}{1000}\sum_{i=0}^{99}i\\ \frac{1}{1000} \frac{99-0}{2}99=\frac{99\cdot 99}{1000} \approx 4.9 $$ ### Zad. 17 ![](https://i.imgur.com/gNtztTu.png) Wartość tego elementu to $(\omega_n^i)^j$, gdzie $\omega$ to n-ty pierwiastek zespolony z jedności. Składowe wektora y są wartościami wielomianu o współczynnikach z wektora a w punktach z macierzy A. ### Zad. 18 ![](https://i.imgur.com/y6bOCFm.png) Pamiętamy, że dla zbalansowanego Uniona drzewa rzędu r mają co najmniej $2^r$ wierzchołków. r jest utożsamiany z wysokością drzewa utworzonego po ciągu samych operacji union (bez find), zatem jest to nasza sytuacja. Oznacza to, że dla n-elementowego drzewa jego wysokość może być co najwyżej logn. Udowodnijmy to indukcyjnie po rzędach: **Podstawa** Dla r = 0 mamy drzewo jednoelementowe, czyli ma ono 2^0=1 wierzchołek, dla r=1 mamy drzewo dwuwierzchołkowe, zatem 2^1=2. **Krok** Załóżmy, że działa dla $k<r$. Pokażmy, że wtedy działa dla $r+1$. Weźmy t -- minimalne drzewo o rzędzie r+1. By utworzyć drzewo o rzędzie r+1 potrzebujemy dwóch drzew o mniejszym rzędzie -- t1 i t2. Załóżmy, że t1 jest większe od t2. Skoro podpinamy mniejsze pod większe i otrzymaliśmy drzewo wysokości r+1, to t1 i t2 musiały być co najmniej rzędu r. Zauważmy też, że t1 nie mogło być większego rzędu niż r, bo inaczej to ono byłoby minimalnym drzewem o rzędzie r+1. Również t2 nie mogło być większego rzędu niż r, inaczej to t1 byłoby podwieszone pod t2. Zatem oba muszą być rzędu r. Zatem liczba wierzchołków w t to: $\nu(t) = \nu(t1)+\nu(t2)$, ale rząd t1 i t2 to r, czyli z zał. indukcyjnego mamy $\nu(t) \geq 2^r + 2^r = 2^{r+1}$ Zatem drzewo o rzędzie $r$ ma co najmniej $2^r$ wierzchołków, więc wysokość takiego drzewa nie może być większa niż $O(logn)$. ### Zad. 19 ![](https://i.imgur.com/YsvhxNU.png) W tej strukturze operacje insert oraz successor są rekurencyjne. Pamiętanie tych dwóch wartości dla każdej podstruktury sprawia, że możemy uniknąć niepotrzebnych wywołań rekurencyjnych, co zmniejsza złożoność obu tych operacji do $O(log\ log\ n)$. ### Zad. 20 ![](https://i.imgur.com/qwDohNx.png) ![](https://i.imgur.com/tKopEC9.png)