Algoritmos para Grafos  |  Índice

Florestas DFS

figs/the-web/footprints-2.jpg

A busca em profundidade deixa uma espécie de rastro ao percorrer um grafo.  Esse rastro é uma floresta radicada.  Pode-se dizer que a floresta é um esqueleto do grafo.  Para registrar a floresta radicada, basta acrescentar duas linhas de código ao par de funções GRAPHdfs()dfsR() que discutimos no capítulo Busca em profundidade.

Sumário:

Floresta da busca

Examine as funções GRAPHdfs() e dfsR() que implementam uma busca em profundidade.  O arco  v-w  que dfsR() percorre para descobrir o vértice  w  é conhecido como arco de floresta.   No fim da execução de GRAPHdfs(), o conjunto dos arcos de floresta define uma floresta radicada.  Essa é a floresta de busca em profundidade, ou floresta DFS, construída pela função.

Uma floresta DFS contém todos os vértices do grafo e portanto é um sub­grafo gerador.  A floresta DFS se diferencia de outras sub­florestas geradoras do grafo por estar associada a uma numeração do grafo em pré-ordem, que é também uma numeração topológica da floresta.

Uma floresta DFS pode ser confortavelmente representada por um vetor de pais, digamos  pa[0..V-1]:  para cada vértice wpa[w] é o pai de w na floresta.  (O vetor de pais é como a trilha de migalhas de pão na conto infantil João e Maria.)  A seguinte função calcula o vetor de pais à medida que numera o grafo em pré-ordem:

static int cnt;
int pre[1000];
vertex pa[1000];
/* A função GRAPHdfs() faz uma busca DFS no grafo G. 
   Ela atribui um número de ordem pre[x] a 
   cada vértice x 
   (o k-ésimo vértice descoberto recebe o número de ordem k) e
   registra a correspondente floresta DFS no vetor de pais pa[]. 
   (Código inspirado no programa 18.3
   de Sedgewick.) */
void GRAPHdfs( Graph G) 
{ 
   cnt = 0;
   for (vertex v = 0; v < G->V; ++v) 
      pre[v] = -1;
   for (vertex v = 0; v < G->V; ++v)
      if (pre[v] == -1) {
         pa[v] = v; // v é uma raiz da floresta
         dfsR( G, v);
      }
}
/* A função dfsR() visita 
   todos os vértices de G 
   que podem ser alcançados a partir de v
   sem passar por vértices já descobertos.
   Todos esses vértices, e só esses,
   tornam-se   descendentes de v
   na floresta radicada definida por pa[].
   Se x é o k-ésimo vértice descoberto,
   a função atribui cnt+k a pre[x]. 
   (O código supõe que G 
   é representado por listas de adjacência.) */
static void dfsR( Graph G, vertex v) 
{ 
   pre[v] = cnt++; 
   for (link a = G->adj[v]; a != NULL; a = a->next) {
      vertex w = a->w;
      if (pre[w] == -1) {
         pa[w] = v; // v-w é arco da floresta 
         dfsR( G, w); 
      }
   }
}

(O código acima supõe que G é representado por listas de adjacência. Não é difícil adaptar o código para grafos representados por matriz de adjacências.)

O sub­grafo de G representado pelo vetor pa[] é, de fato, uma floresta radicada pois [!]

  1. o vetor pre[] é uma numeração topológica do sub­grafo e
  2. no máximo um arco do sub­grafo entra em cada vértice.

Para deduzir essas duas propriedades basta examinar o código com atenção.

A floresta DFS consiste em uma ou mais árvores radicadas.  Cada etapa da busca constrói uma das árvores. A etapa que começa com a linha dfsR( G, v) de GRAPHdfs() constrói uma árvore com raiz v.

Exemplo A. Aplique a função GRAPHdfs() ao grafo G representado pelas seguintes listas de adjacência:

figs/mine/diwheel6-j20.png
0: 1 4
1:
2: 0 3 4
3: 4 5
4: 1 5
5: 1

Cada lista de adjacência está em ordem crescente, o que torna fácil conferir o rastreamento da execução da função:

figs/mine/diwheel6-j20-gray.png
v-w dfsR(G,w)

0 dfsR(G,0)
0-1 dfsR(G,1)
  1
0-4 dfsR(G,4) 
  4-1
  4-5 dfsR(G,5) 
    5-1
    5
  4
0

2 dfsR(G,2)
2-0               
2-3 dfsR(G,3)
  3-4
  3-5
  3
2-4
2

Um vértice v isolado numa linha da tabela representa o fim do processamento de v, ou seja, o fim da execução de dfsR(G,v).  As linhas do rastreamento que têm a forma  v-w dfsR(G,w)  mostram que o vetor pa[] estará no seguinte estado no fim da execução da função:

   w   0  1  4  5  2  3
pa[w]  0  0  0  4  2  2

Aqui os vértices estão listados na ordem em que foram descobertos, mas a tabela pode ser reescrita colocando a primeira linha em ordem crescente:

   w   0  1  2  3  4  5
pa[w]  0  0  2  2  0  4

A floresta DFS consiste em duas árvores: uma tem raiz 0 e a outra tem raiz 2.

Exemplo B. Aplique a função GRAPHdfs() ao grafo G representado pelas seguintes listas de adjacência:

0: 2 4
1: 3
2: 7
3: 6
4: 5 7
5: 4 1 7
6: 0 2 4
7: 5 3

Para tornar o exemplo mais interessante, começamos a busca pelo vértice 2 e não pelo vértice 0, como manda o código de GRAPHdfs().  Veja o rastreamento da execução da função:

figs/Sedgewick-Wayne/spt-from-2.png
2 dfsR(G,2)
2-7 dfsR(G,7)
  7-5 dfsR(G,5)
    5-4 dfsR(G,4)
      4-5
      4-7
      4
    5-1 dfsR(G,1)
      1-3 dfsR(G,3)
        3-6 dfsR(G,6)
          6-0 dfsR(G,0)
            0-2
            0-4
            0
          6-2
          6-4
          6
        3
      1
    5-7
    5
  7-3
  7
2

É fácil deduzir do rastreamento o estado final do vetor pa[]. Para cada linha da forma v-w dfsR(G,w) na tabela temos pa[w] = v:

   w   2 7 5 4 1 3 6 0
pa[w]  2 2 7 5 5 1 3 6

Exemplo C. Seja G o grafo não-dirigido representado pelas seguintes listas de adjacência:

figs/Sedgewick-Wayne/GraphRepDFSex-tinyCG-x.png
0: 2 1 5
1: 0 2
2: 0 1 3 4
3: 5 4 2
4: 2 3
5: 0 3
figs/Sedgewick-Wayne/DFSTinyPath-x.png

Veja o rastreamento da execução de GRAPHdfs( G):

0 dfsR(G,0)
0-2 dfsR(G,2)
. 2-0
. 2-1 dfsR(G,1)
. . 1-0
. . 1-2
. . 1
. 2-3 dfsR(G,3)
. . 3-5 dfsR(G,5)
. . . 5-0
. . . 5-3
. . . 5
. . 3-4 dfsR(G,4)
. . . 4-2
. . . 4-3
. . . 4
. . 3-2
. . 3
. 2-4
. 2
0-1
0-5
0

(Os pontos foram acrescentados apenas para deixar o alinhamento vertical mais visível.)  Basta observar as linhas da forma v-w dfsR(G,w) para deduzir o estado final do vetor de pais:

   0     
   |     
   2     
  / \    
 1   3   
    / \  
   5   4 
   w   0 2 1 3 5 4
pa[w]  0 0 2 2 3 3

Essa floresta DFS consiste em uma só árvore, que tem raiz 0.  Às vezes é útil fazer uma figura do grafo que coloque a floresta DFS em destaque. Veja a figura à direita, oriente todos os arcos de cima para baixo, e acrescente os demais arcos do grafo.

Exercícios 1

  1. A floresta radicada que aparece em destaque na figura é uma floresta de busca em profundidade? Em caso afirmativo, dê as listas de adjacência que representam o grafo.
  2. É verdade que todo grafo tem uma única floresta DFS?
  3. A figura ao lado (copiada do livro de Sedgewick-Wayne) mostra a construção de uma floresta DFS num grafo não-dirigido aleatório com 250 vértices.  Note que a floresta começa como um caminho que vai penetrando profundamente no grafo.
  4. Execute uma busca em profundidade no grafo dado pelas listas de adjacência a seguir.  Exiba o vetor de pais da floresta DFS.
     0: 1 4   
     1: 2 5   
     2: 3     
     3: 7     
     4: 8     
     5: 4     
     6: 5 10 2
     7: 11 6  
     8: 9     
     9: 5 8   
    10: 9     
    11: 10    
    
  5. Prova da correção.  Prove que GRAPHdfs() está correta. Especificamente, prove que depois da execução de GRAPHdfs() o vetor pre[] é uma numeração topológica do sub­grafo de G representado pelo vetor pa[]. Mostre também que no máximo um arco do sub­grafo entra em cada vértice.
  6. ★ Suponha que G é uma floresta radicada. Submeta G à função GRAPHdfs() e considere os vetores pre[] e pa[] calculados pela função. É verdade que pa[] é o vetor de pais de G?  É verdade que pre[] é uma numeração topológica de G?
  7. Analise e discuta a seguinte versão alternativa de GRAPHdfs():
    void GRAPHdfs( Graph G) { 
       cnt = 0;
       for (vertex v = 0; v < G->V; ++v) pre[v] = -1;
       for (vertex v = 0; v < G->V; ++v)
          if (pre[v] == -1) 
             dfsR( G, v, v);
    }
    static void dfsR( Graph G, vertex u, vertex v) { 
       pre[v] = cnt++; 
       pa[v] = u;
       for (link a = G->adj[v]; a != NULL; a = a->next)
          if (pre[a->w] == -1) 
             dfsR( G, v, a->w);
    }
    
  8. Matriz de adjacências.  Escreva uma versão da função dfsR() acima para grafos representados por matriz de adjacências.
  9. Alocação dinâmica.  O código de GRAPHdfs() acima aloca os vetores pre[] e pa[] estaticamente. Reescreva o código usando alocação dinâmica.
  10. [Sedgewick 18.18]  Altura da floresta DFS.  Escreva uma variante da função GRAPHdfs() acima que calcule a altura da floresta DFS.
  11. Caminhos.  Escreva uma função que use GRAPHdfs() para resolver o seguinte problema: dado um grafo G e dois vértices s e t, imprimir um caminho de st. (Esse problema é um complemento do problema de acessibilidade no capítulo Acessibilidade.)  Se um tal caminho não existir, imprima um conjunto de vértices que contenha s, não contenha t, e tenha leque de saída vazio. Esse conjunto servirá como prova da inexistência do caminho desejado.

Acima, abaixo, à esquerda e à direita

Digamos que x e y são dois vértices da floresta DFS de um grafo.  Como em qualquer floresta radicada, x pode ser ancestral, descendente, ou primo de y. (Um vértice é primo de outro se não for ancestral nem descendente do outro.)  Além disso, em consequência da ordem em que os vizinhos de cada vértice são visitados durante a busca DFS, podemos distinguir um vértice mais velho de um vértice mais novo. Assim, se x e y são primos e pre[] é a numeração em pré-ordem associada à floresta então

É claro que essas relações são mutuamente inversas: x é primo mais velho de y se e somente se y é primo mais novo de x.

Essas relações genealógicas entre os vértices de uma floresta DFS poderiam também ser descritas por uma metáfora espacial: acima corresponde a ancestral, abaixo corresponde a descendente, à esquerda corresponde a primo mais velho e à direita corresponde a primo mais novo.

Como saber, algoritmicamente, se um dado vértice é ancestral, descendente, primo mais velho ou primo mais novo de outro?  A aplicação simplória das definições não leva a um algorimo rápido pois exige que a floresta seja percorrida a partir de um dos vértices à procura do outro, e isso pode consumir muito tempo. O capítulo seguinte dará uma resposta eficiente depois de introduzir a numeração em pós-ordem.

Exercícios 2

  1. Pegadinha.  Considere o grafo definido pelos arcos 0-1 0-4 2-0 2-3 2-4 3-4 3-5 4-1 4-5 5-1.  O vértice 4 é primo esquerdo ou direito do vértice 3?
  2. Escolha um grafo interessante G e construa uma floresta DFS de G. Exiba dois vértices xy tais que x é ancestral de y. Exiba dois vértices xy tais que x é descendente de y. Exiba xy tais que x é primo esquerdo de y. Exiba xy tais que x é primo direito de y.
  3. ★ Sejam x e y dois vértices quaisquer e suponha que pre[x] < pre[y] depois de uma execução de GRAPHdfs(). É verdade que x é primo esquerdo de y?

Arcos de retorno, de avanço, e cruzados

Dada uma floresta DFS de um grafo, cada arco v-w do grafo pertence a um de três tipos, conforme a posição relativa de vw na floresta:

Cabem duas observações:  1. Usualmente, quando se diz arco de avanço fica subentendido que o arco não é de floresta, embora todos os arcos da floresta sejam também de avanço.  2. As pontas de um arco cruzado podem estar em diferentes árvores da floresta.

É fácil deduzir do código de dfsR() que os arcos cruzados são todos esquerdos, ou seja, que não existe arco v-w tal que w é primo direito de v.  Em outras palavras, se v-w é um arco e w é primo de v então w é primo esquerdo de v.

Essa propriedade tem a seguinte consequência imediata:  grafos não-dirigidos não têm arcos cruzados,

Exemplo D. Na figura abaixo, os arcos de floresta são vermelhos, os de retorno são verdes, os de avanço são roxos, e os cruzados são azuis.  (O exemplo foi copiado dos slides de 2011 de José Coelho de Pina.) 

coelho-aula06-arcos-dfs.png

Exemplo E. Em relação à floresta DFS calculada no exemplo B, os arcos do grafo que não pertencem à floresta têm a seguinte classificação:

figs/Sedgewick-Wayne/spt-from-2.png
0-2  retorno
0-4  cruzado
4-5  retorno
4-7  retorno
5-7  retorno
6-2  retorno
6-4  cruzado
7-3  avanço

Exemplo F. Em relação à floresta DFS calculada no grafo não-dirigido do exemplo C, os arcos que não pertencem à floresta têm os seguintes tipos:

figs/Sedgewick-Wayne/DFSTinyPath-x.png
0-1  avanço
0-5  avanço
1-0  retorno
1-2  retorno
2-0  retorno
2-4  avanço
3-2  retorno
4-2  retorno
4-3  retorno

Como acontece com todos os grafo não-dirigidos, não há arcos cruzados.

Exercícios 3

  1. Pegadinha.  Considere o grafo definido pelos arcos  0-1 1-2 2-3 3-4 2-4 4-2.  Diga qual o tipo — de retorno, de avanço, ou cruzado — de cada arco.
  2. Classificação dos arcos.  Escreva uma função que receba um grafo, construa uma floresta DFS do grafo (invocando GRAPHdfs()), e depois diga qual o tipo (de floresta, de avanço, de retorno, ou cruzado) de cada arco.  Quanto tempo sua função consome?
  3. Depois que a função GRAPHdfs() foi aplicada a um grafo, é possível dizer, a partir de pre[] apenas, se um dado arco v-w é de floresta, de avanço, de retorno, ou cruzado?
  4. [Sedgewick 18.4]  Aplique a função GRAPHdfs() ao grafo não-dirigido definido pelas arestas  0-2 0-5 1-2 3-4 4-5 3-5.  Exiba o rastreamento da execução da função.  (Antes de aplicar a função, escreva as listas de adjacência do grafo de modo que os vértices estejam em ordem crescente de nomes em cada lista.)  Ao percorrer um arco durante o rastreamento, anote o tipo do arco percorrido. Faça uma figura da floresta DFS.
  5. [Sedgewick 18.14, 18.15]  Aplique a função GRAPHdfs() ao grafo não-dirigido definido pelo conjunto de arestas  3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.  Exiba o rastreamento da execução da função supondo que o grafo é representado por uma matriz de adjacências.  Durante o rastreamento, anote o tipo de cada arco percorrido. Repita o exercício supondo que o grafo é representado por listas de adjacência e as listas são construídas inserindo as arestas acima, uma a uma, na ordem dada, num grafo inicialmente vazio.
  6. Arco de retorno versus ciclo.  Seja F uma floresta DFS de um grafo G. Seja pa[] o vetor o vetor de pais de F.  Escreva uma função que decida se um arco x-y de G é de retorno em relação a F. Em caso afirmativo, sua função deve imprimir a sequência de vértices de um ciclo simples que passa pelo arco x-y.  A função deve receber apenas o vetor pa[] e os vértices xy (além de uma representação de G).

Exercícios 4

Nos exercícios a seguir, pre[] é a numeração em pré-ordem calculada pela função GRAPHdfs() e F é a correspondente floresta DFS.  Prove as seguinte propriedades:

  1. Sejam xz dois vértices do grafo tais que x é ancestral próprio de z em F. Mostre que pre[x] < pre[z].
  2. Sejam xz dois vértices do grafo tais que pre[x] < pre[z]. Mostre que x é ancestral próprio ou primo esquerdo de z em F.
  3. ★ Mostre que não existe arco v-w tal que w é primo direito de v em F. [Solução]
  4. ★ Suponha que o grafo é não-dirigido. Mostre que nenhum de seus arcos é cruzado em relação a F.
  5. Mostre que, para qualquer vértice x, o conjunto de todos os números da forma pre[z] com z descendente de x em F é um intervalo e o mínimo do intervalo é pre[x].  Essa propriedade pode ser reformulada assim: Para quaisquer dois vértices x e z, se x é ancestral próprio de z então, para cada inteiro i tal que pre[x] < i < pre[z], existe um descendente próprio y de x tal que pre[y] ≡ i.
  6. Mostre que, para quaisquer dois vértices sx, se s é primo esquerdo de x em F então todo descendente de s é primo esquerdo de todo descendente de x.  Analogamente, se s é primo direito de x então todo descendente de s é primo direito de todo descendente de x.