Ciclos e digrafos acíclicos

Ciclos tornam digrafos complexos, interessantes e difíceis. Digrafos sem ciclos são fáceis, mas bastante úteis. Digrafos com ciclos são como animais selvagens, enquanto digrafos sem ciclos são como animais domesticados.

Esta página trata do problema de encontrar um ciclo num digrafo. O problema tem uma solução muito elegante e eficiente que começa com uma busca em profundidade.

Esta página é inspirada na seção 22.4 de CLRS).  

does this digraph have a cycle?

Ciclos

Um ciclo (= cycle) é um caminho com dois ou mais arcos cujo primeiro vértice coincide com o último. Em outras palavras, um ciclo é um caminho  (v0, v1, … , vk-1, vk)  tal que vk = v0 e k ≥ 2. (Convém lembrar que caminhos não têm arcos repetidos.)

../gif/DC8.png

Os arcos de um ciclo apontam todos na mesma direção. Para enfatizar esse fato, usam-se ocasionalmente as expressões ciclo orientado e ciclo digrigido (= directed cycle) no lugar de ciclo.

Um ciclo é simples se não tem vértices repetidos exceto o último, que é igual ao primeiro. Todo ciclo contém um ciclo simples: se C é um ciclo então alguma subsequência de C é um ciclo simples.

Problema do ciclo:  Encontrar um ciclo num digrafo dado.

Nem toda instância do problema tem solução, pois muitos digrafos são acíclicos, ou seja, não têm ciclos.  Digrafos acíclicos são conhecidos como DAGs (= Directed Acyclic Graphs). Faz parte do problema verificar se o digrafo dado é um DAG.

A versão booleana do problema consiste em decidir, tão somente, se um digrafo tem um ciclo. Diremos que essa é a versão de decisão do problema.

../gif/google_centrality.jpg

Exemplo A:  No digrafo da figura à direita, (4, 1, 6, 4, 5, 4) é um ciclo. Os ciclos (1, 6, 4, 1) e (1, 6, 1) são simples. Nem (6) nem (6, 4, 1, 6, 4, 5, 6) são ciclos.  A figura abaixo mostra um DAG.

../gif/Directed_acyclic_graph.png

Exercícios 1

  1. ★ Discuta o seguinte algoritmo para o problema do ciclo: para cada arco vw do digrafo, encontre um caminho de wv; para procurar um caminho, use busca em largura ou busca em profundidade. Quanto tempo esse algoritmo consome? Sua estimativa é justa? O algoritmo é eficiente?
  2. ★ Discuta a seguinte heurística, que tenta resolver o problema do ciclo. A heurística penetra no digrafo a partir de um vértice r à procura de um ciclo. O caminho percorrido é pintado de cinza. Se chegar a um beco sem saída, ou seja, a um vértice de grau de saída nulo, a heurística desiste de encontrar um ciclo. Descreva a heurística em código.
  3. Mostre que toda floresta radicada é um DAG. Dê um exemplo de um DAG que não é uma floresta radicada.

Algoritmo

O seguinte algoritmo decide, de uma maneira muito elegante e eficiente, se um digrafo G tem um ciclo. O algoritmo faz uma busca em profundidade, calcula uma floresta DFS, e em seguida procura arcos que sejam de retorno em relação à floresta.

Ciclo (G)
1 . (pai, pós) := Busca-em-Profundidade (G)
2 . para cada v em V(G)
3 .ooo para cada w em Adj[v]
4 .oooooo se pós[v] < pós[w]
5 .ooooooooo devolva true e pare
6 . devolva false

A linha 1 constrói uma floresta DFS, representada pelo vetor pai. Na linha 4, a desigualdade pós[v] < pós[w] mostra que vw é um arco de retorno, ou seja, que w é ancestral de v na floresta. Portanto, existe um caminho, digamos P, de w a v na floresta. A concatenção P + vw é um ciclo. Portanto, o devolva true na linha 5 está correto!

Resta mostrar que o return false na linha 6 está correto. Suponha que estamos no início da linha 6. Nessa ocasião, temos

  pós[v] > pós[w] para todo arco vw. (*)

Isso garante que não existe ciclo algum! Com efeito, qualquer ciclo teria que voltar ao ponto de partida, e (*) torna isso impossível. Portanto, o devolva falso na linha 6 está correto.

O análise do algoritmo prova o seguinte teorema: dada uma floresta DFS de um digrafo, todo ciclo do digrafo tem um arco de retorno em relação à floresta.

Desempenho.  Tal como a busca em profundidade, o algoritmo Ciclo consome

Ο(n + m)

unidades de tempo, sendo n o número de vértices e m o número de arcos do digrafo. Essa estimativa é justa, pois para algumas famílias de digrafos o algoritmo consome Ω(n + m) unidades de tempo.

Exercícios 2

  1. Mostre que o algoritmo Ciclo consome Θ(n + m) unidades de tempo no pior caso.
  2. Um arco de avanço em relação a uma floresta DFS pode pertencer a um ciclo? Um arco cruzado pode pertencer a um ciclo?

Permutação topológica

A condição (*) acima é tão importante que merece uma discussão mais geral. Uma permutação (v1, v2, … , vn) dos vértices de um digrafo é topológica se

i < j  para todo arco vivj ,

ou seja, se todos os arcos do digrafo apontam da esquerda para a direita em relação à permutação. Uma permutação (v1, v2, … , vn) é anti-topológica se i > j para todo arco vivj, ou seja, se todos os arcos apontem da direita para esquerda.  Qualquer permutação anti-topológica pode ser representada por um vetor numérico, digamos t, tal que t[v] > t[w] para todo arco vw. Como vimos em (*) acima, o vetor pós no algoritmo Ciclo tem essa propriedade.

figs/Sedgewick-Wayne/topological-sort-of-courses-x.png

Exemplo B.  As duas figuras representam o mesmo digrafo. A figura à direita sugere a permutação topológica (8, 7, 2, 3, 0, 6, 9, 10, 11, 12, 1, 5, 4). Todos os arcos apontam de cima para baixo.

figs/Sedgewick-Wayne/standard-digraph-model-1.png

Exemplo C.  Suponha que um certo projeto de software é composto por um grande número de tarefas. Há uma relação de precedência entre tarefas: uma tarefa não pode ser executada antes que as tarefas que a precedem tenham sido executadas. O projeto pode ser representado por um digrafo: cada tarefa é um vértice e há um arco vw se e somente se a tarefa v precede a tarefa w. Suponha agora que cada tarefa consome 1 dia de trabalho e duas tarefas não podem ser executadas no mesmo dia. Qualquer permutação topológica do digrafo descreve um cronograma de execução do projeto.

A existência de uma permutação topológica é uma prova simples e cabal de que o digrafo é acíclico. De fato, como já observamos acima, qualquer ciclo precisa voltar ao ponto de partida, e a permutação topológica impede essa volta.

Na linha 6 do algoritmo Ciclo, a permutação dos vértices em pós-ordem inversa — ou seja, a permutação que coloca os vértices em ordem decrescente de pós — é topológica. Assim, o algoritmo prova o seguinte teorema:

Teorema:  Um digrafo é acíclico se e somente se seu conjunto de vértices admite um permutação topológica.

Mas o algoritmo Ciclo faz mais que provar o teorema. Suponha que na linha 6 o algoritmo devolva o vetor pós e na linha 5 devolva o arco vw e o vetor pai. Com esse aparelhamento, o algoritmo não só diz se G tem um ciclo como também fornece um certificado que habilita o usuário a verificar, por conta própria, se a resposta está correta:

Exercícios 3

  1. Suponha que (v1, v2, … , vn) é uma permutação topológica dos vértices de um digrafo. Mostre que o grau de entrada de v1 é 0 e o grau de saída de vn é 0. Algum outro vértice pode ter uma dessas propriedades?
  2. Mostre que um digrafo pode ter várias permutações topológicas diferentes.
  3. Suponha que os vértices de nosso digrafo são 1, 2,  …  , n e que sequência (1, 2,  …  , n) é uma permutação topológica. Que aparência tem a matriz de adjacência do digrafo?
  4. Suponha que um digrafo tem uma permutação topológica. Mostre como obter um vetor t, indexado pelos vértices, tal que t[v] < t[w] para todo arco vw. Em seguida, mostre como executar a operação recíproca: transformar um vetor t com a propriedade que acabamos de especificar em uma permutação topológica dos vértices.
  5. ★ Acrescente código ao algoritmo Ciclo para imprimir um ciclo na linha 5 e uma permutação topológica na linha 6.
  6. Escreva um algoritmo que receba um digrafo G e uma permutação topológica de seus vértices armazenada num vetor p[1 .. n] e um calcule um vetor numérico t indexado por vértices tal que t[v] < t[w] para todo arco vw.  Escreva um algoritmo que receba um digrafo G e um vetor numérico t indexado por vértices tal que t[v] < t[w] para todo arco vw e imprima os vértices do digrafo em ordem topológica.

Apêndice: versão on-the-fly do algoritmo

O algoritmo Ciclo examina o digrafo todo duas vezes: uma vez na linha 1 e outra no bloco de linhas 2-5. Embora isso resulte em um algoritmo linear, é preferível obter o mesmo efeito examinando o digrafo uma só vez. Para fazer isso, é preciso inserir o código de Busca-em-Profundidade na linha 1 de Ciclo e interromper a busca assim que um ciclo for encontrado. Diremos que essa é a versão on-the-fly de Ciclo.

Ciclo-on-the-fly (G)
1 . para cada r em V(G)
2 .ooo pré[r] := pós[r] := 0
3 . t := 0
4 . para cada r em V(G)
5 .ooo se pre[r] = 0
6 .oooooo pai[r] := r
7 .oooooo se DFS-r-Cycle (G, r) = true
8 .ooooooooo devolva true e pare
9 . devolva false
DFS-r-Cycle (G, v)
10 . pré[v] := t := t+1
11 . para cada w em Adj[v]
12 .ooo se pré[w] = 0
13 .oooooo pai[w] := v
14 .oooooo se DFS-r-Cycle (G, w) = true
15 .ooooooooo devolva true
16 .ooo senão se pós[w] = 0
17 .ooo senão ooo devolva true base da recursão
18 . pós[v] := t := t+1
19 . devolva false