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.
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:
- seja v o primeiro vértice da fila
- retire v da fila
- para cada vizinho w de v
- se dist[w] está indefinido
- então faça dist[w] = dist[v] + 1
- então faça pa[w] = v
- então coloque w no fim da fila
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
-
★
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.)
-
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.
-
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 subgrafo 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.
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.)
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):
dist[queue[]]
0
1 1 1
1 1
1 2 2
2 2
2
Exercícios 2
-
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?
-
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)
?
-
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.
-
★
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)?
-
★
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.
-
★
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 s a w em T.
-
★
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.)
-
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()?
-
★
Depois de executar a função GRAPHspt()
com argumentos G e s,
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.
-
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.
-
Matriz de adjacências.
Adapte a função GRAPHspt() a grafos
representados por matriz de adjacências.
-
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:
-
todo arco com ambas as pontas em T está
relaxado em relação a dist[] e
-
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:
-
Primeira observação:
de acordo com o invariante 2,
nenhum arco começa em T e termina fora de T.
Logo, todos os vértices que estão ao alcance de s
pertencem a T.
Portanto, T é um subgrafo gerador de G.
-
Segunda observação:
o potencial dist[]
é relaxado.
De fato,
os invariantes 1 e 2
garantem que todo arco com ponta inicial em T
tem ponta final em T
e portanto está relaxado.
-
Terceira observação:
para cada vértice t de T,
o caminho de s a t em T
tem comprimento dist[t],
conforme o exercício Distâncias na árvore
acima.
De acordo com a condição suficiente de minimalidade
do capítulo Caminhos de comprimento mínimo,
essa observação,
junto com as anteriores,
garante que o caminho de s a t em T
é mínimo em G.
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 s a t
em G.
Isso prova que GRAPHspt() está correto.
Exercícios 3
-
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.
-
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 s a t em G
é exatamente dist[t].
-
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
-
★
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.)
-
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).
-
★
Grafos que mudam com o tempo.
Sejam dist[] e pa[] os vetores
calculados pela função GRAPHspt()
com argumentos G e s.
(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.
-
[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.
-
★
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 s a t.
Escreva uma função que devolva
um vértice central
de qualquer grafo não-dirigido conexo.
-
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.
-
Estude a evolução do diâmetro
de 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.
-
★
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.)
-
★
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
-
Pergunta:
Não seria melhor restringir a função GRAPHspt()
a grafos cujos vértices estão todos
ao alcance
do vértice s?
Assim, todos os vértices teriam dist[] finito
no fim da execução da função.
Resposta:
Não é uma boa ideia.
Em primeiro lugar, isso não simplificaria em nada
o código de GRAPHspt().
Em segundo lugar,
seria necessário verificar,
antes de invocar GRAPHspt(),
se todos os vértices estão ao alcance de s,
e essa verificação
repetiria a maior parte do trabalho de GRAPHspt().