Algoritmos para Grafos  |  Índice

Algoritmo de caminhos mínimos

O capítulo anterior fez uma introdução geral ao problema do caminho de comprimento mínimo e mostrou que ele é essencialmente equivalente ao problema da árvore de caminhos curtos.  Aquele capítulo também mostrou um algoritmo para a restrição do problema a grafos acíclicos.  O presente capítulo trata de um algoritmo geral, para grafos arbitrários.

Sumário:

spt-from-5.png

O algoritmo

Como já dissemos no capítulo Caminhos de comprimento mínimo, uma árvore de caminhos curtos, ou SPT (= shortest-paths tree), de um grafo G é uma subárvore radicada geradora T de G tal que todo caminho em T a partir da raiz é mínimo em G.

Problema da SPT:  Dado um vértice s de um grafo G, encontrar uma SPT de G que tenha raiz s.

Para resolver o problema, basta adaptar o algoritmo de busca em largura de modo que a numeração dos vértices represente as distâncias a partir de s.  O algoritmo é iterativo. Cada iteração começa com  (1) uma árvore radicada com raiz s, representada por um vetor de pais pa[ ],  (2) um vetor dist[ ] que dá a distância de s a cada vértice da árvore, e (3) uma fila de vértices da árvore.   Cada iteração consiste no seguinte:

No começo da primeira iteração, s é o único vértice da árvore, dist[s] vale 0, e a fila contém s e nada mais.  O processo iterativo termina quando a fila fica vazia.

A fila de vértices tem um papel semelhante à da permutação topológica que guia a execução do algoritmo especial para dags (capítulo Caminhos de comprimento mínimo). Mas, diferentemente do que acontece naquele algoritmo, cada elemento do vetor dist[ ] é definido uma só vez: para cada vértice w, dist[w] é definido em alguma iteração e nunca mais alterado.

A operação  dist[w] = dist[v] + 1  é uma relaxação do arco v-w.

Exercícios 1

  1. ★ Como começa cada iteração do algoritmo dos caminhos mínimos? (Cuidado! Não se trata de descrever as ações que ocorrem no começo da iteração. Trata-se de saber que informações estão disponíveis no início de uma iteração genérica, antes que a execução da iteração comece.)
  2. Considere o grafo definido pelos arcos 0-2 0-3 0-4 1-2 1-4 2-4 3-4 3-5 4-5 5-1.  Calcule uma árvore de caminhos curtos com raiz 0.
  3. 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.  Calcule uma árvore de caminhos curtos com raiz 0.  Exiba o estado da fila e dos vetores dist[ ] e pa[ ] no início de cada iteração.

Implementação do algoritmo

A implementação do algoritmo dos caminhos mínimos é uma adaptação da função GRAPHbfs() de busca BFS.  O resultado é a função GRAPHspt(). No fim da execução da função, o vetor dist[] é um potencial relaxado, embora a função não procure arcos tensos explicitamente, como acontece na função que calcula uma SPT num dag.

/* Esta função recebe um grafo G
   e um vértice s de G
   e armazena em pa[]
   o vetor de pais de uma SPT
   do sub­grafo induzido pelos vértices 
   que estão ao alcance de s.
   A SPT tem raiz s e
   as distâncias a partir de s
   são armazenadas no vetor dist[].
   O espaço para os vetores pa[] e dist[]
   deve ser alocado pelo usuário.
   Esta implementação supõe que o grafo G 
   é representado por listas de adjacência. 
   (Código inspirado no programa 18.9 de Sedgewick.) */
void GRAPHspt( Graph G, vertex s, 
                         int *dist, vertex *pa)
{ 
   const int INFINITY = G->V;
   for (vertex v = 0; v < G->V; ++v) 
      dist[v] = INFINITY, pa[v] = -1;
   dist[s] = 0, pa[s] = s;
   QUEUEinit( G->V);
   QUEUEput( s); 

   while (!QUEUEempty( )) {
      vertex v = QUEUEget( ); 
      for (link a = G->adj[v]; a != NULL; a = a->next) {
         vertex w = a->w; 
         if (dist[w] == INFINITY) {
            dist[w] = dist[v] + 1; 
            pa[w] = v;
            QUEUEput( w); 
         }
      }
   }
   QUEUEfree( ); 
}

A constante INFINITY é uma boa representação de ∞ pois seu valor é maior que o comprimento de qualquer caminho no grafo.  Se um vértice v não estiver ao alcance de s, teremos dist[v] ≡ INFINITY até o fim da execução da função.

As funções auxiliares que manipulam a fila — QUEUEget(), QUEUEput(), etc. — são as mesmas que já usamos na função de busca em largura.  A fila foi dimensionada corretamente na linha QUEUEinit( G->V) pois cada vértice de G entra na fila no máximo uma vez.

Desempenho.  A análise do desempenho de GRAPHspt() é idêntica à análise do desempenho da função GRAPHbfs() de busca em largura. Se o grafo tem V vértices e A arcos, a função consome  V + A  unidades de tempo no pior caso.  Esse tempo é proporcional ao tamanho do grafo e portanto podemos dizer que a função é linear.   A variante de GRAPHspt() para grafos representados por matriz de adjacências consome tempo proporcional a V2 no pior caso.  Se nos restrigirmos a grafos densos, esse consumo é considerado linear.

figs/mine/diwheel6-k-gray.png

Exemplo A.  Considere o grafo definido pelos arcos 0-2 0-3 0-4 1-2 1-4 2-4 3-4 3-5 4-5 5-1 .  Submeta o grafo à função GRAPHspt() com segundo argumento 0.  Veja o estado da fila, e o estado dos vetores dist[] e pa[] (com * no lugar de INFINITY e - no lugar de -1) no início de cada iteração:

queue[]        dist[]         pa[]          
               0 1 2 3 4 5    0 1 2 3 4 5
0              0 * * * * *    0 - - - - -
  2 3 4        0 * 1 1 1 *    0 - 0 0 0 -
    3 4        0 * 1 1 1 *    0 - 0 0 0 -
      4 5      0 * 1 1 1 2    0 - 0 0 0 3
        5      0 * 1 1 1 2    0 - 0 0 0 3
          1    0 3 1 1 1 2    0 5 0 0 0 3
               0 3 1 1 1 2    0 5 0 0 0 3

(Estou supondo que, em cada lista de adjacência, os vértices aparecem em ordem crescente de nomes.)

figs/Sedgewick-Wayne/GraphRepDFSex-tinyCG-x.png

Exemplo B.  Seja G o grafo não-dirigido definido pelas arestas  0-1 0-2 0-5 2-1 2-3 2-4 3-4 3-5.  Submeta G à função GRAPHspt() com segundo argumento 0.  Veja o estado da fila, e o estado dos vetores dist[] e pa[] no início de cada iteração:

queue[]        dist[]         pa[]          
               0 1 2 3 4 5    0 1 2 3 4 5
0              0 * * * * *    0 - - - - -
  1 2 5        0 1 1 * * 1    0 0 0 - - 0
    2 5        0 1 1 * * 1    0 0 0 - - 0
      5 3 4    0 1 1 2 2 1    0 0 0 2 2 0
        3 4    0 1 1 2 2 1    0 0 0 2 2 0
          4    0 1 1 2 2 1    0 0 0 2 2 0
               0 1 1 2 2 1    0 0 0 2 2 0

(Estou supondo que, em cada lista de adjacência, os vértices aparecem em ordem crescente de nomes.)  Veja os valores de dist[] nos vértices da fila no início de sucessivas iterações (compare com o exercício Propriedades da fila abaixo):

figs/Sedgewick-Wayne/BFSTrace6-x.png
dist[queue[]]

0              
  1 1 1        
    1 1        
      1 2 2    
        2 2    
          2    

Exercícios 2

  1. Instâncias extremas.  A função GRAPHspt() produz o efeito desejado se G tem apenas 1 vértice?  E se G não tem arcos?  E se G é uma árvore radicada com 2 vértices?  E se G é uma árvore radicada com 3 vértices?
  2. No código da função GRAPHspt(), não deveríamos escrever if (dist[v] + 1 < dist[w]) em lugar de if (dist[w] == INFINITY)?
  3. Considere o grafo não-dirigido definido pelas arestas  8-9 3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.  Suponha que o grafo é representado por matriz de adjacência. Calcule a distância do vértice 0 a cada um dos demais.  Faça uma figura da árvore de caminhos curtos.   Repita o exercício supondo que o grafo é representado por listas de adjacência construídas inserindo as arestas, na ordem dada, num grafo inicialmente vazio.
  4. A figura mostra o grafo do cavalo 4-por-4:  os vértices são as casas do tabuleiro de xadrez 4-por-4 e dois vértices são adjacentes se um cavalo do jogo de xadrez pode saltar de um deles para o outro em um só movimento.  Qual a distância do vértice (2,1) ao vértice (2,4)? Qual a distância do vértice (1,1) ao vértice (1,4)?
  5. Código compacto.  Escreva uma versão de GRAPHspt() que incorpore o código das funções de manipulação da fila. (A fila deve ser implementada em um vetor.) Escreva código simples, sem variáveis e instruções supérfluas.
  6. Distâncias na árvore.  Seja T a árvore radicada representada pelo vetor pa[] no código de GRAPHspt().  Mostre que  dist[v]+1 ≡ dist[w]  para cada arco v-w de T.  Deduza daí que, para cada w, dist[w] é o comprimento do único caminho de sw em T.
  7. Propriedades da fila.  Seja v, q1, … , qk a sequência dos vértices que estão na fila no início de uma iteração qualquer de GRAPHspt().  Adote a abreviatura temporária d para dist e mostre que

    d[v]  ≤  d[q1] ≤ ... ≤ d[qk]  ≤  d[v]+1.

    Agora considere a árvore radicada T representada pelo vetor pa[] e mostre que d[u] ≤ d[v] para cada u em T que não está na fila.  (Essas propriedades são usadas na prova da correção do algoritmo.)

  8. Caminhos mínimos em grafos não-dirigidos.  Escreva uma função que calcule caminhos mínimos a partir de um dado vértice s em um grafo não-dirigido.  Seu código é mais simples que o de GRAPHspt()?
  9. ★ Depois de executar a função GRAPHspt() com argumentos Gs, seja  X  o conjunto dos vértices x para os quais pa[x] ≠ -1.  Descreva o leque de saída de X, ou seja, o conjunto de arcos que têm ponta inicial em X e ponta final fora de X.
  10. Suponha dado o vetor pa[] que representa uma árvore de caminhos curtos de um grafo G.  Escreva uma função que receba um vértice t e devolva a distância em G da raiz da árvore até t. Se t estiver na árvore, imprima o caminho na árvore da raiz até t.
  11. Matriz de adjacências.  Adapte a função GRAPHspt() a grafos representados por matriz de adjacências.
  12. Atualize sua biblioteca.  Acrescente a função GRAPHspt() à biblioteca GRAPHlists mencionada no capítulo Estruturas de dados para grafos.  Repita o exercício para a biblioteca GRAPHmatrix.

Por que o algoritmo está correto?

Para provar que a função GRAPHspt() está correta, vamos recorrer à condição suficiente de minimalidade descrita no capítulo Caminhos de comprimento mínimo. O vetor dist[] fará o papel de potencial relaxado. Vale lembrar que um arco v-w está relaxado se dist[v] + 1 ≥ dist[w].

Seja T a árvore radicada representada por pa[]. Com essa notação, as seguintes propriedades valem no início de cada iteração, sendo pois invariantes:

  1. todo arco com ambas as pontas em T está relaxado em relação a dist[] e
  2. para cada arco x-y, se x está em T e y está fora de T então x está na fila.

Provar os invariantes 1 e 2 é um excelente exercício. Note que o invariante 1 não é óbvio, uma vez que o algoritmo não verifica explicitamente a relaxação de cada arco, como faz DAGspt().

Agora que temos os invariantes, podemos analisar a última iteração, que começa com a fila vazia.  Para simplificar ligeiramente a discussão, vamos supor que todos os vértices do grafo estão ao alcance de s.  A análise se reduz a três observações:

Portanto, no fim da execução do algoritmo, T é uma árvore de caminhos curtos com raiz s, como já havíamos antecipado.  Ademais, para cada vértice t de G, o número dist[t] é a distância de st em G.  Isso prova que GRAPHspt() está correto.

Exercícios 3

  1. Prova dos invariantes.  Prove os invariantes 1 e 2 do processo iterativo na função GRAPHspt().  As provas dependem das propriedades da fila discutidas num exercício acima. Também dependem das propriedades discutidos no exercício Distâncias na árvore
  2. Deduza dos invariantes do processo iterativo na função GRAPHspt() que no início de cada iteração, para cada vértice t na árvore T, a distância de st em G é exatamente dist[t].
  3. Digamos que dist[] é o potencial relaxado produzido pela aplicação da função GRAPHspt() a um grafo G com vértice inicial s.  Podemos concluir que um caminho mínimo de um vértice r a um vértice t tem comprimento dist[t] - dist[r]?  (Veja a condição suficiente de minimalidade no capítulo Caminhos de comprimento mínimo.)

Exercícios 4

  1. Caminhos mínimos a partir de um conjunto.  Generalize o código de GRAPHspt() para resolver o seguinte problema:  dado um conjunto não vazio S de vértices, determinar, para cada vértice t do grafo, um caminho de comprimento mínimo dentre os que começam em S e terminam em t.  (O resultado desse exercício é usado, por exemplo, no algoritmo dos caminhos aumentadores para o problema do emparelhamento máximo.)
  2. Ciclo mínimo.  Escreva uma função que calcule um ciclo de comprimento mínimo em um grafo (ou diga que o grafo é acíclico).
  3. Grafos que mudam com o tempo.  Sejam dist[] e pa[] os vetores calculados pela função GRAPHspt() com argumentos Gs.  (1) Se um arco v-w for removido, dist[] continua sendo o vetor das distâncias do grafo?  (2) Se a direção de um arco v-w for invertida (ou seja, v-w for trocado por w-v), dist[] continua sendo o vetor das distâncias do grafo?  (3) Se um novo arco x-y for inserido no grafo, dist[] continua sendo o vetor das distâncias do grafo?  Dê algoritmos eficientes para responder essas perguntas.
  4. [Sedgewick 18.55]  Diâmetro de grafo não-dirigido.  Denote por d(s,t) a distância de um vértice s a um vértice t de um grafo não-dirigido.  O diâmetro do grafo é o valor máximo da expressão  d(s,t)  com s e t variando no conjunto de todos os vértices.  Escreva uma função UGRAPHdiameter() que calcule o diâmetro de um grafo não-dirigido.
  5. Vértices centrais. Um vértice s de um grafo não-dirigido é central se minimiza maxt d(s,t), sendo d(s,t) a distância de st.  Escreva uma função que devolva um vértice central de qualquer grafo não-dirigido conexo.
  6. Centros de árvores. Mostre que toda árvore tem no máximo dois vértices centrais, e se tiver dois então eles são adjacentes.  Escreva uma função que calcule todos os vértices centrais de uma árvore dada.
  7. Fenômeno small world.  Estude a evolução da distância média entre dois vértices diferentes em grafos não-dirigidos aleatórios com 10000 vértices em função do número de arestas. Repita o exercício com número maior de vértices. (A propósito, veja a pergunta What's the significance of the cliché six degrees of separation? no Quora.)
  8. Fenômeno small world.  Considere grafos não-dirigido aleatórios construídos da seguinte maneira:  comece com um grafo não-dirigido G produzido pela função UGRAPHclosePoints(); depois, para cada vértice v de G, acrescente k arestas do tipo v-w sendo w escolhido aleatoriamente dentre todos os demais vértices de G.  Digamos, para efeito deste exercício, que grafos não-dirigidos assim construídos são do tipo WS (referência a Watts e Strogatz).  Escreva uma função UGRAPHsmallWorld() que calcule a distância média entre dois vértices distintos de um grafo do tipo WS. Se o grafo for desconexo, a distância média será infinita. (A propósito, veja a pergunta What's the significance of the cliché six degrees of separation? no Quora.)

Perguntas e respostas