###### tags: `ilo`, `map` # Notatki - grupa zaawansowana *MAP 2022* ## 19.02.2022 ### Drzewo punkt-przedział Drzewo sum z komentarzami ```cpp= #include <iostream> using namespace std; // Potęga dwójki, która jest większa niż "n" w zadaniu // 2^17 > 10^5, 2^20 > 10^6 const int TWO_EXP = 17; const int BASE = 1<<TWO_EXP; const int TREE_SIZE = BASE<<1; int tree[TREE_SIZE]; void update(int x, int c) { // Przejdź do odpowiedniego indeksu int xx = BASE + x; // Wstaw element c na pozycji x tree[xx] = c; // Przejdź do rodzica xx>>=1; // Wykonuj dopóki nie wyjdziesz poza drzewo // (Korzeń ma numer 1) while (xx > 0) { // Zaktualizuj wartość w wierzchołku // Wynik w wierzchołku to suma dzieci tree[xx] = tree[xx<<1] + tree[(xx<<1) + 1]; // Przejdź do rodzica xx>>=1; } } // SPOSÓB 1. WYBIERAJ PRZEDZIAŁY BAZOWE OD GÓRY // Zacznij od korzenia, który pokrywa cały przedział int query(int a, int b, int v=1, int l=0, int r=BASE-1) { // Sprawdź czy wierzchołek obsługuje przedział i zwróć dla niego wynik // Obsługiwany przedział jest poza szukanym if(r<a || l>b || r < l) return 0; // Obsługiwany przedział zawiera się cały w szukanym // Znaleźliśmy wierzchołek bazowy - zwróć wynik if(l>=a && r<=b) return tree[v]; // Obsługiwany przedział jest za duży. Zleć zapytanie dzieciom int left = query(a, b, v<<1, l, (l+r)>>1); int right = query(a, b, (v<<1)+1, ((l+r)>>1)+1, r); return left + right; } // SPOSÓB 2. WYBIERAJ PRZEDZIAŁY BAZOWE OD DOŁU int query2(int a, int b) { int xa = BASE + a, xb = BASE + b; int suma = 0; // Dopóki lewa strzałka jest lewa i prawa jest prawa while(xa <= xb) { // Wierzchołek, który jest prawym dzieckiem na lewej ścieżce, // jest wierzchołkiem bazowym. Weź jego wynik i przesuń wskaźnik w prawo if (xa % 2 == 1) { suma += tree[xa]; xa += 1; } // Symetrycznie dla wierzchołka na prawej ścieżce if (xb % 2 == 0) { suma += tree[xb]; xb -= 1; } // Przejdź do rodziców xa >>= 1; xb >>= 1; } return suma; } // SPOSÓB 3. WYBIERAJ POD PRZEDZIAŁAMI BAZOWYMI int query3(int a, int b) { int xa = BASE + a, xb = BASE + b; int suma = 0; // Dodaj wyniki w liściach. Uważaj, by nie dodać dwa razy suma += tree[xa]; if(xa != xb) suma += tree[xb]; // Wykonuj dopóki lewy i prawy nie mają wspólnego rodzica while ((xa>>1) != (xb>>1)){ // Sprawdź czy należy wziąć brata do wyniku. // W przypadku lewej ścieżki: znaleziono lewego syna na lewej ścieżce if (xa % 2 == 0) suma += tree[xa + 1]; // Symetrycznie dla prawej ścieżki if (xb % 2 == 1) suma += tree[xb - 1]; // Przejdź do rodziców xa >>= 1; xb >>= 1; } return suma; } int main(){ ios_base::sync_with_stdio(false); cin.tie(0); int n, o, a, b; cin>>n; while(n--){ cin>>o>>a>>b; if(o==1) update(b, a); else if(o==0){ cout<<query3(a, b)<<'\n'; } } return 0; } ``` ### Drzwo przedział-punkt Update analogicznie do query z drzewa punkt-przedział. Query analogicznie do update z drzewa punkt-przedział. ```cpp= #include <iostream> using namespace std; const int TWO_EXP = 17; const int BASE = 1 << TWO_EXP; const int TREE_SIZE = BASE << 1; int tree[TREE_SIZE]; int query(int x) { int suma = 0; int xx = BASE + x; while (xx > 0) { suma += tree[xx]; xx >>= 1; } return suma; } // SPOSÓB 1. PRZEDZIAŁY BAZOWE OD GÓRY void update(int a, int b, int c, int v = 1, int l = 0, int r = BASE - 1) { if (r < a || l > b || r < l) return; if (l >= a && r <= b){ tree[v] += c; return; } update(a, b, c, v << 1, l, (l + r) >> 1); update(a, b, c, (v << 1) + 1, ((l + r) >> 1) + 1, r); } // SPOSÓB 2. PRZEDZIAŁY BAZOWE OD DOŁU void update2(int a, int b, int c) { int xa = BASE + a, xb = BASE + b; while (xa <= xb) { if (xa % 2 == 1) { tree[xa] += c; xa += 1; } if (xb % 2 == 0) { tree[xb] += c; xb -= 1; } xa >>= 1; xb >>= 1; } } // SPOSÓB 3. OD DOŁU INACZEJ void update3(int a, int b, int c) { int xa = BASE + a, xb = BASE + b; tree[xa] += c; if (xa != xb) tree[xb] += c; while ((xa >> 1) != (xb >> 1)) { if (xa % 2 == 0) tree[xa + 1] += c; if (xb % 2 == 1) tree[xb - 1] += c; xa >>= 1; xb >>= 1; } } int main() { ios_base::sync_with_stdio(false); cin.tie(0); int n, o, a, b, c; cin >> n; while (n--) { cin >> o; if (o == 1){ cin>>a>>b>>c; update3(a, b, c); } else if (o == 0) { cin>>a; cout << query(a) << '\n'; } } return 0; } ``` ### Drzewo przedział-przedział Dla zadania: [Drzewo przedziałowe](https://themis.ii.uni.wroc.pl/MAP1LO22_Z/ITREE) ```cpp= #include <iostream> using namespace std; const int TWO_EXP = 20; const int BASE = 1 << TWO_EXP; const int TREE_SIZE = BASE << 1; int tree[TREE_SIZE]; int lazy[TREE_SIZE]; void update_lazy(int l, int r, int v) { // Wartość z lazy w wierzchołku v dodaj do lewego i prawego dziecka, // a następnie zepchnij lazy v do lazy dzieci i wyzeruj lazy v tree[l] = max(tree[l] + lazy[v], 0); tree[r] = max(tree[r] + lazy[v], 0); lazy[l] += lazy[v]; lazy[r] += lazy[v]; lazy[v] = 0; } void update( int a, int b, int value, int v=1, int l=0, int r=BASE-1 ) { // Jeśli wierzchołek nie mieści się w pytanym przedziale, anuluj if(r<a || l>b || r<l) return; // Jeśli obsługiwany przedział cały mieści się w zapytaniu, // to zaktualizuj wartość w wierzchołku i dodaj do lazy if(l >= a && r <= b){ tree[v] += value; tree[v] = max(tree[v], 0); lazy[v] += value; return; } int vl = v<<1; int vr = vl + 1; int mid = (l + r)>>1; // zaktualizuj lazy update_lazy(vl, vr, v); // Zleć zapytanie do dzieci update(a, b, value, vl, l, mid); update(a, b, value, vr, mid + 1, r); // Zaktualizuj wartość w v tree[v] = max(tree[vl], tree[vr]); } int query(int a, int b, int v=1, int l=0, int r=BASE-1) { if(r < a || l > b) return 0; if(l >= a && r <= b) return tree[v]; int vl = v<<1; int vr = vl+1; int mid = (l+r)>>1; update_lazy(vl, vr, v); return max( query(a, b, vl, l, mid), query(a, b, vr, mid + 1, r) ); } int main() { ios_base::sync_with_stdio(false); cin.tie(0); int n, o, a, b, c; cin >> n; while (n--) { cin >> o; if (o == 1) { cin >> a >> b; update(a, b, 1); } else if (o == 2) { cin >> a >> b; update(a, b, -1); } else if (o == 0) { cout<<query(0, BASE-1)<<'\n'; } } return 0; } ``` --- *MAP 2021* ## 12.06.2021 ### Drzewa <!-- :::danger --> **Definicja 1.** Graf posiada **cykl** jeżeli istnieją jakieś dwa wierzchołki $u$, $v$ takie, że z $u$ do $v$ można przejść na więcej niż jeden sposób (nie cofając się po krawędziach). <!-- ::: --> <!-- :::info --> Intuicyjnie, cykl to po prostu "pętla" wierzchołków w grafie. <!-- ::: --> <!-- :::danger --> **Definicja 2. Drzewo** to spójny graf acykliczny (nie posiadający cyklu). <!-- ::: --> <!-- :::info --> ```graphviz graph G { // rankdir=LR // rank = same // layout=dot subgraph clusterA{ pencolor=white node [shape=circle]; 1 -- 2; 1 -- 3; 3 -- 4; 3 -- 5; labelloc=b; label="Przykład drzewa"; } subgraph clusterB { pencolor=white node [shape=circle]; 6 [label="1"]; labelloc=b; label="Drzewo jednowierzchołkowe" } subgraph clusterC { pencolor=white; node[shape=circle]; 7 [label="1"] 8 [label="2"] 9 [label="3"] 7 -- {8, 9} {rank=same; 8 -- 9} labelloc=b label="To nie jest drzewo" } label="Rysunek 1. Przykłady drzew." } ``` <!-- ::: --> --- <!-- :::info --> Drzewa często pojawiały się już na zajęciach pod postacią drzewa wywołań rekurencyjnych (np. w zadaniu _Hetmany_), czy trochę bardziej ukryte drzewo "zaznaczane" przez algorytm **DFS**: ```graphviz graph G { node [shape=circle] 1 -- 2 [color="red", style="bold"] 2 -- 3 [color="red", style="bold"] 2 -- 4 [color="red", style="bold"] {rank = "same"; 3 -- 4 [style="dashed"; color="blue"]} 1 -- 5 [color="red", style="bold"] 5 -- 6 [color="red", style="bold"] 6 -- 7 [color="red", style="bold"] 6 -- 8 [color="red", style="bold"] 6 -- 9 [color="red", style="bold"] 5 -- 9 [style="dashed", color="blue"] 1 -- 7 [style="dashed", color="blue"] label="Rysunek 2. Przykład przejścia algorytmu DFS po drzewie.\nCzerwone krawędzie to te, po których przejdziemy w algorytmie DFS,\n niebieskie to te, po których algorytm nie przejdzie. Czerwone krawędzie\nwyznaczają drzewo." } ``` **Definicja 3. Liściem** w drzewie nazywamy każdy wierzchołek stopnia $1$. Bardziej matematycznie: wierzchołek $v$ jest **liściem** $\iff \deg{v} = 1$. **Twierdzenie 4.** Liczba krawędzi $N$-wierzchołkowego drzewa wynosi dokładnie $N - 1$. **Dowód:** Dowód przeprowadzimy przy pomocy indukcji matematycznej, ważnej metody w dowodzeniu wielu twierdzeń, nie tylko dotyczących drzew. Pokażemy najpierw, że teza jest prawdziwa dla $N = 1$. W zasadzie, to nie mamy czego pokazywać - takie drzewo jest tylko jedno, nie ma w ogóle krawędzi, zatem teza jest prawdziwa, gdyż $N - 1 = 1 - 1 = 0$. Załóżmy teraz, że teza jest prawdziwa dla wszystkich drzew $N$-wierzchołkowych. Pokażemy, że każde $N+1$ wierzchołkowe drzewo ma w takim razie dokładnie $N$ krawędzi. Wybierzmy dowolne $N+1$ wierzchołkowe drzewo. **Każde drzewo ma co najmniej jednego liścia (dlaczego?)**. Wyobraźmy sobie teraz, że wyrzucamy tego liścia z naszego drzewa. Dostajemy w takim razie drzewo, które ma $N$ wierzchołków. Założyliśmy, że teza zachodzi dla wszystkich $N$ wierzchołkowych drzew, zatem nasze drzewo ma dokładnie $N-1$ krawędzi. Przypomnijmy sobie teraz o wyrzuconym przez nas liściu i dołóżmy go z powrotem do naszego drzewa. Skoro to był liść, to znaczy, że wraz z nim dołożyliśmy dokładnie jedną krawędź. W takim razie krawędzi jest teraz $N - 1 + 1 = N$, co potwierdza naszą tezę. <!-- $$\tag*{$\blacksquare$}$$ --> <a name="rys3"></a> ```graphviz graph G { node [shape="circle"] 1--2 1--3 3--4 3--5 3--6 [style="dashed", color="red"] 2--7 6 [color="red"] label="Rysunek 3. Przykładowy rysunek obrazujący dowód dla pewnego 7-wierzchołkowego grafu.\n W \"czarnym\" grafie, z założenia mamy dokładnie 5 krawędzi. Po dołożeniu czerwonego\n wierzchołka i czerwonej krawędzi wracamy do pierwotnego grafu, dla którego teza\nrównież zachodzi." } ``` Pokazaliśmy w tym momencie dowodzie rzeczy: - teza zachodzi dla $N = 1$, - jeżeli teza zachodzi dla pewnego $N$, to zachodzi dla $N + 1$. To wystarczy, żeby udowodnić prawdziwość tezy dla dowolnego $N\in\mathbb{N}$. Skoro teza zachodzi dla $N = 1$, to z drugiego punktu zachodzi dla $N = 2$. Ale w takim razie zachodzi też dla $N = 3$ (znowu korzystając z drugiego punktu), więc zachodzi też dla $N = 4$, itd. \begin{align} ''1 \Rightarrow 2 \Rightarrow 3 \Rightarrow 4 \Rightarrow 5 \Rightarrow \ldots'' \end{align} Indukcja po rozmiarze drzewa jest bardzo częstą i użyteczną metodą dowodzenia własności drzew. Będzie się jeszcze pojawiać w przyszłości, dlatego zachęcam do próby dokładnego zrozumienia powyższego dowodu. <!-- ::: --> --- Bardzo często i wygodnie będzie nam wyróżniać jeden wierzchołek drzewa. Taki wyróżniony wierzchołek nazywamy **korzeniem** drzewa. Najczęściej drzewa rysujemy z korzeniem u szczytu i z wierzchołkami spadającymi w dół. #### Od tego momentu zakładamy, że każde roważane przez nas drzewo jest ukorzenione. **Fakt 4.** W drzewie, z każdego wierzchołka do każdego innego istnieje dokładnie jedna ścieżka. To twierdzenie również można udowodnić indukcyjnie. Zachęcam Was do samodzielnego spróbowania. **Definicja 5.** **Ojcem** wierzchołka $v$ w ukorzenionym drzewie jest pierwszy wierzchołek "nad" nim na ścieżce od $v$ do korzenia. Analogicznie definiujemy kolejne pojęcie: **synem** wierzchołka $v$ są wszystkie wierzchołki, dla których jest on ojcem. Na [rysunku 3](#rys3) ojcem wierzchołka $3$ jest wierzchołek $1$, a jego synami są wierzchołki $4, 5, 6$. **Definicja 6.** Wierzchołek $u$ jest **Przodkiem** wierzchołka $v$, jeżeli $u$ jest ojcem $v$ lub $u$ jest przodkiem ojca $v$ (czyli ojcem ojca ojca... ojca $v$). Mówimy, że $v$ jest **potomkiem** $u$ kiedy $u$ jest potomkiem $v$. Zauważ, że przodkowie wierzchołka to wszystkie wierzchołki leżące na ścieżce od tego wierzchołka do korzenia. **Definicja 7.** W ukorzenionym dzrzewie, **poddrzewem** wierzchołka $v$ nazwiemy graf złożony z wierzchołka $v$ i wszystkich jego potomków. ```graphviz graph G { node [shape="circle"] 1 -- 2 2 -- 5 5 -- 6 [color="red"] 5 -- 7 [color="red"] 1 -- 8 8 -- 9 2 -- 10 5 -- 11 [color="red"] 7 -- 3 [color="red"] 7 -- 4 [color="red"] 9 -- 12 5 [color="red"] 6 [color="red"] 7 [color="red"] 11 [color="red"] 4 [color="red"] 3 [color="red"] label="Rysunek 4. Czerwonym kolorem zostało oznaczone poddrzewo wierzchołka 5." } ``` ## 29.05.2021 ### Przeszukiwanie w głąb (DFS) ```cpp= void DFS(int v) { // Zaznaczamy, że wierzchołek został odwiedzony odw[v]=true; // Przeszukujemy jego sąsiadów for(int i=0; i<G[v].size(); i++) { int sasiad = G[v][i]; // Jeśli sąsiad nieodwiedzony, to odwiedzamy if(!odw[sasiad]) { DFS(sasiad); } } // i tyle :) } ``` ### Tablica https://miro.com/app/board/o9J_lBtFzrk=/ <iframe width="768" height="432" src="https://miro.com/app/live-embed/o9J_lBtFzrk=/?moveToViewport=-770,-383,3395,1664" frameBorder="0" scrolling="no" allowFullScreen></iframe> ## 15.05.2021 ### Reprezentacja grafu #### Lista sąsiedztwa ```cpp= //n=5 vector<int>G[5]; // 1 → 2 G[1].push_back(2); // 2 → 3 G[2].push_back(3); G[2].push_back(4); G[4].push_back(2); /* Lista sąsiedztwa: * 1 : 2 * 2 : 3 4 * 3 : * 4 : 2 * */ //Wypisujemy sąsiadów 2ki for(int i=0; i<G[2].size(); i++){ cout<<G[2][i]<<" "; } ``` ### Stopień wierzchołka Liczba sąsiadów tego wierzchołka. W macierzy sąsiedztwa trzeba przejrzeć cały wiersz, by obliczyć stopień, w liście sąsiedztwa natomiast wystarczy sprawdzić długość listy. ```cpp= //stopień wierzchołka 2ego G[2].size(); ``` ### Przeszukiwanie wszerz (BFS) Odwiedzamy wierzchołki w kolejności ich odległości od wierzchołka stratowego. ```cpp= // nax 100 wierzchołków vector <int> G[101]; //graf w reprezentacji listy sąsiedztwa bool odw[101]; //tablica odwiedzonych (wyzerowana), nikt nie odwiedzony int odl[101]; //tablica odległości queue<int>K; //co następne do przejrzenia //zacznijmy od wierzchołka o numerze 0 K.push(0); odw[0] = true; //odwiedzę wierzchołek 0 //przeglądamy, tak długo jak jest coś do przejrzenia while(!K.empty()){ int v = K.front(); //wierzchołek z początku kolejki K.pop(); //jak go zapisałem to go wyrzucam //pętla przeglądająca wszystkich sąsiadów wierzchołka v for(int i=0; i<G[v].size(); i++){ int sasiad = G[v][i]; if(!odw[sasiad]){ K.push(sasiad); odl[sasiad] = odl[v] + 1; odw[sasiad] = true; } } } ``` ### Tablica https://miro.com/app/board/o9J_lET9ebM=/ <iframe width="768" height="432" src="https://miro.com/app/live-embed/o9J_lET9ebM=/?moveToViewport=-1715,1749,1535,749" frameBorder="0" scrolling="no" allowFullScreen></iframe> ## 07.05.2021 ### Graf #### Pojęcia - __wierzchołki__ - __krawędzie__, drogi, połączenie - krawędzie mogą być __skierowane__ → (jednokierunkowe) lub __nieskierowane__ - (dwukierunkowe) - wierzchołki do których możemy dotrzeć (zwykle bezpośrednio) z jakiegoś wierzchołka nazywamy jego __sąsiadami__ ### Reprezentacja grafu #### Macierz sąsiedztwa W tablicy zaznaczamy, które wierzchołki (wiersze) są połączone z innymi (kolumny). --- ### Przykład implementacji vectora ```cpp= ... #include <vector> vector< int > w; int main(){ w.size(); // 0 w.push_back(3); // [3] w.size(); // 1 w.push_back(4); // [3, 4] w.size(); // 2 w[0] = 17; // [17, 4] w[5] = 9; // ŹLE! BRAK TAKIEGO INDEKSU for(int i=0; i<w.size(); i++) { cout<<w[i]<<" "; } return 0; } ``` ### Zadanie Napisz program, który zapisuje do vectora liczby od 1 do n. ```cpp= #include <iostream> #include <vector> using namespace std; vector<int> wek; int main(){ int n; cin>>n; for(int i=1; i<=n;i++) { wek.push_back(i); } return 0; ``` ### Tablica https://miro.com/app/board/o9J_lF40y20=/ <iframe width="768" height="432" src="https://miro.com/app/live-embed/o9J_lF40y20=/?moveToViewport=-824,2959,1092,533" frameBorder="0" scrolling="no" allowFullScreen></iframe>