Algoritmos para Grafos, via Sedgewick  |  Índice remissivo

"Computer science is no more about computers
than astronomy is about telescopes."

Edsger W. Dijkstra

Algoritmo de Dijkstra

Esta página discute um algoritmo eficiente para o problema da SPT com custos positivos nos arcos que já foi mencionado em outra página:

Problema da SPT com Custos Positivos:  Dado um vértice s de um digrafo com custos positivos nos arcos, encontrar uma SPT com raiz s no digrafo.

Um excelente algoritmo para o problema foi descoberto por Dijkstra [pronuncie algo entre "Dêcstra" e "Dêicstra"] e publicado em 1959.  O algoritmo resolve uma ligeira generalização do problema: ele aceita quaisquer custos não negativos (ou seja, admite arcos de custo positivo ou nulo).

[Esta página é um resumo das seções 21.1 (Underlying Principles) e 21.2 (Dijkstra's Algorithm), p.273-290 do livro de Sedgewick.]

Exemplo A

O exemplo [copiado de 'Algorithms', 4th.ed., de Sedgewick e Wayne] mostra uma SPT com raiz 5 num digrafo com custos nos arcos.

spt-from-5.png
          4-5  .35
          5-4  .36
          4-7  .37
          5-7  .28
          7-5  .28
          5-1  .32
          0-4  .38
          0-2  .26
          7-3  .39
          1-3  .29
          2-7  .34
          6-2  .40
          3-6  .52
          6-0  .58
          6-4  .93

O algoritmo

Cada iteração do algoritmo de Dijkstra começa com uma arborescência T com raiz s.  Para dizer o que acontece durante a iteração, precisamos dos seguintes conceitos.  A franja de T é o conjunto de todos os arcos que saem de T. (Compare com o conceito de corte.)  O preço de um vértice w que esteja fora de T é

a distância de  s  a  w  no digrafo  T + F,

sendo  F  a franja de T.  Em outras palavras, o preço de w é o custo de um caminho de sw que é mínimo no digrafo T + F.  O último arco do caminho está necessariamente na franja F e será chamado arco-franja de w.

No começo da primeira iteração, a arborescência T consiste no vértice s e nada mais.  Cada iteração do algoritmo consiste no seguinte:

(Pode-se dizer, informalmente, que o algoritmo escolhe sempre um vértice w0 fora de T que é o "mais próximo" de T.)  No início da última iteração, para cada vértice v, a distância de s a v em T é também a distância de s a v no digrafo e portanto T é uma SPT.

Nas implementações abaixo, a arborescência T será representada por um vetor de pais parent; os preços dos vértices serão armazenados num vetor dist; e as pontas iniciais dos arcos-franja serão armazenadas num vetor frj.  (O nome do vetor de preços, dist, é apropriado porque o preço de um vértice é uma distância num subdigrafo do nosso digrafo.)

Exemplo B

Aplique o algoritmo de Dijkstra ao digrafo com custos nos arcos definido a seguir para encontrar uma SPT com raiz 0.

     0-1 0-2 1-3 1-4 2-3 2-4 3-5 4-5 6-5
      .1  .2  .8  .9  .6  .7  .1  .9  .0

Registramos a seguir o estado dos vetores parent, frj e dist no início de cada iteração, com "-" indicando valores indefinidos e "*" indicando ∞.

           parent            franja                              dist
     0  1  2  3  4  5  6                               0   1   2   3   4   5   6 
                                                                                
     0  -  -  -  -  -  -     0-1 0-2                  .0  .1  .2   *   *   *   *
     0  0  -  -  -  -  -     1-3 1-4 0-2              .0  .1  .2  .9 1.0   *   *
     0  0  0  -  -  -  -     1-3 1-4 2-3 2-4          .0  .1  .2  .8  .9   *   *
     0  0  0  2  -  -  -     1-4 2-4 3-5              .0  .1  .2  .8  .9  .9   *
     0  0  0  2  -  3  -     1-4 2-4                  .0  .1  .2  .8  .9  .9   *
     0  0  0  2  2  3  -                              .0  .1  .2  .8  .9  .9   *

Exercícios 1

  1. Seja G o digrafo com custos nos arcos descrito a seguir.
         0-1 0-5 1-2 1-3 2-5 2-6 3-6 5-1 5-4 5-6 6-3 6-4
          .1  .6  .1  .2  .5  .5  .5  .1  .0  .3  .2  .2
    

    Seja T a arborescência com raiz 0 cujos arcos são  0-1 1-2 1-3.  Seja F a franja de T.  Faça uma lista dos arcos de F. Dê todos os caminhos mínimos em T + F que começam em 0.

  2. Use o algoritmo de Dijkstra para encontrar uma SPT com raiz 1 no digrafo com custos nos arcos descrito a seguir. No começo de cada iteração, dê o preço de cada vértice. 
         0-1 0-4 1-5 2-0 2-3 2-4 4-3 5-0 5-2 
          .1  .3  .1  .1  .6  .5  .1  .4  .2         
    
  3. Considere o grafo com custos nas arestas definido a seguir. Uma certa iteração do algoritmo de Dijkstra começa com a arborescência cujos arcos são 6-16-2. Dê o estado dos vetores parent e dist no início da próxima iteração.
         6-1 6-2 1-2 3-4 3-5 3-0 4-1 4-2 4-0 5-1 6-0 0-1 0-2 
         1.5 1.6 2.5 2.5 1.5 1.5 4.4 4.2 1.1 4.5 4.7 3.0 3.0
    
  4. [O algoritmo está correto?]  Prove que o algoritmo de Dijkstra de fato produz uma SPT com origem s.
  5. [Arcos de custo negativo]  Mostre que o algoritmo de Dijkstra pode produzir resultados errados se o digrafo tiver arcos de custo negativo.  [Solução]
  6. Compare o algoritmo de Dijkstra com o algoritmo de Prim para o problema da MST. Quais as diferenças? Quais as semelhanças?

Implementação para digrafos densos

Suponha que nosso digrafo está representado por sua matriz de adjacência. Assim, se v-w não é arco então adj[v][w] vale maxCOST, que nesse caso tem o sabor de infinito.  Supõe-se que o valor de maxCOST é tão grande que não se confunde com o custo de um caminho simples.  Em virtude dessa representação, o digrafo é completo e a franja no começo da primeira iteração tem um arco s-w para cada vértice w.

/* Recebe digrafo G com custos não negativos nos arcos e um vértice s. Calcula uma SPT com raiz s. A SPT é armazenada no vetor parent. As distâncias a partir de s são armazenadas no vetor dist. */

/* A função supõe que maxCOST é estritamente maior que a soma dos custos de todas os arcos.  Supõe também que o digrafo tem no máximo maxV vértices. (O código abaixo é uma versão modificada do Programa 20.3, p.238, de Sedgewick.) */

void 
DIGRAPHsptD1( Digraph G, Vertex s, Vertex parent[], double dist[]) { 
   Vertex w, w0, frj[maxV];
   for (w = 0; w < G->V; w++) { 
      parent[w] = -1; 
      dist[w] = G->adj[s][w]; 
      frj[w] = s; 
   }
   parent[s] = s;
   dist[s] = 0.0;
   while (1) {
      double mindist = maxCOST;
      for (w = 0; w < G->V; w++) 
         if (parent[w] == -1 && mindist > dist[w])
            mindist = dist[w0=w]; 
      if (mindist == maxCOST) break;
      parent[w0] = frj[w0];
      for (w = 0; w < G->V; w++) 
         if (dist[w] > dist[w0] + G->adj[w0][w]) {
            dist[w] = dist[w0] + G->adj[w0][w]; 
            frj[w] = w0; 
         }
   }
}

(Três observações técnicas sobre o código:  1. Note como a comparação de dist[w] com dist[w0] + G->adj[w0][w] trata corretamente do caso em que w0 e w não são adjacentes e portanto G->adj[w0][w] vale maxCOST.  2. Estamos supondo, implicitamente, que maxCOST é menor que a metade de DBL_MAX, de modo que a expressão  dist[w0] + G->adj[w0][w] não produz overflow.  3. Embora a variável w0 não seja explicitamente inicializada, seu valor está bem definido sempre que ela é usada.)

No começo de cada iteração, valem os seguintes invariantes:

  1. Para cada vértice v, se dist[v] é menor que maxCOST (que representa o infinito) então dist[v] é o custo de um caminho de sv.
  2. Para cada vértice v, se dist[v] é menor que maxCOST então v ou frj[v] estão na arborescência.
  3. Para cada arco u-v, se u ou v estão na arborescência então o arco u-v está relaxado, ou seja, dist[v] ≤ dist[u] + c, sendo c o custo de u-v.

No fim da execução do algoritmo, em virtude dos invariantes 2 e 3, dist é um potencial. Também pelo invariante 3, se existe caminho de s a algum vértice v então dist[v] é menor que maxCOST.  Segue daí que a função DIGRAPHsptD1 está correta.

A operação mais característica do algoritmo é a relaxação do arco w0-w:

         if (dist[w] > dist[w0] + G->adj[w0][w]) 
             dist[w] = dist[w0] + G->adj[w0][w]; 

Essa operação aparece em toda implementação do algoritmo.

Exemplo C

Considere o digrafo com custos nos arcos definido a seguir [copiado das transparências de José Coelho de Pina, 2011]. Use o algoritmo de Dijkstra para encontrar uma SPT com raiz 0.

     0-2 0-3 0-4 2-4 3-4 3-5 4-1 4-5 5-1 1-2
      .7  .2  .4  .1  .1  .3  .4  .1  .2  .0

Registramos a seguir o estado dos vetores parent, frj e dist no início de cada iteração, com "-" indicando valores indefinidos e "*" indicando ausência de arco (e portanto custo ∞):

          parent                frj                      dist         
     0  1  2  3  4  5     0  1  2  3  4  5       0   1   2   3   4   5
                                                                      
     0  -  -  -  -  -     -  -  0  0  0  -      .0   *  .7  .2  .4   *
     0  -  -  0  -  -     -  -  0  0  3  3      .0   *  .7  .2  .3  .5
     0  -  -  0  3  -     -  4  0  0  3  4      .0  .7  .7  .2  .3  .4
     0  -  -  0  3  4     -  5  0  0  3  4      .0  .6  .7  .2  .3  .4
     0  5  -  0  3  4     -  5  1  0  3  4      .0  .6  .6  .2  .3  .4
     0  5  1  0  3  4     -  5  1  0  3  4      .0  .6  .6  .2  .3  .4
coelho-2011/aula15-dijkstra-C.png

Desempenho

A função DIGRAPHsptD1 consome tempo proporcional a  V 2  no pior caso, sendo V o número de vértices de G. Portanto, a função é linear quando restrita a digrafos densos. Também é linear quando restrita a digrafos representados por matriz de adjacência.

(O desempenho da função DAGmin, que resolve o problema em DAGs, é bem melhor.)

Exercícios 2

  1. [Transparências de José Coelho de Pina, 2011]  Use o algoritmo de Dijkstra para encontrar uma SPT com raiz 0 no digrafo com custos nos arcos definido a seguir.
         0-2 0-3 0-4 2-4 3-4 3-5 4-1 4-5 5-1 1-2
          .7  .5  .3  .1  .1  .2  .0  .3  .5  .3
    
  2. [Sedgewick-Wayne 4.4.4, p.685]  Remova o vértice 7 do exemplo A acima. Calcule uma SPT com raiz 0.  Repita o exercício depois de inverter todos os arcos.
  3. [Sedgewick 21.17, p.288]  Considere o grafo cujos vértices são os pontos no plano definidos a seguir.
               vértice    0     1     2     3     4     5
           coordenadas  (1,3) (2,1) (6,5) (3,4) (3,7) (5,3)
    

    Suponha que as arestas do grafo são  1-0 3-5 5-2 3-4 5-1 0-3 0-4 4-2 2-3  e o custo de cada aresta é igual ao comprimento do segmento de reta que liga as pontas da aresta.  Submeta o grafo à função DIGRAPHsptD1 com s = 0.  Exiba o estado dos vetores parent e dist no início de cada iteração.

  4. Escreva uma variante da função DIGRAPHsptD1, para digrafos representados por listas de adjacência.
  5. No código de DIGRAPHsptD1, antes de "if (dist[w] > dist[w0]+G->adj[w0][w])", não deveríamos verificar se parent[w] é -1?
  6. No código de DIGRAPHsptD1 o papel do vetor frj[] pode ser exercido pelo vetor parent[]. Reescreva DIGRAPHsptD1 sem frj[].
  7. Escreva uma variante da função DIGRAPHsptD1 que receba dois vértices s e t e devolva a distância de st. Escreva código simples, sem variáveis e operações supérfluas.
  8. [Algoritmo correto?]  Prove que a função DIGRAPHsptD1 está correta.  Comece provando os invariantes 1, 2 e 3.
  9. [Desempenho]  Prove que DIGRAPHsptD1 consome tempo proporcional a V2 no pior caso.
  10. Analise e discuta a seguinte variante de DIGRAPHsptD1:
       void DIGRAPHsptD1( Digraph G, Vertex s, Vertex parent[], double dist[]) { 
          Vertex w, w0, frj[maxV];
          for (w = 0; w < G->V; w++) { 
             parent[w] = -1; 
             dist[w] = maxCOST; 
          }
          frj[s] = s;
          dist[s] = 0.0;
          while (1) {
             double mindist = maxCOST;
             for (w = 0; w < G->V; w++) {
                if (parent[w] == -1 && mindist > dist[w])
                      mindist = dist[w0=w]; 
             if (mindist == maxCOST) break;
             parent[w0] = frj[w0];
             for (w = 0; w < G->V; w++) 
                if (dist[w] > dist[w0] + G->adj[w0][w]) {
                   dist[w] = dist[w0] + G->adj[w0][w]; 
                   frj[w] = w0; 
                }
          }
       }
    
  11. Compare o código de DIGRAPHsptD1 com o de GRAPHmstP1. Quais as diferenças? Quais as semelhanças?

Implementação para digrafos esparsos

A seguinte implementação do algoritmo de Dijkstra é adequada para digrafos esparsos. Assim, é apropriado supor que o digrafo é representado por listas de adjacência:

/* Recebe digrafo G com custos não negativos nos arcos e um vértice s. Calcula uma SPT com origem s. A SPT é armazenada no vetor parent. As distâncias a partir de s são armazenadas no vetor dist. */

/* A implementação supõe que a soma dos custos de todos os arcos é estritamente menor que maxCOST.  Supõe também que o digrafo tem no máximo maxV vértices.  (O código abaixo é uma versão modificada dos Programas 20.4, p.242, e 21.1, p.284, de Sedgewick.) */

void 
DIGRAPHsptD2( Digraph G, Vertex s, Vertex parent[], double dist[]) { 
   link p; Vertex w, w0, frj[maxV];
   PQinit( ); 
   for (w = 0; w < G->V; w++) 
      parent[w] = frj[w] = -1; 
   parent[s] = s;
   dist[s] = 0.0; 
   for (p = G->adj[s]; p != NULL; p = p->next) {
      dist[p->w] = p->cost; 
      PQinsert( p->w); 
      frj[p->w] = s; 
   } 
   while (!PQempty( )) {
      w0 = PQdelmin( ); 
      parent[w0] = frj[w0]; 
      for (p = G->adj[w0]; p != NULL; p = p->next) {
         if (frj[p->w] == -1) { 
            dist[p->w] = dist[w0] + p->cost; 
            PQinsert( p->w); 
            frj[p->w] = w0; 
         } 
         else if (dist[p->w] > dist[w0] + p->cost) {
            dist[p->w] = dist[w0] + p->cost; 
            PQdec( p->w); 
            frj[p->w] = w0; 
         }
      }
   }
}

A função DIGRAPHsptD2 usa uma fila de prioridades (= priority queue) para escolher, eficientemente, o próximo vértice a ser acrescentado à SPT.  [Veja capítulo 9 (Priority Queues and Heapsort), p.389, do volume 1 do livro de Sedgewick.]  Trata-se de uma fila de vértices com prioridades definidas pelo vetor dist.  A fila é manipulada pelas seguintes funções:

A implementação clássica da fila de prioridades usa uma estrutura de heap.

trace-of-dijkstras-algorithm.jpg

Rastreamento do algoritmo de Dijkstra aplicado ao digrafo do exemplo A acima, com raiz 0.
O vetor distTo da figura corresponde ao nosso dist.
O vetor edgeTo da figura corresponde aproximadamente ao nosso parent.
A cor vermelha identifica a franja da arborescência (que é indicada em preto).
[Copiado de 'Algorithms', 4th.ed., de Sedgewick e Wayne.]

Desempenho

Se a fila de prioridades for implementada como um heap então DIGRAPHsptD2 consome tempo proporcional a  (V+A) log V  no pior caso, sendo V o número de vértices e A o número de arcos de G. (Se G for fracamente conexo então A ≥ V−1 e portanto o consumo de tempo é proporcional a A log V no pior caso.)

Se nos restringirmos a digrafos esparsos, DIGRAPHsptD2 é assintoticamente mais rápida que DIGRAPHsptD1. Mas se G é denso então DIGRAPHsptD1 é mais rápida.

(O desempenho da função DAGmin, que resolve o problema em DAGs, é bem melhor.)

Exercícios 3

  1. [Desempenho]  Prove que DIGRAPHsptD2 consome tempo proporcional a (V+A) log V no pior caso. (Suponha que a fila de prioridades é implementada em um heap.)
  2. Escreva o código das funções PQinit, PQempty, PQinsert, PQdelmin e PQdec.  [Solução]
  3. Escreva uma versão compacta de DIGRAPHsptD2, incorporando ao código os códigos das funções de manipulação da fila de prioridade.
  4. No código de DIGRAPHsptD2 o papel do vetor frj[] pode ser exercido pelo vetor parent[]. Reescreva DIGRAPHsptD2 sem frj[].
  5. Compare o código de DIGRAPHsptD2 com o de GRAPHmstP2. Quais as diferenças? Quais as semelhanças?

Outra implementação para digrafos esparsos

Podemos reorganizar o código de DIGRAPHsptD2 de modo a inserir todos os vértices na fila de prioridades antes mesmo do início do processo iterativo:

/* (Código inspirado no Programa 21.1, p.284, de Sedgewick.) */

void 
DIGRAPHsptD3( Digraph G, Vertex s, Vertex parent[], double dist[]) { 
   Vertex w, w0; link p;
   for (w = 0; w < G->V; w++) { 
      parent[w] = -1;
      dist[w] = maxCOST; 
   }
   parent[s] = s;
   dist[s] = 0.0; 
   for (p = G->adj[s]; p != NULL; p = p->next)
      dist[p->w] = p->cost; 
   PQinit( ); 
   for (w = 0; w < G->V; w++)
      if (w != s) PQinsert( w); 
   while (!PQempty( )) {
      w0 = PQdelmin( );
      if (dist[w0] == maxCOST) break;
      for (p = G->adj[w0]; p != NULL; p = p->next) {
         if (dist[p->w] > dist[w0] + p->cost) { 
            dist[p->w] = dist[w0] + p->cost; 
            PQdec( p->w); 
            parent[p->w] = w0; 
         }
      }
   }
}

Exercícios 4

  1. No código da função DIGRAPHsptD3, não deveríamos fazer  "if (dist[w] > dist[w0] + p->cost) {...}"  somente se w está na fila de prioridades?
  2. Compare o código de DIGRAPHsptD3 com o de GRAPHmstP3. Quais as diferenças? Quais as semelhanças?

Exercícios 5

  1. [Sedgewick 21.20, p.289]  O diâmetro de um grafo é o máximo das distâncias entre dois vértices. Escreva código que usa o algoritmo de Dijkstra para calcular o diâmetro de um grafo.
  2. Escreva um algoritmo que encontre um arco cuja remoção causa o maior aumento na distância de um vértice s a um vértice t.
  3. Escreva uma função que faça a análise de sensibilidade em relação a um par (s,t) de vértices.  Sua função deve preencher uma matriz M indexada pelos vértices de tal modo que M[v][w] valha 1 se v-w é um arco cujo custo pode ser aumentado sem que isso aumente a distância de st. Nos demais casos, M[v][w] deve valer 0.
  4. [Sedgewick 21.28, p.289]  Escreva uma função que receba conjuntos S e T de vértices e calcule a distância de ST, ou seja, o custo de um caminho de custo mínimo que começa em S e termina em T. (Basta introduzir uma pequena modificação no algoritmo de Dijkstra.) 
  5. [Caminhos máximos?]  Digamos que a "antidistância" de um vértice s a um vértice t é o custo de um caminho simples de custo máximo dentre os que vão de st. Modifique DIGRAPHsptD1 da seguinte maneira: em cada iteração, escolha um vértice w0 que maximize dist[w0]. (Para isso, troque mindist por maxdist calculado da maneira óbvia.) Essa versão modificada calcula a "antidistância" de um vértice s e cada um dos demais vértices do digrafo com custos positivos?
  6. [Sedgewick 21.8, p.273]  Escreva um programa que gere V pontos aleatórios no plano e construa um grafo que tem esses pontos por vértices. Dois vértices v e w são adjacentes se a distância geométrica entre v e w é no máximo d.  (Veja exercício na página Grafos aleatórios.)  O custo de cada arco v-w é a distância geométrica entre vw. Depois de gerar o digrafo, o seu programa deve calcular a distância do primeiro vértice gerado a cada um dos demais.
  7. [Todos os pares]  Escreva uma função que receba um digrafo G com custos não negativos nos arcos e preencha uma matriz d[0..V-1][0..V-1] com as distância entre todos os pares de vértices.
  8. [Custos no vértices[Sedgewick 21.52, p.299]  Suponha dado um digrafo com custos não negativos nos vértices (e não aos arcos). O custo de um caminho num tal digrafo é a soma dos custos dos vértices do caminho. Quero encontrar um caminho de custo mínimo dentre os que começam num vértice s e terminam num vértice t.  Adapte o algoritmo de Dijkstra para resolver esse problema.
  9. Escreva uma função que encontre um caminho de custo mínimo num tabuleiro com n linhas e n colunas. Cada casa do tabuleiro tem um custo não negativo. O seu caminho deve começar na casa que está no cruzamento da linha 1 com coluna 1 e terminar na casa que está no cruzamento da linha n com a coluna n. O caminho só pode passar de um casa para a casa vizinha na horizontal ou vertical (não na diagonal). O custo de um caminho é a soma dos custos dos casas por onde o caminho passa.

Valid HTML 4.01 Strict Valid CSS!