Algoritmos para Grafos  |  Índice

Caminhos baratos em dags

Este capítulo trata de um algoritmo muito eficiente para o problema da árvore de caminhos de custo mínimo restrito a dags (grafos acíclicos). Uma introdução geral ao problema foi dada no capítulo Caminhos de custo mínimo.

Problema da CPT em dags:  Dado um vértice s de um dag G com custos arbitrários (positivos e negativos) nos arcos, encontrar uma árvore de caminhos baratos em G que tenha raiz s.

Vale lembrar que uma árvore de caminhos baratos, ou CPT (= cheapest-paths tree), é uma sub­árvore radicada geradora T de G tal que todo caminho em T que começa na raiz tem custo mínimo em G.  Como observamos no capítulo introdutório, um dag tem uma CPT com raiz s se e somente se todos os seus vértices estão ao alcance de s.  Para escamotear essa condição, podemos trabalhar com o sub­grafo induzido pelos vértices que estão ao alcance de s.

Sumário:

topsortDAGk-x.png

Algoritmo

Como se sabe, um grafo é um dag se e somente se admite uma permutação topológica do seu conjunto de vértices.  A permutação topológica permite processar o dag de modo simples e eficiente.

Ao receber um dag G com custos nos arcos e um vértice s, o algoritmo deveria produzir uma CPT de G com raiz s ou informar que uma tal CPT não existe.  Para simplificar as coisas, tomaremos duas decisões de projeto: (1) suporemos que todos os vértices de G estão ao alcance de s e (2) suporemos dada uma permutação topológica, digamos vv[0..V-1], dos vértices.  Em virtude de (1), temos vv[0] ≡ s.

A função DAGcpt() a seguir é uma generalização óbvia de DAGspt() do capítulo Caminhos de comprimento mínimo.  Suporemos que o dag G é representado por listas de adjacência com custos.  Assim, para cada vértice v e cada a em G->adj[v], o arco que vai de v a a->w tem custo  a->c.

#define Dag Graph
/* Esta função recebe um dag G 
   com custos nos arcos,
   uma permutação topológica vv[] 
   de G,
   e um vértice s de G.
   Supõe que todos os vértices estão ao alcance de s.
   A função calcula e armazena em pa[]
   o vetor de pais de uma CPT de G com raiz s.
   As distâncias a partir de s
   são armazenadas no vetor dist[].

   (O código foi inspirado no programa 21.6
   de Sedgewick.) */
void DAGcpt( Dag G, vertex *vv, vertex s,
             vertex *pa, int *dist) 
{ 
   for (vertex v = 0; v < G->V; ++v)
      pa[v] = -1, dist[v] = INT_MAX;

   pa[s] = s, dist[s] = 0;
   for (int j = 0; j < G->V; ++j) {
      vertex v = vv[j];
      if (dist[v] == INT_MAX) continue;
      for (link a = G->adj[v]; a != NULL; a = a->next) {
         if (dist[v] + a->c < dist[a->w]) {
            dist[a->w] = dist[v] + a->c; 
            pa[a->w] = v;
         }
      }
   }
}

Algumas observações podem ajudar a entender o código:

A função supõe que todos os vértices estão ao alcance de s. Mas não é necessário testar essa condição antes de invocar a função: Se a condição não estiver satisfeita, a função produzirá uma CPT no sub­grafo induzido pelo conjunto de vértices que estão ao alcance de s.

figs/Sedgewick-part5-java/flow-fig-22-6-a.png

Exemplo A.  Considere o dag definidos pelos arcos e custos abaixo.  Observe que a sequência  0 1 2 3 4 5  é uma permutação topológica dos vértices.

0-1 0-2 1-3 1-4 2-3 2-4 3-5 4-5
 10  10  10  10  10  10  20  10

Queremos calcular uma CPT com raiz 0.  Invoque a função DAGcpt() com parâmetro s igual a 0. Veja o estado dos vetores dist[] e pa[] no início de sucessivas iterações (com * representando INT_MAX):

           dist[]               pa[]       
j vv[j]    0  1  2  3  4  5     0 1 2 3 4 5
                                           
0   0      0  *  *  *  *  *     0 - - - - -
1   1      0 10 10  *  *  *     0 0 0 - - -
2   2      0 10 10 20 20  *     0 0 0 1 1 -
3   3      0 10 10 20 20  *     0 0 0 1 1 -
4   4      0 10 10 20 20 40     0 0 0 1 1 3
5   5      0 10 10 20 20 30     0 0 0 1 1 4
6          0 10 10 20 20 30     0 0 0 1 1 4

Agora, pa[] representa uma CPT com raiz 0.

figs/Sedgewick-part5-java/flow-fig-22-6-a.png

Exemplo B.  Considere o dag definidos pelos arcos e custos abaixo.  Observe que a sequência  0 1 2 3 4 5  é uma permutação topológica dos vértices.

0-1 0-2 1-3 1-4 2-3 2-4 3-5 4-5
 10  10  10  10  10  10  20  10

Queremos calcular as distâncias a partir do vértice 2, embora nem todos os vértices estejam ao alcance de 2.  Invoque a função DAGcpt() com parâmetro s igual a 2 e veja o estado dos vetores dist[] e pa[] no início de sucessivas iterações (com * representando INT_MAX):

           dist[]               pa[]       
j vv[j]    0  1  2  3  4  5     0 1 2 3 4 5
                                           
0   0      *  *  0  *  *  *     - - 2 - - -
1   1      *  *  0  *  *  *     - - 2 - - -
2   2      *  *  0  *  *  *     - - 2 - - -
3   3      *  *  0 10 10  *     - - 2 2 2 -
4   4      *  *  0 10 10 30     - - 2 2 2 3
5   5      *  *  0 10 10 20     - - 2 2 2 4
6          *  *  0 10 10 20     - - 2 2 2 4

Agora, pa[] representa uma CPT com raiz 2 no sub­grafo induzido pelo conjunto de vértices que estão ao alcance de 2.

Exercícios 1

  1. ★ Calcule uma CPT com raiz 0 no dag com custos nos arcos especificado a seguir. Adote a permutação topológica  0 1 2 3.
    0-1  0-2  1-3  2-3
     11   21   31   18  
    
  2. A linha  if (dist[v] == INT_MAX) continue  é realmente necessária no código de DAGcpt()?
  3. Calcule uma CPT com raiz 0 no dag com custos nos arcos descrito a seguir. Use a permutação topológica  0 3 2 4 5 1 .
    0-2  0-4  0-3  3-4  3-5  2-1  2-4  4-1  4-5  5-1
     20   30   40  -20   10   10  -10    0   10   20
    
  4. Calcule uma CPT com raiz 2 no sub­grafo induzido pelo conjunto dos vértices que estão ao alcance de 2. Os custos dos arcos são dados a seguir.
    0-1 0-5 0-6 2-0 2-3 3-5 5-4 6-4 6-9 7-6 8-7 9-10 9-11 9-12 11-12 
     10  30  20  10  20  30  50  50  10  10  10   10   10   10    10
    

Análise

Para verificar que a função DAGcpt() está correta, basta observar que os seguintes invariantes valem no início de cada iteração:

  1. o vetor pa[] define uma árvore radicada, digamos T, com raiz s;
  2. para todo vértice x de G, dist[x] é a distância de s a x em T;
  3. para todo arco x-y de G, se x está em vv[0..j-1] então x-y está relaxado em relação a dist[].

(Segue imediatamente do invariante ii que dist[x] vale INT_MAX se e somente se x não está em T.)

Agora considere a última iteração, que começa com j ≡ G->V e termina imediatamente.  Em virtude do invariante iii, dist[] é um potencial relaxado em G.  Em virtude dos invariantes i e ii, T é uma árvore radicada em G e dist[] é o vetor das distâncias em T a partir de s.

Resta apenas provar que T é geradora.  Para isso, basta mostrar que não existe arco de G com ponta inicial em T e ponta final fora de T.  Tome um arco qualquer x-y de G tal que x pertence a T. Como x-y está relaxado, dist[y] ≤ dist[x] + 1. Graças ao invariante ii, dist[x] < INT_MAX. Segue daí que dist[y] < INT_MAX e portanto y está em T. Isso mostra que T contém todos os vértices que estão ao alcance de s em G, e portanto todos os vértices de G.  Concluímos assim que os caminhos em T a partir de s satisfazem a condição suficiente de minimalidade (veja a seção Potencial e condições de minimalidade do capítulo Caminhos de custo mínimo). Portanto T é uma CPT de G.

Desempenho.  A função DAGcpt() examina cada arco do dag uma única vez.  Assim, o consumo de tempo é proporcional a  V + A  no pior caso, sendo V o número de vértices e A o número de arcos do dag.  Remumindo: a função DAGcpt() é linear.

Exercícios 2

  1. Invariantes.  Prove os invariantes do processo iterativo em DAGcpt().
  2. É verdade que no início de cada iteração em DAGcpt() tem-se dist[vv[i]] ≤ dist[vv[k]] para cada i ≤ j e cada k ≥ j?
  3. Considere o dag definido pelos arcos e custos abaixo.  Observe que a sequência  5 1 3 6 4 7 0 2  é uma permutação topológica dos vértices.  Verifique que a árvore radicada da figura é uma CPT. (Use a condição suficiente para CPT.)
    5-4 4-7 5-7 5-1 4-0 0-2 3-7 1-3 7-2 6-2 3-6 6-0 6-4
     35  37  28  32  38  26  39  29  34  40  52  58  93
    
  4. ★ Escreva uma variante simplificada da função DAGcpt() que receba dois vértices st de um dag G e uma permutação topológica vv[] e devolva a distância — apenas a distância — de st. Escreva código enxuto, sem variáveis e operações supérfluas.
  5. Escreva uma variante da função DAGcpt() sem o parâmetro vv[]. A função deve calcular uma permutação topológica de G ao mesmo tempo em que calcula as distâncias. (Veja o algoritmo de eliminação iterada de fontes.)
  6. Versão recursiva.  Escreva uma função recursiva DAGcptR() que calcule um caminho mínimo de um vértice s a um vértice t de um dag com custos nos arcos.