Ata de Aula (04/06)

Disciplina de Complexidade de Algoritmos

Professor: Dr. João Meidanis

Assunto: Caminhos mais curtos de todos os pares (Capítulo 25)

Livro-texto: Algoritmos – Thomas H. Cormen, et. al

Autor: Eduardo Akira Yonekura

RA: 025989

Esta ata cobre os seguintes tópicos:

Caminhos mais curtos e multiplicação de matrizes.

Algoritmo de Floyd-Warshall.

Fecho Transitivo de um grafo orientado.

 

Introdução

Considere o problema de encontrar caminhos mais curtos entre todos os pares de vértices em um grafo. Esse problema poderia surgir, por exemplo, na elaboração de uma tabela de distâncias entre todos os pares de cidades para um atlas rodoviário.

Tem-se um grafo orientado ponderado G = (V,E) com uma função peso w: E->R que mapeia arestas como pesos de valores reais. Deseja-se encontrar, para todo par de vértices u, v ∈ V, um caminho mais curto (de peso mínimo) desde u até v, onde o peso de um caminho é a soma dos pesos de suas arestas constituintes. É desejável que a saída esteja em uma forma tabular: a entrada na linha u e na coluna v deve ser o peso de um caminho mais curto desde u até v. É importante salientar que todos os algoritmos descritos nesta ata fazem uso de uma representação do grafo como uma matriz de adjacências.

 

Caminhos mais curtos e multiplicação de matrizes

Nesta seção, é apresentado um algoritmo de programação dinâmica para o problema de caminhos mais curtos de todos os pares sobre um grafo orientado G = (V,E). Recapitulando então os passos necessários para desenvolver um algoritmo de programação dinâmica:

  1. Caracterizar a estrutura de uma solução ótima.
  2. Definir recursivamente o valor de uma solução ótima.
  3. Calcular o valor de uma solução ótima de baixo para cima.

Nota do Autor: O 4º passo, ou seja, a construção de uma solução ótima a partir de informações calculadas, é examinado somente nos exercícios.

  1. A estrutura de um caminho mais curto
  2. Primeiramente, considere um caminho mais curto p desde o vértice i até o vértice j, e suponha que p contenha no máximo m arestas (suponha também que não exista nenhum ciclo de peso negativo). Se os vértices i e j são distintos (caso contrário p tem peso 0 e nenhuma aresta), então decompomos o caminho p em i ~p’ k -> j, onde o caminho p’ contém no máximo m-1 arestas. Pelo lema 24.1, p’ é um caminho mais curto desde i até k; desse modo, d (i,j) = d (i,k) + wkj

  3. Fórmula recursiva para o problema de caminhos mais curtos de todos os pares
  4. Considere lij(m) o peso mínimo de qualquer caminho desde o vértice i até o vértice j que contém no máximo m arestas. Portanto, tem-se que, para m = 0 (ou seja, existe um caminho mais curto desde i até j sem arestas se, e somente se i = j):

    lij(0) =

    0, se i = j

    ∞ , se i ≠ j

    Para m>= 1, tem-se que:

    lij(m) = min (lij(m-1), min1<=k<=n { lik(m-1) + wkj }) = min1<=k<=n { lik(m-1) + wkj })

    onde:

    lij(m-1) – peso do caminho mais curto desde i até j, que consiste no máximo em m-1 arestas

    min1<=k<=n{ lik(m-1) + wkj } - peso mínimo de qualquer caminho desde i até j, que consiste no máximo em m arestas, obtido pelo exame de todos os possíveis predecessores k de j.

  5. Cálculo dos pesos de caminhos mais curtos de baixo para cima

Dadas as matrizes L(m - 1) e W, o algoritmo abaixo retorna a matriz L(m), ou seja, ele estende os caminhos mais curtos calculados até agora por mais uma aresta.

EXTEND-SHORTEST-PATHS(L,W)

1 n <- linhas[L]

2 seja L’ = (lij) uma matriz n x n

3 for i <- 1 to n

4 do for j<- 1 to n

5 do l’ij <- ∞

6 for k <- 1 to n

7 do l’ij <- min (l’ij , lik + wkj)

8 return L’

É possível perceber, através das substituições abaixo, a relação do algoritmo EXTEND-SHORTEST-PATHS com a multiplicação de matrizes (algoritmo MATRIX-MULTIPLY)

l(m-1) por a,

w por b,

l(m) por c,

min por +,

+ por *,

Nota do autor: O Professor João Meidanis explicou-nos melhor a estreita relação entre os algoritmos EXTEND-SHORTEST-PATHS e MATRIX-MULTIPLY da seguinte maneira:

+,*

comutativa: a + b = b + a

elemento neutro: + é 0

elemento neutro: * é 1

min,+

comutativa: min(a,b) = min(b,a)

elemento neutro: min é µ

elemento neutro: + é 0

a(b+c) = ab + ac (V)

a+(b*c) = (a+b)(a+c) (F)

a + min(b,c) = min (a+b,a+c) (V)

min(a,b+c) = min(a,b) + min(a,c) (F)

Idealmente, deve ser observada as propriedades comutativa, elemento neutro, associativa e distributiva de uma operação com relação à outra.

MATRIX-MULTIPLY(A,B)

1 n <- linhas[A]

2 seja C uma matriz n x n

3 for i <- 1 to n

4   do for j<- 1 to n

5      do cij <- 0

6     for k <- 1 to n

7       do cij <- cij + aik * bkj

8 return C

Como descrito no algoritmo abaixo, calcula-se então os pesos de caminhos mais curtos estendendo os caminhos mais curtos aresta por aresta (cujo tempo de execução é Theta(n4)).

SLOW-ALL-PAIRS-SHORTEST-PATHS(W)

1 n <- linhas[L]

2 L(1) <- W

3 for m <- 2 to n - 1

4   do L(m) <- EXTEND-SHORTEST-PATHS(L(m-1), W)

5 return L(n - 1)

 

0

3

8

-4

 

0

3

8

2

-4

 

0

1

7

 

3

0

-4

1

7

L(1)

4

0

L(2)

4

0

5

11

 

2

-5

0

 

2

-1

-5

0

-2

 

6

0

 

8

1

6

0

 

0

3

-3

2

-4

 

0

1

-3

2

-4

 

3

0

-4

1

-1

 

3

0

-4

1

-1

L(3)

7

4

0

5

11

L(4)

7

4

0

5

3

 

2

-1

-5

0

-2

 

2

-1

-5

0

-2

 

8

5

1

6

0

 

8

5

1

6

0

Figura 25.1 Um grafo orientado e a sequência de matrizes L(m) calculada por SLOW-ALL-PAIRS-SHORTEST-PATHS.

 

Melhorando o tempo de execução

Contudo, não é necessário o cálculo de todas as matrizes L(m). O objetivo é calcular a matriz L(n-1). Graças a propriedade de associatividade do procedimento EXTEND-SHORTEST-PATHS, pode-se calcular L(n-1) apenas com Teto(lg(n-1)) produtos de matrizes. O procedimento abaixo emprega então essa importante propriedade denominada elevação ao quadrado repetida.

FASTER-ALL-PAIRS-SHORTEST-PATHS(W)

1 n <- linhas[L]

2 L(1) <- W

3 m <- 1

4 while  m < n – 1

5   do L(m) <- EXTEND-SHORTEST-PATHS(L(m), L(m))

6     m <- 2m

7 return L(m)

Explicação do algoritmo e tempo de execução:

Em cada iteração do loop while das linhas 4 a 6, calcula-se L(2m) = (L(2m))2, começando com m = 1. No valor de cada iteração, o valor de m é duplicado. A iteração final calcula L(n-1) calculando na verdade L(2m) para algum n-1 <= 2m < 2n-2. O tempo de execução total desse algoritmo é Theta(n3 lg n).

 

Algoritmo de Floyd-Warshall

O algoritmo de Floyd-Warshall também utiliza uma formulação de programação dinâmica (assim como no caso dos algoritmos de todos os pares baseados na multiplicação de matrizes). Porém, é importante salientar que esta caracterização é diferente da que foi apresentada anteriormente.

FLOYD-WARSHALL(W)

1 n <- linhas[L]

2 D(0) <- W

3 for k <- 1 to n

4   do for i <- 1 to n

5      do for j <- 1 to n

6        do dij(k) <-min(dij(k-1), dik(k-1)+ dkj(k-1))

7   return D(n)

Explicação do algoritmo de Floyd-Warshall e tempo de execução:

Para um detalhamento maior, consulte o livro-texto na pág. 497-498. O tempo de execução do algoritmo de Floyd-Warshall é determinado pelos loops for triplamente aninhados das linhas 3 a 6. Cada execução da linha 6 demora o tempo O(1). Portanto, o algoritmo é executado no tempo Theta(n3).

 

0

3

8

-4

 

0

3

8

-4

 

0

1

7

 

0

1

7

D(0)

4

0

D(1)

4

0

 

2

-5

0

 

2

5

-5

0

-2

 

6

0

 

6

0

 

0

3

8

4

-4

 

0

3

8

4

-4

 

0

1

7

 

0

1

7

D(2)

4

0

5

11

D(3)

4

0

5

11

 

2

5

-5

0

-2

 

2

-1

-5

0

-2

 

6

0

 

6

0

 

0

3

-1

4

-4

 

0

1

-3

2

-4

 

3

0

-4

1

-1

 

3

0

-4

1

-1

D(4)

7

4

0

5

3

D(5)

7

4

0

5

3

 

2

-1

-5

0

2

 

2

-1

-5

0

-2

 

8

1

6

0

 

8

5

1

6

0

Figura 25.4 A sequência de matrizes D(k) calculada pelo algoritmo de Floyd-Warshall para o grafo da Figura 25.1

 

Fecho Transitivo de um grafo orientado

O objetivo é, dado um grafo orientado G = (V,E) com o conjunto de vértices V = {1,2,...,n}, descobrir se existe um caminho em G desde i até j para todos os pares de vértices i,j ∈ V. O fecho transitivo de G é definido como o grafo G* = (V, E*), onde

E* = {(i,j) : existe um caminho desde o vértice i até o vértice j em G}.

TRANSITIVE-CLOSURE(G)

1 n <- |V[G]|

2 for i <- 1 to n

3   do for j <- 1 to n

4     do if i = j or (i,j) ∈ E[G]

5        then tij(0) <- 1

6        else tij(0) <- 0

7   for k <- 1 to n

8    do for i <- 1 to n

9      do for j <- 1 to n

10      do tij(k) <- tij(k-1) ∨ (tij(k) ∧ tij(k))

11 return T(n)

Explicação do algoritmo de fecho transitivo e tempo de execução:

O algoritmo acima envolve a substituição das operações aritméticas min e + no algoritmo de Floyd-Warshall pelas operações lógicas ∨ (OU lógico) e ∧ (E lógico). Para i,j,k = 1,2,...,n, definimos tij(k) como 1 se existe um caminho no grafo G desde o vértice i até o vértice j com todos os vértices intermediários no conjunto {1,2,...,k}e 0 em caso contrário. Construímos o fecho transitivo G* = (V, E*) inserindo a aresta (i,j) em E* se, e somente se tij(n) = 1.

 

1

0

0

0

 

1

0

0

0

 

1

0

0

0

T(0)

0

1

1

1

T(1)

0

1

1

1

T(2)

0

1

1

1

 

0

1

1

0

 

0

1

1

0

 

0

1

1

1

 

1

0

1

1

 

1

0

1

1

 

1

0

1

1

 

 

1

0

0

0

 

1

0

0

0

T(3)

0

1

1

1

T(4)

1

1

1

1

 

0

1

1

1

 

1

1

1

0

 

1

1

1

1

 

1

1

1

1

Figura 25.5. Um grafo orientado e as matrizes T(k) calculadas pelo algoritmo de fecho transitivo

Nota do Autor: A informação importante dada pelo colega Augusto é que o fecho transitivo de um grafo é um outro grafo.