This shows you the differences between two versions of the page.
pa:laboratoare:laborator-10 [2022/03/01 23:57] darius.neatu created |
pa:laboratoare:laborator-10 [2025/04/27 19:52] (current) darius.neatu [Kruskal] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Laborator 10: Drumuri minime (2/2) ====== | + | ====== Laborator 10: Arbori minimi de acoperire ====== |
- | TODO Gigel | + | |
+ | {{:pa:new_pa:partners:bitdefender-logo.png?190 |}} Bitdefender provides cybersecurity solutions with leading security efficacy, performance and ease of use to small and medium businesses, mid-market enterprises and consumers. Guided by a vision to be the world’s most trusted cybersecurity solutions provider, Bitdefender is committed to defending organizations and individuals around the globe against cyberattacks to transform and improve their digital experience. | ||
+ | |||
+ | ===== Obiective laborator ===== | ||
+ | |||
+ | În acest laborator vom introduce contextul pentru **minimum spanning tree problem** și vom studia algoritmi care pot rezolva această problemă. | ||
+ | |||
+ | * Prezentarea problemei (diverse variante). | ||
+ | * Prezentarea algoritmilor pentru calcul a unui arbore minim de acoperire. | ||
+ | |||
+ | ===== Importanţă – aplicaţii practice ===== | ||
+ | |||
+ | Algoritmii pentru determinarea unor arbori (minimi) de acoperire au multiple aplicații practice. Câteva exemple de aplicații sunt: | ||
+ | |||
+ | * Designul circuitelor electronice: | ||
+ | * Maparea pinilor pe un circuit astfel încât să se foloseacă o lungime minimă de traseu. | ||
+ | * Rețele de calculatoare: | ||
+ | * Interconectarea mai multor stații în rețea, cu un cost / latență redus(ă). | ||
+ | * STP Protocol: protocol care previne apariția buclelor într-un LAN. | ||
+ | * Segmentarea imaginilor: | ||
+ | * Împărțirea unei imagini în regiuni cu proprietăți asemănătoare. | ||
+ | * [[https://en.wikipedia.org/wiki/Minimum_spanning_tree-based_segmentation | Minimum spanning tree-based segmentation]]. | ||
+ | * Clustering: | ||
+ | * [[https://en.wikipedia.org/wiki/Single-linkage_clustering|Single-linkage clustering]]. | ||
+ | |||
+ | ===== Minimum spanning tree problem ===== | ||
+ | |||
+ | Puteți consulta capitolul **Minimum Spanning Trees** din **Introduction to Algorithms** [0] pentru mai multe definiții formale. Această secțiune sumarizează principalele notații folosite în laboratoarele de PA. | ||
+ | |||
+ | Cele mai uzuale notații din laboratoarele de grafuri sunt descrise în [[https://ocw.cs.pub.ro/courses/pa/skel_graph|Precizări laboratoare 07-12]] (ex. $n$, $m$, $adj$, $adj\_trans$, $(x, y)$, etc). | ||
+ | |||
+ | Vom adăuga alte notații în continuarea celor introduse anterior. | ||
+ | |||
+ | |||
+ | |||
+ | > **Arbore de acoperire** / **spanning tree**: Într-un graf neorientat **conex** $G = (V, E)$, cu funcția de cost $w: E -> W$, numim **un arbore de acoperire** un subgraf al lui **G** cu număr minim de muchii și care interconectează toate nodurile din graf. | ||
+ | |||
+ | <spoiler Exemplu> | ||
+ | |||
+ | {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab11-spanning-tree-example.png?800| Arbore de acoperire }} | ||
+ | |||
+ | În exemplul atașat, avem un graf **neorientat** și **conex** cu următoare configurație: | ||
+ | |||
+ | * ''%%n = 5%%'', ''%%m = 5%%'' | ||
+ | * Pentru acest exemplu, avem **3** arbori de acoperire evidențiați în aceeași figură (rândul 2). | ||
+ | |||
+ | Observație: Un graf **conex** are cel puțin un arbore de acoperire! | ||
+ | |||
+ | </spoiler> \\ | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | > **Pădure de arbori de acoperire** / **spanning tree forest**: Se definește similar, dar pentru graf neorientat **neconex**. Fiecare componentă conexă are un **arbore de acoperire** / **spanning tree**. Spunem că acești arbori formează împreună o **pădure** (**forest**). | ||
+ | |||
+ | <spoiler Exemplu> | ||
+ | |||
+ | {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab11-spanning-forest-example.png?800| Pădure de arbori de acoperire }} | ||
+ | |||
+ | În exemplul atașat, avem un graf **neorientat** și **neconex** cu următoare configurație: | ||
+ | |||
+ | * ''%%n = 6%%'', ''%%m = 6%%'' | ||
+ | * În figura următoare evidențăm toate pădurile de arbori de acoperire - 9 pe acest exemplu. | ||
+ | * Putem elimina în 3 moduri câte o muchie din prima componentă conexă - deci 3 arbori posibili. | ||
+ | * Putem elimina în 3 moduri câte o muchie din a doua componentă conexă - deci 3 arbori posibili. | ||
+ | * Numărul de păduri este $3 * 3 =9$ (combinăm oricare 2 arbori corespunzători componentelor conexe diferite). | ||
+ | |||
+ | Observație: Un graf **neconex** are cel puțin o pădure de arbori de acoperire! | ||
+ | |||
+ | </spoiler> \\ | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | > **Arbore minim de acoperire** (**AMA**)/ **minimum spanning tree** (**MST**): Un arbore de acoperire este **ȘI** arbore minim de acoperire dacă costul total al muchiilor din arbore este minim posibil. Analog se poate defini și noțiunea de **pădure de arbori minimi de acoperire** (pornind de la cea de pădure de arbori de acoperire). | ||
+ | |||
+ | <spoiler Exemplu> | ||
+ | |||
+ | {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab11-minimum-spanning-tree-example.png?900| Exemplu funcție de cost pentru graf orientat}} | ||
+ | |||
+ | În exemplul atașat, avem un graf **neorientat** și **conex** cu următoare configurație: | ||
+ | |||
+ | * ''%%n = 5%%'', ''%%m = 5%%'' | ||
+ | * În figura următoare evidențăm toți arborii de acoperire. Doar 2 dintre aceștia au cost minim **5**. | ||
+ | |||
+ | Observație: Un graf **conex** are cel puțin un arbore minim de acoperire! | ||
+ | |||
+ | </spoiler> \\ | ||
+ | |||
+ | |||
+ | ===== Algoritmi ===== | ||
+ | |||
+ | Pentru **minimum spanning tree problem** există mai mulți algoritmi, dintre care amintim: | ||
+ | |||
+ | * **Kruskal**: abordare greedy care se bazează pe sortarea muchiile după cost. Mai multe detalii în **Introduction to Algorithms** [0]. | ||
+ | * **Prim**: abordare greedy care extinde arborele căutat pornind de la o rădăcină (nod ales oarecare). Mai multe detalii în **Introduction to Algorithms** [0]. | ||
+ | * **Karger, Klein & Tarjan**: algoritm randomizat cu complexitate liniară (mai bună decât Kruskal și Prim). Mai multe detalii în **A Randomized Linear-Time Algorithm to Find Minimum Spanning Trees** [1]. | ||
+ | * **Bernard Chazelle**: algoritm determinist cu o complexitate liniară (cu complexitate mai bună decât soluția anterioară). Mai multe detalii în **A Minimum Spanning Tree Algorithm with Inverse Ackermann Type Complexity** [2]. | ||
+ | |||
+ | În acest laborator vom studia și vom implementa **algoritmul lui Kruskal**. De asemenea, oferim ca studiu de caz pentru acasă, un material pentru algoritmul lui Prim. | ||
+ | |||
+ | ===== Kruskal ===== | ||
+ | |||
+ | Algoritmul lui [[https://en.wikipedia.org/wiki/Joseph_Kruskal|Joseph Bernard **Kruskal**]] (**Kruskal’s algorithm**) rezolvă **minimum spanning tree problem** în grafuri neorientate **G = (V, E)** cu costurile muchiilor **oarecare** aplicând o strategie greedy foarte simplă: muchiile de cost minim probabil fac parte din MST! | ||
+ | |||
+ | ==== Kruskal - Pseudocod ==== | ||
+ | |||
+ | |||
+ | <note> | ||
+ | |||
+ | Algoritmul lui Kruskal folosește o structură de date care suportă 2 operații: | ||
+ | |||
+ | * În ce mulțime este elementul **x**? | ||
+ | * Să se reunească mulțimile din care fac parte elementele **x** și **y**. | ||
+ | |||
+ | Se pot folosi mai multe structuri de date. Pentru o implementare eficientă, alegem **DisjointSet**. Vă recomandăm să parcurgeți [[https://infoarena.ro/problema/disjoint | DisjointSet]]. **ATENȚIE!** Scheletul de laborator oferă o astfel de implementare. | ||
+ | |||
+ | |||
+ | </note> | ||
+ | |||
+ | <code cpp> | ||
+ | // apply Kruskal's algorithm for computing a Minimum Spanning Tree (MST). | ||
+ | // | ||
+ | // nodes = list of all nodes from G | ||
+ | // edges = the list of all edges in G | ||
+ | // example: edge (node, neigh, weight) | ||
+ | // | ||
+ | // returns: cost, mst | ||
+ | // cost = the cost of the MST | ||
+ | // mst = the actual set of edges in MST | ||
+ | // | ||
+ | Kruskal(G=(nodes, edges)) { | ||
+ | // STEP 0: initialize results | ||
+ | cost = 0; // cost of the built MST | ||
+ | mst = {}; // the actual set of edges (u, v) in the built MST | ||
+ | |||
+ | // STEP 1: initialize disjoint set data structure | ||
+ | // (a tree is created for each node) | ||
+ | disjointset = DisjointSet(nodes); | ||
+ | |||
+ | // STEP 2: | ||
+ | sort(edges, compare=nondecreasing order by weight); | ||
+ | |||
+ | // STEP 3: Add edge by edge to MST if no cycles are created. | ||
+ | foreach ( (u, v, w) in edges ) { | ||
+ | // STEP 3.1: Check if u anv v are in different trees. | ||
+ | if (disjointset.setOf(u) != disjointset.setOf(v)) { | ||
+ | // STEP 3.2: Merge these 2 trees (no cycles created). | ||
+ | disjointset.union(u, v); | ||
+ | |||
+ | // STEP 3.3: Extend MST with the current edge. | ||
+ | cost += w; | ||
+ | mst += { (u, v) }; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | return cost, mst; | ||
+ | } | ||
+ | |||
+ | // Usage example: | ||
+ | cost, mst = Kruskal(G=(nodes, edges)); | ||
+ | // 1. Use cost of MST. | ||
+ | // 2. Use edges of MST (e.g. actually build the tree). | ||
+ | </code> | ||
+ | Algoritmul lui Kruskal pornește cu structura de date Păduri de mulțimi disjuncte inițializată cu n noduri izolate. Muchiile sunt sortate crescător după cost și se încearcă adăugarea lor, rând pe rând la pădurea de arbori. | ||
+ | |||
+ | * Dacă o muchie $(u, v)$ are capetele în 2 arbori diferiți, atunci prin adăugarea acesteia, nu vom crea ciclu, ci vom crea un arbore mai mare (din cei 2 mici reuniți). | ||
+ | * Altfel, nu putem adăuga muchia, deoarece am crea un ciclu. Această muchie se ignoră. | ||
+ | |||
+ | ==== Exemple ==== | ||
+ | |||
+ | === Exemplu Kruskal === | ||
+ | |||
+ | {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab11-kruskal.png?900| Exemplu Kruskal}} | ||
+ | |||
+ | Un **MST** găsit cu algoritmul lui Kruskal este: ${ (1, 2); (2, 4); (2, 3); (3, 5)}$ de cost **5**. | ||
+ | |||
+ | <spoiler Explicație pas cu pas> | ||
+ | |||
+ | În exemplul atașat, avem un graf **neorientat** cu următoare configurație: | ||
+ | |||
+ | * ''%%n = 5%%'', ''%%m = 5%%'' | ||
+ | * Funcția de cost ''%%w%%'' are valorile menționate pe muchii. | ||
+ | * Muchiile sortate după cost sunt: | ||
+ | * $(3, 5)$ de cost $-1$ | ||
+ | * $(2, 4)$ de cost $1$ | ||
+ | * $(2, 3)$ de cost $2$ | ||
+ | * $(2, 5)$ de cost $2$ | ||
+ | * $(1, 2)$ de cost $3$ | ||
+ | * Explicație pas cu pas | ||
+ | * STEP 0: Inițializăm pădurile de mulțimi disjuncte - pornind cu n arbori (noduri izolate). | ||
+ | * STEP 1: Muchia cu costul cel mai mic care e nefolosită este $(3, 5)$ de cost -1. Se adaugă la **MST**. Se reunesc 2 arbori. | ||
+ | * STEP 2: Muchia cu costul cel mai mic care e nefolosită este $(2, 4)$ de cost 1. Se adaugă la **MST**. Se reunesc 2 arbori. | ||
+ | * STEP 3: Muchia cu costul cel mai mic care e nefolosită este $(2, 3)$ de cost 2. Se adaugă la **MST**. Se reunesc 2 arbori. | ||
+ | * Observație: În acest pas am fi putut alege muchia $(2, 5)$ cu același cost. | ||
+ | * STEP 4: | ||
+ | * Muchia $(2, 5)$ nu va fi folosită deoarece are capetele în același arbore. | ||
+ | * Muchia cu costul cel mai mic care e nefolosită este $(1, 2)$ de cost 3. Se adaugă la **MST**. Se reunesc 2 arbori. | ||
+ | * Avem un arbore. STOP. | ||
+ | |||
+ | Observație: În STEP 3 pas am fi putut alege muchia $(2, 5)$ cu același cost. Obțineam un MST care diferea printr-o singură muchie ({${ (1, 2); (2, 5); (2, 3); (3, 5)}}$), dar cu același cost **5**. | ||
+ | |||
+ | </spoiler> \\ | ||
+ | |||
+ | |||
+ | ==== Complexitate ==== | ||
+ | |||
+ | * **complexitate temporală**: $T = O(m * log m)\ sau\ O(|E| * log |E|)$ | ||
+ | * **complexitate spațială** : $S = O(n)$ | ||
+ | |||
+ | <spoiler Detalii (analiză + optimizări)> | ||
+ | |||
+ | * **complexitate temporală**: | ||
+ | * Se sortează cele **m** muchii în ordine crescătoare după cost - $O(m log m)$. | ||
+ | * Se folosesc **n** operații pe **DisjointSet** - $O(n log_{*}n)$, care poate fi aproximat cu $O(n)$ (demonstrație pe pagina **DisjointSet**). | ||
+ | * ATENȚIE! Complexitatea conține funcția logaritm iterat, nu logaritm ([[https://en.wikipedia.org/wiki/Iterated_logarithm | Iterated logarithm]]), care pentru valorile uzuale din problemele de algoritmică poate fi aproximată cu o constantă foarte mică. | ||
+ | * **complexitate spațială** : Se ține o structură de date **DisjointSet** cu maximum **n** noduri. | ||
+ | |||
+ | </spoiler> \\ | ||
+ | |||
+ | |||
+ | ===== [Studiu de caz] Prim ===== | ||
+ | |||
+ | Algoritmul lui [[https://en.wikipedia.org/wiki/Robert_C._Prim|Robert C **Prim**]] (**Prim’s algorithm**) rezolvă **minimum spanning tree problem** în grafuri neorientate **G = (V, E)** cu costurile muchiilor **oarecare** aplicând o strategie constructivă greedy, asemănătoare cu algoritmul lui Dijkstra: merge pe muchiile de cost minim, din aproape în aproape, considerând o sursă inițial aleasă aleator și extinzând MST-ul curent cu muchia de cost minim neadăugată încă la arbore. | ||
+ | |||
+ | ==== Prim Pseudocod ==== | ||
+ | |||
+ | <spoiler Prim - Pseudocod> | ||
+ | |||
+ | <code cpp> | ||
+ | // apply Prim's algorithm for computing a Minimum Spanning Tree (MST). | ||
+ | // | ||
+ | // source = source / starting node for computing / expading MST | ||
+ | // (if not specified, node 1 is defaulted) | ||
+ | // nodes = list of all nodes from G | ||
+ | // adj[node] = the adjacency list of node | ||
+ | // example: adj[node] = {..., (neigh, weight) ...} => edge (node, neigh, weight) | ||
+ | // | ||
+ | // returns: cost, mst | ||
+ | // cost = the cost of the MST | ||
+ | // mst = the actual set of edges in MST | ||
+ | // | ||
+ | Prim(source=1, G=(nodes, adj)) { | ||
+ | // STEP 0: initialize results | ||
+ | // d[node] = distance from MST (any node in current/partial MST) to node | ||
+ | // p[node] = parent of node: node is connected to MST with edge p[node] - node | ||
+ | foreach (node in nodes) { | ||
+ | d[node] = +oo; // distance not yet computed | ||
+ | p[node] = null; // parent not yet found | ||
+ | } | ||
+ | |||
+ | // STEP 0: initialize results | ||
+ | cost = 0; // cost of the built MST | ||
+ | mst = {}; // the actual set of edges (u, v) in the built MST | ||
+ | |||
+ | // STEP 1: initialize a priority queue | ||
+ | pq = {}; | ||
+ | |||
+ | // STEP 2: add the source(s) into pq | ||
+ | d[source] = 0; // distance from source to MST | ||
+ | p[source] = null; // source never has parent | ||
+ | pq.push( (source, d[source]) ); | ||
+ | |||
+ | // STEP 3: Build MST. | ||
+ | while ( !pq.empty() ) { | ||
+ | // STEP 3.1: Pop next possible node to connect with MST. | ||
+ | node, _ = pq.pop() | ||
+ | if (used[node]) { | ||
+ | continue; | ||
+ | } | ||
+ | |||
+ | // STEP 3.2: Extend MST with edge p[node] - node | ||
+ | used[node] = true; | ||
+ | if (p[node] != null) { | ||
+ | cost += d[node]; | ||
+ | mst += { (node, p[node]) }; | ||
+ | } | ||
+ | |||
+ | // STEP 3.3: relax all edges (node, neigh) | ||
+ | foreach ( (neigh, weight) in adj[node]) { | ||
+ | // Can we find a better / shorter link / edge from node to current MST? | ||
+ | if (!used[neigh] && weight < d[neigh]) { | ||
+ | d[neigh] = weight; // update the new distance from neigh to MST | ||
+ | p[neigh] = node; // save parent | ||
+ | |||
+ | pq.push( (neigh, d[neigh]) ); // replace distance for neigh in pq | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | return cost, mst; | ||
+ | } | ||
+ | |||
+ | // Usage example: | ||
+ | cost, mst = Prim(1, G=(nodes, edges)); // e.g. source = 1, can be any node in G | ||
+ | // 1. Use cost of MST. | ||
+ | // 2. Use edges of MST (e.g. actually build the tree). | ||
+ | </code> | ||
+ | </spoiler> \\ | ||
+ | |||
+ | |||
+ | ==== Complexitate ==== | ||
+ | |||
+ | * **complexitate temporală**: $T = O(m * log n)\ sau\ O(|E| * log |V|)$ | ||
+ | * **complexitate spațială** : $S = O(n)$ | ||
+ | |||
+ | <spoiler Detalii (analiză + optimizări)> | ||
+ | |||
+ | * **complexitate temporală**: Se încearcă relaxarea tuturor celor **m** muchii din graf, analog algoritmului Dijkstra. | ||
+ | * **complexitate spațială** : Se ține o coadă de priorități / un set cu maximum **n** noduri. | ||
+ | * **optimizare**: Analog Dijkstra, putem să obținem: | ||
+ | * **Prim cu heap binar** - $O(m log n)$. | ||
+ | * **Prim cu heap Fibonacci** - $O(n logn + m)$. | ||
+ | |||
+ | </spoiler> \\ | ||
+ | |||
+ | |||
+ | ===== TLDR ===== | ||
+ | |||
+ | * Pentru **minimum spanning tree problem**, am studiat 2 algoritmi (Kruskal și Prim) cu aceeași complexitate ($O(m log m)$). | ||
+ | * Algoritmul lui Kruskal este de preferat pentru simplitatea / intuitivitatea lui, dificultatea reducându-se la a implementa structura de date **DisjointSet**. | ||
+ | |||
+ | ===== Exerciții ===== | ||
+ | |||
+ | |||
+ | <note> | ||
+ | |||
+ | Scheletul de laborator se găsește pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/skel/lab11|pa-lab::skel/lab11]]. | ||
+ | |||
+ | </note> | ||
+ | <note warning> | ||
+ | |||
+ | Înainte de a rezolva exercițiile, asigurați-vă că ați citit și înțeles toate precizările din secțiunea [[https://ocw.cs.pub.ro/courses/pa/skel_graph | Precizari laboratoare 07-12]]. | ||
+ | |||
+ | Prin citirea acestor precizări vă asigurați că: | ||
+ | |||
+ | * știți **convențiile** folosite | ||
+ | * evitați **buguri** | ||
+ | * evitați **depunctări** la lab/teme/test | ||
+ | |||
+ | |||
+ | </note> | ||
+ | |||
+ | ==== Task-1: Kruskal ==== | ||
+ | |||
+ | Se dă un graf **neorientat** și **conex** cu **n** noduri și **m** muchii (cu costuri oarecare pe muchii). | ||
+ | |||
+ | Folosiți algoritmul lui **Kruskal** pentru a găsi un **MST**. | ||
+ | |||
+ | Task-uri: | ||
+ | |||
+ | - Găsiți **costul** MST. | ||
+ | - Găsiți care sunt **muchiile** din MST. **ATENȚIE!** Se poate găsi orice **MST** (în caz că există mai mulți). Ordinea muchiilor din vectorul rezultat **NU** este relevantă. | ||
+ | |||
+ | |||
+ | <note warning> | ||
+ | |||
+ | Restricții și precizări: | ||
+ | |||
+ | * $ n <= 2 * 10^5 $ | ||
+ | * $ m <= 4 * 10^5 $ | ||
+ | * $ -10^3 <= c <= 10^3$, unde c este costul unei muchii | ||
+ | * timp de execuție | ||
+ | * C++: ''%%1s%%'' | ||
+ | * Java: ''%%7s%%'' | ||
+ | * Vă recomandăm să parcurgeți [[https://infoarena.ro/problema/disjoint | DisjointSet]]. **ATENȚIE!** Scheletul de laborator oferă o astfel de implementare. | ||
+ | |||
+ | |||
+ | </note> | ||
+ | |||
+ | |||
+ | === Task-2: Costul minim să conectăm n puncte 2D în plan === | ||
+ | Rezolvați problema [[ https://leetcode.com/problems/min-cost-to-connect-all-points/description/ | Min Cost to Connect All Points ]] pe LeetCode. | ||
+ | |||
+ | |||
+ | |||
+ | ==== BONUS ==== | ||
+ | |||
+ | La acest laborator, asistentul va alege 1-2 probleme din secțiunea extra. | ||
+ | |||
+ | ==== Extra ==== | ||
+ | |||
+ | * [[https://infoarena.ro/problema/desen|infoarena/desen]] | ||
+ | * [[https://infoarena.ro/problema/radiatie|infoarena/radiatie]] | ||
+ | * [[https://infoarena.ro/problema/bile|infoarena/bile]] | ||
+ | * [[https://codeforces.com/problemsets/acmsguru/problem/99999/323|codeforces/aviamachinations]] | ||
+ | |||
+ | ===== Referințe ===== | ||
+ | |||
+ | [0] Chapters **Minimum Spanning Trees**, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein. | ||
+ | |||
+ | [1] [[http://cs.brown.edu/research/pubs/pdfs/1995/Karger-1995-RLT.pdf | A Randomized Linear-Time Algorithm to Find Minimum Spanning Trees]], David R. Karger, Philip N. Klein, Robert E. Tarjan. | ||
+ | |||
+ | [2] [[https://dl.acm.org/doi/pdf/10.1145/355541.355562 | A Minimum Spanning Tree Algorithm with InverseAckermann Type Complexity|]], Bernard, Chazelle. | ||