CT-234 Estruturas de Dados, Dados Análise de Algoritmos e Complexidade Estrutural Carlos Alberto Alonso Sanches CT-234 9)) Algoritmos g em g grafos Tarjan, Dijkstra, Kruskal, Prim Ideia de Tarjan j (1972) ( ) Durante a exploração em profundidade de um digrafo numerar seus vértices de acordo com o digrafo, início e o término dessa exploração. As diferentes situações permitem estabelecer uma classificação dos arcos. Exemplo mp expl: p número de exploração p ç Não visitado Em exploração comp: número de complementação Terminado E 4 1 6 5 A F D B 1 6 A D 2 4 7 E 8 C B 8 3 5 2 H C3 G7 F H G Classificação f ç dos arcos Classificação do arco <v,u>: Árvore (T): u ainda não havia sido explorado, e será filho de v em T (expl[u]=0) A Retorno (B): u é antecessor de v em T, p pois começou antes de v e ainda está em exploração (expl[u]<expl[v] e comp[u]=0) Cruzamento (C): u está em outra árvore ou sub-árvore, pois começou antes de v e já foi explorado (expl[u]<expl[v] e comp[u]>0) Avanço (F): u é descendente de v em T T, pois começou depois de v e já foi explorado (expl[u]>expl[v] e comp[u]>0) D B E C F H G Algoritmo Algor tmo de Tarjan Tarjan(G) { int ce = 0; int cc = 0; for v ∈ V { expl[v] = 0; comp[v] = 0; } for v ∈ V if (expl[v] == 0) DFST( ) DFST(v); } DFST(v) { expl[v] = ++ce; for <v,u> ∈ E if (expl[u] == 0) { tipo[<v,u>] = T; DFST(u); } else if (expl[u] > expl[v]) tipo[<v,u>] = F; else if ( (comp[u] p[ ] > 0) ) tipo[<v,u>] = C; else tipo[<v,u>] = B; comp[v] = ++cc; } Complexidade de tempo: Θ(n+m) Exercício Considere um grafo não orientado, orientado sem laços e sem arestas repetidas. Se aplicarmos nele o algoritmo de Tarjan, Tarjan somente haverá arestas de árvore e de retorno. Por que neste caso não existem arestas de avanço e de cruzamento? Teste de aciclicidade Em certas aplicações aplicações, como a ordenação topológica, uma tarefa importante é o reconhecimento de um digrafo acíclico (conhecido como DAG). A exploração l em profundidade f d d d pode d nos d dar a solução desse problema em tempo O(n+m). Concretamente, basta uma variação do algoritmo de Tarjan: se um arco de retorno for encontrado durante a exploração, então o digrafo será cíclico. cíclico Algoritmo g m Aciclicidade(G) ( ) { stack P; bool aciclico = true; int ce = 0; int cc = 0; for v ∈ V { p [ ] = 0; ; expl[v] comp[v] = 0; } for v ∈ V if (expl[v] == 0) DFSA(v); if (aciclico) di digrafo f é acíclico í li else digrafo é cíclico } DFSA(v) { expl[v] = ++ce; Arco de push(P,v); retorno for <v,u> , ∈ E if (expl[u] == 0) DFSA(u); else if (expl[u]<expl[v] && comp[u]==0) { aciclico = false; // ciclo está em P // desde o topo até u stop; } pop(P); comp[v] = ++cc; } Ordenação ç topológica p g Considere o DAG abaixo: Vamos fazer V f sua exploração em p profundidade dando prioridade aos vértices de menor valor.. m Sequência de término de exploração Algoritmo g m OrdemTopol(G) O d T l(G) { // supõe digrafo acíclico int ce = 0; int cc = 0; for v ∈ V { expl[v] = 0; comp[v] = 0; } for v ∈ V if (expl[v] p == 0) DFSOT(v); for v ∈ V f[v] = n-comp[v]+1; } DFSOT(v) DFSOT( ) { expl[v] = ++ce; for <v,u> ∈ E if (expl[u] == 0) DFSOT(u); comp[v] = ++cc; } Usando uma pilha, seria possível imprimir os vértices jjá na ordem topológica. Como? Complexidade de tempo: Θ(n+m) Bicoloração ç de vértices Vimos que um grafo G é bipartido p (ou bicolorido) quando existe uma bipartição de seus vértices em subconjuntos disjuntos V1 e V2 tais que qualquer aresta de G possui uma extremidade em V1 e outra em V2 . Exemplo: V1 = {0, 3, 4, 7, 10, 11} V2 = {1, 2, 5, 6, 8, 9, 12} É possível demonstrar que um grafo admite bicoloração se e somente se não tiver ciclos de tamanho ímpar. Uma simples variação no algoritmo de exploração em profundidade permite encontrar uma bicoloração de um grafo, se existir. O algoritmo a gor tmo a segu seguirr produz uma bicoloração co oração (atr (atribui u “1” ou “2” ao número de exploração de cada vértice) ou informa que o grafo não pode ser bipartido. Algoritmo g m Ambos pseudocódigos retornam 0 se não existir bicoloração TwoColor(G) { for v ∈ V expl[v] = 0; for v ∈ V if (expl[v] == 0) if (DFSB(v,2) == 0) return 0; return 1; } DFSB(v,color) S ( l ) { c = (color % 2) + 1; expl[v] = c; // troca cor for <v <v,u> u> ∈ E if (expl[u] == 0) { if (DFSB(u,c) == 0) return 0; } else if (expl[u] == c) return 0; return 1; } Complexidade de tempo: O(n+m) Componentes mp fortemente f m conexas (SCC) ( ) Ép possível encontrar as componentes p fortemente conexas de um digrafo através de uma variante do algoritmo de Tarjan. Ideia: Considere a árvore T de exploração em profundidade e a numeração expl[v] para cada v ∈ V. V Os vértices que estão em exploração são empilhados (permanecerão pilha até que q seja j encontrada a sua componente p conexa). ) nessa p Cada vértice v guardará CFC[v], que é o menor número de exploração entre os vértices na pilha que atingir durante sua exploração. Desse modo, ficará á automaticamente associado a uma componente conexa. Quando a exploração do vértice v terminar, se expl[v] = CFC[v] então t d s oss vértices todos é ti s na pilha ilh (desde (d sd o ttopo até té v)) pertencem t m a uma m mesma componente, e podem ser desempilhados. Exemplo mp E 4 4 F 2 H 6 B E A D 6 C 1 1 7 A G D 7 F 2 2 B 8 3 5 5 G 8 7 C 23 H Importante: nem todos os vértices de uma mesma componente terminam com o mesmo valor de CFC. Exemplo: acrescente um arco <C,A> nesse mesmo digrafo, e visite <C,F> antes. Algoritmo g m TarjanCFC(G) { stack P; ; int ce = 0; for v ∈ V expl[v] p = 0; for v ∈ V if (expl[v] == 0) DFSCFC(v); } DFSCFC(v) { expl[v] l[ ] = ++ce; Arco de d árvore push(P,v); CFC[v] = expl[v]; f for < <v,u> > ∈ E if (expl[u] == 0) { DFSCFC(u); CFC[v] = min{CFC[v] min{CFC[v],CFC[u]}; CFC[u]}; } else if (u ∈ P) CFC[v] = min{CFC[v] min{CFC[v],expl[u]}; expl[u]}; if (CFC[v] == expl[v]) do { Teste em tempo x = top(P); constante t t com pop(P); vetor de flags } while (x != v); } Complexidade de tempo: Θ(n+m) Vértices de corte Uma variante do algoritmo de Tarjan pode encontrar os vértices é i d de corte ((ou pontos d de articulação) i l ã )d de um grafo f G=(V,E) conexo não-orientado, sem laços ou arestas repetidas. Ideia: Desse modo, se fizéssemos uma exploração em profundidade a partir de cada vértice do grafo, poderíamos identificar todos os vértices de corte (no entanto, há outra solução mais eficiente) Considere a árvore T de exploração em profundidade e a numeração expl[v] l[ ] para cada d v ∈ V. V Raiz: será vértice de corte se tiver pelo menos dois filhos em T. Demais vértices: v será vértice de corte se tiver algum filho sem retorno para nenhum dos ancestrais de v. É calculado l l d m[v] [ ] = mín{ í { expl[v], l[ ] expl[x] l[ ] }}, onde d x é um vértice é ti que v ((ou um d de seus descendentes) atinge em T através de uma única aresta de retorno. Portanto, v será vértice de corte se tiver algum filho u tal que m[u] ≥ expl[v]. Exemplo mp F 4 4 1 D 1 6 A 6 1 B 1 1 A H 2 12 3 7 1 C D 3 5 1 3 7 B 5 2 6 4 8 C 1 1 G 8 13 E E 5 F 1 5 7 H 3 8 C e H são vértices de corte G 8 Algoritmo Algor tmo TarjanVC(r) { DFSVC(v) { // válido se for conexo e expl[v] = ++ce; // não tiver laços ou m[v] = expl[v]; // arestas repetidas for <v,u> ∈ E int ce = 0; ; if (expl[u] == 0) { for v ∈ V { pai[u] = v; Trata as arestas de expl[v] = 0; nfilhos[v]++; retorno, tanto na ida pai[v] = null; DFSVC(u); como na volta nfilhos[v] = 0; m[v] = min{m[v],m[u]}; VC[v] = false; } } else // arestas de retorno DFSVC(r); if(u != pai[v]) VC[r] = (nfilhos[r] > 1); m[v] = min{m[v],expl[u]}; for v ∈ V-{r} { } p = pai[v]; VC[p] = VC[p] || (m[v]>=expl[p]); } Complexidade p de tempo: p Θ(n+m) ( ) f for v ∈ V if (VC[v]) v é vértice de corte; } Arestas de corte A identificação das arestas de corte (ou pontes) é realizada de maneira semelhante: Encontrar uma árvore á de exploração T, calculando as mesmas numerações expl e m para os vértices. É fácil constatar que nenhuma aresta de retorno dessa exploração pode ser de corte. Uma aresta <v,u> є T será de corte se m[u] = expl[u]. No exemplo mp anterior F 4 4 1 D 1 6 A 6 1 B 1 1 A H 2 12 3 7 1 C D 3 5 1 3 7 B 5 2 6 4 8 C 1 1 G 8 13 E E 5 F 1 5 7 H 3 8 <C,E> e <H,G> são arestas de corte G 8 Algoritmo TarjanAC(r) { // válido se for conexo e // não ã ti tiver l laços ou // arestas repetidas int ce = 0; for v ∈ V { expl[v] = 0; pai[v] = null; } DFSAC(r); } DFSAC(v) { expl[v] = ++ce; m[v] = expl[v]; for o <v,u> ,u ∈ E if (expl[u] == 0) { pai[u] = v; DFSAC(u); m[v] = min{m[v],m[u]}; if (m[u] == expl[u]) <v,u> é aresta de corte; } else // arestas de retorno if(u != pai[v]) m[v] = min{m[v],expl[u]}; } Complexidade de tempo: Θ(n+m) Caminhos m mais m curtos Um digrafo g G=(V,E), onde V = {v1, v2, ..., vn}, é chamado de ponderado se cada arco (vi, vj) ∈ E tem um custo cij. Distância entre dois vértices é a somatória dos custos dos arcos de um caminho que os une. Um problema clássico em grafos é encontrar o caminho mais curto ou a distância mínima entre dois vértices. E Exemplo: l B 2 8 5 C D 4 4 F I 2 4 4 7 H 4 4 5 A E 6 5 2 4 2 G 4 J K Distância mínima entre A e K: 7+2+2+5 = 16 Uma m condição ç de existência Considere o caminho abaixo entre i e j, e o ciclo w : k j i w Se o comprimento de w for negativo, qual seria a distância mínima entre i e j ? Uma condição de existência para o caminho mais curto é que seja elementar, isto é, é sem ciclos em seu interior. Grafos f com m arestas de mesmo m m peso p Quando todas as arestas têm pesos iguais, basta uma p alteração ç na simples exploração em largura. Afinal, os vértices vão sendo Afinal enfileirados seguindo a ordem de proximidade: portanto, basta incrementar a distância do vértice antecessor antecessor. O algoritmo de Dijkstra generaliza l essa ideia. d BFSMinCam(r) { q queue q q; int ce = 0; for v ∈ V - {r} { d[v] = +∞; expl[v] = 0; } expl[r] = ++ce; d[r] = 0; enqueue(q,r); while (!isEmpty(q)) { u = dequeue(q); for <u,v> ∈ E { if (expl[v] == 0) { expl[v] = ++ce; d[v] = d[u] + 1; enqueue(q,v); } } } } Exemplo mp do algoritmo g m de Dijkstra j +∞ +∞ 4 4 4 7 +∞ 5 2 2 +∞ 5 7 1 0 1 5 1 2 3 3 6 7 2 +∞ 1 4 4 5 2 1 0 1 5 1 +∞ 2 0 3 1 6 7 5 2 1 5 1 2 3 3 1 +∞ 8 5 5 3 7 1 0 5 1 3 1 5 2 5 2 2 1 2 3 7 8 4 4 4 6 68 4 5 2 5 5 2 3 7 1 0 1 5 1 2 7 4 5 2 5 3 7 3 3 1 6 8 7 8 8 4 5 3 7 1 3 4 65 2 3 +∞ 5 7 6 7 8∞ +∞ 6 6 1 0 1 5 1 2 3 3 1 7 6 6 Algoritmo g m de Dijkstra j (1959) ( ) Dijkstra(u) { for v ∈ V – {u} d[v] = +∞; d[u] = 0; S ← V; \\ vértices com distância indefinida while S ≠ ∅ { selecionar j tal que d[j] == mini∈S{d[i]}; S ← S – {j}; \\ j passa a ter distância definida for <j,w> ∈ E, onde w ∈ S if (d[w] > d[j] + cjw) { d[ ] = d[j] + cjw; d[w] pred[w] = j; } } Predecessor: permite encontrar os vértices presentes nesse caminho Assemelha-se a uma exploração em largura Complexidade mp de tempo mp Grosso modo, o algoritmo g de Dijkstra j g gasta tempo p O(n2+m) = O(n2). No entanto N t t é possível í l melhorar lh essa complexidade l id d se o conjunto S for implementado com um heap de mínimo. í i Nesse caso, caso o tempo passaria a ser O((n O((n+m) m).log log n): O heap possuirá inicialmente n elementos. No total, são realizadas n extrações de mínimo e até m modificações de valor (será preciso manter um vetor auxiliar que armazena a posição corrente que cada vértice ocupa no heap). Arcos com m custos negativos g No grafo abaixo, a a o, qua qual a distância stânc a m mínima n ma entre ntr os vértices rt c s A C e C? 3 A -8 10 B O algoritmo de Dijkstra daria como resposta 3, 3 mas o valor correto é 2... Por que isso acontece? No processo d N de construção t ã d do caminho i h mínimo, í i o algoritmo l it de Dijkstra supõe que o custo acumulado sempre cresce. Isso não ocorre se admitirmos arcos com custos negativos... negativos Em um algoritmo mais geral, cada vez que se altera a distâ i até distância té um vértice, é ti também t bé deve d sser recalculada l l d a distância até todos os seus adjacentes. Algoritmo g m de Dijkstra j modificado m f Dijkstra2(u) { for v ∈ V – {u} d[v] = +∞; d[u] = 0; S ← {u}; { } while S ≠ ∅ { selecionar j ∈ S; S ← S – {j}; for <j,w> ∈ E if (d[w] > d[j] + cjw) { d[w] d[ ] = d[j] + cjw; pred[w] = j; S ← S ∪ {w}; \\ w volta para S } } Com uma estrutura adequada para S, S a complexidade de tempo desse algoritmo pode ser o(n.m). Árvore geradora g de custo mínimo m m (MST) ( ) Dado um grafo G=(V G=(V,E), E) conexo e ponderado ponderado, com custo associado c(e), e ∈ E, deseja-se encontrar um subgrafo T tal que: seja gerador de G (isto é, possua todos os vértices); seja acíclico e conexo (isto é, uma árvore); t h custo tenha t total t t l c(T) (T) = Σe∈Ec((e) que seja j mínimo. í i T também m m costuma m ser chamado m de árvore de espalhamento de custo mínimo. Este E t conceito it poderia d i ser generalizado li d para um grafo f desconexo... Exemplo mp 4 A B 9 5 4 3 E 8 F 2 2 7 D C 9 4 A A 5 4 3 4 B E 8 F B 4 3 E 2 F 2 D C Árvore geradora Á d com custo 24 D C Árvore geradora Á com custo 15 Ideia de Kruskal (1956) ( ) Princípio: a aresta de menor custo sempre pertence à árvore geradora de custo mínimo. Demonstração: Suponha, por absurdo Suponha absurdo, que a aresta de custo mínimo não esteja na solução ótima. A inserção i ã desta d aresta na solução l ã ótima ó i gera um ciclo. i l Removendo-se a aresta de maior custo neste ciclo (que (q não é a inserida), obtém-se uma nova árvore geradora. Essa nova árvore tem um custo inferior à solução ótima inicial: contradição. Exemplo mp 4 B C 4 6 7 A Lista ordenada d d 3 D 8 M 5 4 J 6 H 7 G 1 E 4 I 3 2 L 2 3 2 F ( )=7 3 10 20 c(T) 1 5 13 16 24 Componentes { A{{}AA {{}A}A, { }A, BB {{}B B, }B { }B}C, { }C{D, {C, }{ CC, E, {D, }C, {F, D, {D D, E, D, D, G, E, }{E L, E, D, E, J, L, }F, {LL E, LF, EG, }{}}L }G, FJ}}{J}{F}F} } {{GF,{}J F, }G, { HJ{}}G H { H, } { {I H {}{ H I} }}{ J M { {I}{}}M I }}{{ L {MM {}}M} }{ M } e c(e) ((D,E) , ) 1 (D,L) 2 (F,J) 2 (G,J) 2 (C,D) 3 (E,F) 3 (H,I) 3 (A,B) 4 (B,C) 4 … … Exemplo mp 4 B C 4 6 7 A Lista ordenada d d 3 D 8 M 5 4 J 6 H 7 G 1 E 4 I 3 2 L 3 2 2 F c(T) (T) ( ) = 33 24 28 Componentes { {A,A,B,B,C,C,D,D,E,E,F,F,G, G,H,J,I,LJ,} L } { A, B, C, D, E, F, G, H, I, J, L, M } { H, I }{ M }{ M } e c(e) ... ... (A,I) 4 (J,L) X 4 (G,M) 5 (C,M) 6 (I,J) 6 (A,M) 7 (G,H) 7 (B,L) 8 Algoritmo g m de Kruskal Kruskal(G) { T ← ∅; ∅ A ← vetor com as arestas em ordem crescente de custo; criar n componentes com os n vértices isolados; f for (i (i=1; 1 i< i<=m && |T|<n-1; |T|< 1 i++) { <u,v> = A[i]; if (u e v não estão na mesma componente) { T ← T ∪ {<u,v>}; unir as componentes que contêm u e v; } } } Operações p r ç executadas: u Ordenação de m valores 2m testes de componentes n-1 uniões de componentes Complexidade mp de tempo mp Implementação simples: utilizar um vetor de tamanho n que indica a componente de cada vértice. Tempo gasto: Ordenação dos custos: O(m O(m.log log m) 2m testes de componentes: O(m) n-1 uniões de componentes: O(n2) T t l O(m.log Total: O( l m + n2) Estrutura de dados específica p f Os vértices de cada componente são colocados em uma estrutura de árvore e representados pela raiz. Deste D st modo, m d na n união niã d de componentes, mp n nt s a raiz i d da á árvore mais baixa torna-se filha da raiz da árvore mais alta. Encontrar a componente de um vértice corresponde a percorrer o caminho dele até sua raiz. Na procura da componente de um vértice, aproveita-se o percurso para que depois todos apontem para a raiz. Pode-se demonstrar que as alturas dessas árvores permanecem O(log p ( g n). ) Na verdade,, é bem inferior a isso... Complexidade mp de tempo mp Com essa estrutura específica: Ordenação dos custos: O(m.log m) 2m testes de componentes: O(m.log n) n-11 uniões iõ d de componentes: t Θ( ) Θ(n) Total: O(m.log (m g m), supondo p m > n. Ideia de Prim m (1957) ( ) Inicialmente T será um vértice arbitrário de G Inicialmente, G. Critério de inclusão de vértices e arestas em T: Dentre todas as arestas de G incidentes em T, escolhe-se a menor nor custo. de m Essa nova aresta e seu vértice adjacente serão incluídos em T somente se esse novo vértice ainda não estiver em T. T O processo termina quando T ficar com n vértices. É preciso utilizar uma estrutura de dados que armazene em ordem crescente de custo os vértices ainda não incluídos na árvore. Exemplo mp 4 B C 4 6 7 A 3 D 8 M 5 4 J 6 H 7 1 E 4 I 3 2 L G c(T) = 8 4 12 14 17 19 21 25 28 33 11 2 3 2 F Algoritmo g m de Prim m Prim(r) { T ← ∅; \\ árvore de espalhamento de custo mínimo U ← {r}; \\ vértices que já estão na árvore while (U ≠ V) { <u,v> = aresta de custo mínimo | u∈U e v∈V-U; T ← T ∪ {<u,v>}; \\ aqui, T contém só arestas U ← U ∪ {v}; } } C Com h heap d mínimo, de í i pode d ser iimplementado l t d em ttempo O(( O((n+m).log ) l n): ) O heap possuirá apenas os vértices vizinhos da árvore em construção, cada um com sua distância corrente ((começará ç com o vértice r,, com distância nula). ) Quando um vértice é retirado desse heap, modificam-se as distâncias que seus vizinhos têm em relação à árvore (será preciso manter um vetor auxiliar que armazena a posição corrente de cada vértice no heap). ) No total, são realizadas n extrações de mínimo e até m modificações de valor. Outros p problemas m em m grafos g f Vimos algumas resoluções algorítmicas eficientes de determinados problemas em grafos. Teste de planaridade: também pode ser realizado em tempo polinomial no tamanho h do d grafo: f algoritmos l i APG PG (Auslander, ( l d P Parter e Goldstein) G ld i ) e LEC (Lempel, Even e Cederbaum), com suas diferentes implementações No entanto entanto, na teoria de grafos há ainda muitos problemas importantes que são intratáveis, ou seja, se desconhecem ç de tempo p polinomial: p resoluções Clique máximo Coloração Caixeiro viajante Cobertura de vértices Conjunto de vértices dominantes etc. Exercícios Simular (e implementar) em diversos grafos grafos: Variantes do algoritmo de Tarjan: Cl ifi Classificação ã de d arcos Teste de aciclidade Ordenação topológica dos vértices Bicoloração dos vértices Determinação de componentes fortemente conexas Identificação de vértices e arestas de corte Algoritmo de Dijkstra (com custos não negativos) Algoritmo de Kruskal Algoritmo de Prim