Algoritmos para Grafos  |  Índice

DFS e pós-ordem

A busca DFS numera os vértices de um grafo em pré-ordem.  Este capítulo mostra que a busca também pode numerar os vértices em pós-ordem.  A combinação das duas numerações é essencial para decidir eficientemente se um dado arco do grafo é de avanço, de retorno, ou cruzado em relação à floresta DFS.  A classificação dos arcos não resolve um problema específico, mas serve de pré-processamento em algoritmos eficientes para diversos problemas.

A floresta DFS é o esqueleto do grafo e os vetores pre[] e post[] mostram como os demais arcos do grafo se encaixam nesse esqueleto.  Assim, a busca DFS revela a forma, a cara, do grafo.

Sumário:

Numeração em pós-ordem

Durante uma busca em profundidade, um vértice v morre quando a execução da encarnação dfsR(G,v) de dfsR() termina.  Em outras palavras, um vértice morre depois que todos os seus vizinhos em G e todos os seus descendentes na floresta DFS forem visitados.  A ordem em que os vértices morrem é conhecida como pós-ordem (= postorder).

(Se o grafo for uma árvore radicada, a permutação dos vértices em pós-ordem pode ser descrita recursivamente:  para cada vizinho w da raiz, visite, em pós-ordem, a sub­árvore que tem raiz w; depois, visite a raiz.)

A seguinte extensão das funções GRAPHdfs() e dfsR() numera os vértices em pós-ordem.  A numeração é registrada num vetor  post[]  indexado pelos vértices:  o m-ésimo vértice a morrer recebe número m .

static int cnt, pre[1000];
static int cntt, post[1000];
static vertex pa[1000];
void GRAPHdfs( Graph G) 
{ 
   cnt = cntt = 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; 
         dfsR( G, v);
      }
}
static void dfsR( Graph G, vertex v) 
{ 
   pre[v] = cnt++; 
   for (link a = G->adj[v]; a != NULL; a = a->next)
      if (pre[a->w] == -1) {
         pa[a->w] = v;
         dfsR( G, a->w); 
      } 
   post[v] = cntt++; // numeração em pós-ordem
}

A variável cnt conta as descobertas de vértices e fornece os valores de pre[]. A variável cntt conta as mortes de vértices e fornece os valores de post[].

Exemplo A. Considere a aplicação da função GRAPHdfs() ao grafo G definidos pelas seguintes listas de adjacência:

figs/mine/rtree6d.png
0: 4 5
1: 0
2: 
3: 
4: 2 3
5: 

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

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

As linhas que começam com v-w registram o momento em que a função dfsR() percorre o arco v-w. As linhas que têm um vértice v e nada mais representam o instante em que o vértice v morre. O rastreamento mostra que os vértices morrem na ordem  2 3 4 5 0 1.  Essa é, então, a permutação dos vértices em pós-ordem.  Portanto, o vetor post[] termina no seguinte estado:

     v    2  3  4  5  0  1
post[v]   0  1  2  3  4  5

Se preferir, você pode reescrever essa tabela de modo que a primeira linha fique na ordem crescente usual:

     v    0  1  2  3  4  5
post[v]   4  5  0  1  2  3

Exemplo B. Considere a aplicação da função GRAPHdfs() ao grafo G definidos 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

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

figs/mine/diwheel6-j20-gray.png
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              

O rastreamento mostra que  1 5 4 0 3 2  é a permutação dos vértices em pós-ordem (veja as linhas em que aparece apenas o nome de um vértice).  Segue daí o estado final do vetor post[]:

     v   1  5  4  0  3  2
post[v]  0  1  2  3  4  5

Para completar o exemplo, veja o estado final dos vetores pre[]pa[]:

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

(Atenção! Os conjuntos de valores dos dois vetores são diferentes: enquanto pre[] é um vetor de números inteiros, pa[] é um vetor de vértices.)  Para resumir, veja uma figura da floresta DFS com o par  pre/post  escrito abaixo do nome de cada vértice:

Exercícios 1

  1. Caminho.  Aplique a função GRAPHdfs() ao grafo-caminho  0-1 1-2 2-3 3-4 4-5 5-6.  Observe o vetor post[] produzido.  Repita o exercício da seguinte maneira: para escolher o vértice inicial de cada nova etapa da busca, examine os vértices em alguma ordem diferente da ordem 0, 1, 2, 3, etc. usual. Qual o efeito sobre o vetor post[]?
  2. ★ Aplique a função GRAPHdfs() ao grafo definido pelos arcos  0-1 1-2 2-3 3-4 4-5 4-6 . Observe o vetor post[] produzido. Repita o exercício da seguinte maneira: para escolher o vértice inicial de cada nova etapa da busca, examine os vértices em alguma ordem diferente da ordem 0, 1, 2, 3, etc. usual. Qual o efeito sobre o vetor post[]?
  3. ★ Faça uma figura do grafo definido pelos arcos  a-c a-d a-g b-d b-f g-c d-c d-e f-e c-h e-h.  Faça uma busca DFS percorrendo os vértices em ordem alfabética de nomes. Suponha que o grafo é representado por listas de adjacência e que as listas estão em ordem alfabética e escreva as listas de modo que os vértices estejam em ordem crescente de nomes em cada lista.  Deduza do resultado da busca a permutação dos vértices em pós-ordem.
  4. Calcule numerações em pré- e pós-ordem do grafo da figura.
  5. É verdade que a permutação dos vértices em pós-ordem é o inverso da numeração em pré-ordem?  Justifique sua resposta.
  6. Depois de uma busca DFS num grafo, suponha que v-w é um arco da floresta DFS. É verdade que pre[w] = pre[v]+1?

Intervalos de vida

Cada encarnação de dfsR() permanece em execução durante um certo intervalo de tempo. A encarnação dfsR(G,v) permanece em execução entre o instante em que v é descoberto e o instante em que v morre.  Diremos que esse é o intervalo de vida (= lifespan) da encarnação.  É claro que o início do intervalo corresponde a pre[v] e o fim do intervalo corresponde a post[v]. (Mas não faz sentido dizer que o intervalo é (pre[v],post[v]) pois as duas numerações são independentes!)

Em virtude do caráter recursivo de dfsR(), a coleção dos intervalos de vida de todas as encarnações de dfsR() é bem organizada no seguinte sentido: cada dois intervalos são disjuntos ou estão encaixados.  (Veja exercícios no capítulo Florestas DFS.)  Em outras palavras, se um vértice x é descoberto antes de um vértice y então morre antes que y seja descoberto (caso de intervalos disjuntos) ou morre depois que y tenha morrido (caso de intervalos encaixados).

Os exemplos a seguir mostram que é fácil observar os intervalos de vida das várias encarnações de dfsR() no rastreamento da execução de GRAPHdfs().

Exemplo C. Considere novamente o rastreamento da execução de GRAPHdfs( G) no exemplo B.  Veja a indicação dos intervalos de vida das várias encarnações de dfsR();

figs/mine/diwheel6-j20-gray.png
0 dfsR(G,0)       0
0-1 dfsR(G,1)     0 1
  1               0 1
0-4 dfsR(G,4)     0  4
  4-1             0  4
  4-5 dfsR(G,5)   0  4 5
    5-1           0  4 5
    5             0  4 5
  4               0  4 
0                 0
                          
2 dfsR(G,2)       2  
2-0               2
2-3 dfsR(G,3)     2 3
  3-4             2 3
  3-5             2 3
  3               2 3
2-4               2
2                 2

Essa coleção de intervalos pode ser representada por uma permutação dupla dos vértices, isto é, uma sequência em que cada vértice aparece exatamente duas vezes. A primeira aparição de um vértice x corresponde ao início da execução de dfsR(G,x) e a segunda corresponde ao fim da execução de dfsR(G,x):

   0      2 
  / \     | 
 1   4    3 
      \     
       5    
0 1 1 4 5 5 4 0  2 3 3 2
( ( ) ( ( ) ) )  ( ( ) )

Cada dois intervalos nessa permutação dupla estão encaixados (como 54, por exemplo) ou são disjuntos (como 41, por exemplo).  Essa expressão de parênteses nada mais é que uma representação alternativa da floresta DFS.

Exemplo D. Aplique a função GRAPHdfs() ao grafo 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

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

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                      

O rastreamento mostra que a permutação dos vértices em pós-ordem é  4 0 6 3 1 5 7 2.  Portanto, post[] termina no seguinte estado:

     v    4  0  6  3  1  5  7  2
post[v]   0  1  2  3  4  5  6  7  

Para completar o exemplo, veja o estado final de pre[]pa[] (com os vértices listados na ordem em que foram descobertos):

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

O rastreamento também mostra a relação entre os intervalos de vida das várias encarnações de dfsR():

2 7 5 4 4 1 3 6 0 0 6 3 1 5 7 2
( ( ( ( ) ( ( ( ( ) ) ) ) ) ) ) 

Exemplo E. Considere 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

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

figs/Sedgewick-Wayne/DFSTinyPath-x.png
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                    

O rastreamento mostra que a permutação dos vértices em pós-ordem é  1 5 4 3 2 0.  Portanto, post[] termina no seguinte estado:

     v    1  5  4  3  2  0
post[v]   0  1  2  3  4  5

O estado final de pre[] e pa[] é o seguinte:

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

Veja também a relação entre os intervalos de vida das várias encarnações de dfsR():

0 2 1 1 3 5 5 4 4 3 2 0
( ( ( ) ( ( ) ( ) ) ) )

Exercícios 2

  1. Aplique a função GRAPHdfs() ao grafo não-dirigido da figura. Suponha que o grafo está representado por sua matriz de adjacências. Qual a permutação dos vértices em pós-ordem?  Qual o estado final dos vetores pre[], post[]pa[]?  Qual a estrutura da coleção de intervalos de vida das encarnações de dfsR()?
  2. Aplique o algoritmo de busca DFS ao grafo definido pelas seguintes listas de adjacência:
    0: 5 7
    1: 5
    2: 1
    3: 4 6
    4: 0 7
    5: 2 7
    6: 3 4
    7: 1
    
  3. Pré-ordem e pós-ordem intercaladas.  É interessante intercalar as permutações em pré- e pós-ordem.  Para isso, basta trocar a variável cntt por cnt, ou seja, usar o mesmo contador para registrar as descobertas e as mortes dos vértices.  Desenvolva essa ideia.  Quais as propriedades da permutação dupla de vértices correspondente à intercalação de pre[]post[]?  [Solução]

Ancestrais, descendentes, e primos

As relações genealógicas (ancestral, descendente, primo) entre os vértices de uma floresta DFS podem ser formuladas em termos dos intervalos de vida das várias encarnações de dfsR().  Um vértice x é ancestral de um vértice y se o intervalo de dfsR(G,x) contém o intervalo de dfsR(G,y)x é descendente de y se o intervalo de dfsR(G,x) está contido no intervalo de dfsR(G,y)x é primo esquerdo de y se o intervalo de dfsR(G,x) vem antes do de dfsR(G,y)x é primo direito de y se o intervalo de dfsR(G,x) vem depois do de dfsR(G,y)

Segue daí que a combinação das numerações em pré- e pós-ordem permite decidir, em tempo constante, a relação entre dois vértices da floresta DFS:  para quaisquer dois vértices xy,

x é ancestral próprio de y
se e somente se  pre[x] < pre[y]  e  post[x] > post[y];
x é descendente próprio de y
se e somente se  pre[x] > pre[y]  e  post[x] < post[y];
x é primo esquerdo de y
se e somente se  pre[x] < pre[y]  e  post[x] < post[y];
x é primo direito de y
se e somente se  pre[x] > pre[y]  e  post[x] > post[y].

A combinação de pre[] e post[] também permite decidir, em tempo constante, a classe de cada arco em relação à floresta DFS:  para qualquer arco v-w que não pertence à floresta,

v-w é de avanço
se e somente se  pre[v] < pre[w]  e  post[v] > post[w];
v-w é de retorno
se e somente se  pre[v] > pre[w]  e  post[v] < post[w];
v-w é cruzado
se e somente se  pre[v] > pre[w]  e  post[v] > post[w].

Note que se v-w é um arco cruzado então w é primo esquerdo de v. (Veja observação no capítulo Florestas DFS.)  A seguinte tabela resume a caracterização dos arcos:

     pre  post
     v w  v w
      <    >   floresta
      <    >   avanço
      >    <   retorno
      >    >   cruzado

Grafos não-dirigidos não têm arcos cruzados. (Veja observação no capítulo Florestas DFS.)  Portanto, para qualquer arco v-w de um grafo não-dirigido,

Exemplo F. Considere novamente o exemplo E. Veja os vetores pre[] e post[] daquele exemplo:

figs/Sedgewick-Wayne/DFSTinyPath-x.png
     v    0  1  2  3  4  5
 pre[v]   0  2  1  3  5  4
post[v]   5  0  4  3  2  1

(Desta vez os índices v estão listados em ordem crescente.)  A partir desse par de vetores, obtemos a classificação dos arcos que não pertencem à floresta DFS:

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
5-0 retorno
5-3 retorno

Exemplo G. Considere novamente o exemplo D. Veja os vetores pre[] e post[] daquele exemplo:

     v    0  1  2  3  4  5  6  7
 pre[v]   7  4  0  5  3  2  6  1
post[v]   1  4  7  3  0  5  2  6

Deduzimos daí a classificação dos arcos que não pertencem à floresta:

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

Essa classificação também poderia ser produzida em tempo real (= on-the-fly), ou seja, no momento em que cada arco é percorrido:

2 dfsR(G,2)                  
2-7 dfsR(G,7)                
  7-5 dfsR(G,5)              
    5-4 dfsR(G,4)            
      4-5 retorno
      4-7 retorno
      4                 
    5-1 dfsR(G,1)             
      1-3 dfsR(G,3)           
        3-6 dfsR(G,6)         
          6-0 dfsR(G,0)     
            0-2 retorno
            0-4 cruzado 
            0           
          6-2 retorno
          6-4 cruzado
          6             
        3               
      1                 
    5-7 retorno         
    5                   
  7-3 avanço            
  7                     
2                       

Não é óbvio como produzir essa classificação em tempo real. A dificuldade está em decidir a classe de um arco v-w antes que os valores de pre[v], pre[w], post[v] e post[w] sejam todos conhecidos. Veja exercício abaixo.

Exercícios 3

  1. Prove que está correta a caracterização da posição relativa de dois vértices na floresta DFS dada acima.
  2. Pré-ordem, pós-ordem, e a floresta DFS. Seja F uma floresta DFS de um grafo G e sejam pre[] e post[] as correspondentes numerações dos vértices de G em pre- e pós-ordem respectivamente. Mostre que pre[] é uma numeração topológica dos vértices de F. Mostre que post[] é uma numeração anti-topológica dos vértices de F.
  3. Um grafo G tem vértices  0 1 2 3 4 5. Uma busca em profundidade produz os vetores exibidos a seguir.  Se 3-4 0-5 5-3 4-1 são arcos de G, a qual classe cada um desses arcos pertence?
         v     0  1  2  3  4  5
     pre[v]    0  1  5  4  2  3
    post[v]    4  0  5  1  3  2
    
  4. Considere o grafo não-dirigido definido pelas arestas  0-2 2-6 6-4 4-5 5-0 0-7 7-1 7-4 3-4 3-5.  Suponha que o grafo é representado por uma matriz de adjacências. Submeta o grafo à função GRAPHdfs() e faça o rastreamento da execução da função e a correspondente classificação dos arcos.
  5. [Sedgewick 18.16] Classificação on-the-fly dos arcos.  Escreva uma função eficiente que imprima o rastro da execução de uma busca DFS no estilo da segunda parte do exemplo G.  Teste sua função. Use um sub­grafo aleatório da grade quadrada para os testes.   Para depurar/validar a função (ou seja, para conferir a correção dos resultados) escreva código para verificar explicitamente (1) se w é ancestral de v para cada arco de retorno v-w, (2) se w é descendente de v para cada arco de avanço v-w, e (3) se nenhuma dessas alternativas vale no caso de arco cruzado.
  6. Reconhecimento de floresta radicada.  Escreva uma função que decida se um dado grafo G é uma árvore radicada. Em caso afirmativo, a função deve deixar no vetor pre[] uma numeração topológica de G.  (Sugestão: Faça uma busca em profundidade e veja a classificação dos arcos.)
  7. Atualize sua biblioteca.  Acrescente as funções GRAPHdfs() e dfsR() à biblioteca GRAPHlists que mencionamos no capítulo Estruturas de dados para grafos.  Aloque os vetores pre[], post[] e pa[] dinamicamente.  Repita o exercício para a biblioteca GRAPHmatrix.