Aula por Lucca Siaudzionis
Com edições por Lúcio Cardoso
"O cientista Succa Liaudzionis decidiu tirar férias na Europa. Enquanto ele estava em seu descanso, o cientista Sictor Vales fez uma descoberta impressionante no quartel-general do Núcleo Organizado de Informática e Computação (NOIC). Agora, Succa precisa voltar imediatamente para o quartel do Noic para poder avaliar a descoberta. Succa foi para o aeroporto, mas a quantidade de voos disponíveis é muito grande (repare que alguns podem ter conexões e todos existem nos dois sentidos) e ele precisa saber qual o menor tempo para que possa voltar para a base."
Vamos montar o grafo que é importante para a vida do Succa.
Vértices: Cada uma das cidades disponíveis que possuem voos.
Arestas: Cada um dos voos. Aqui, peso de cada aresta é o tempo de duração de voo.
Queremos então achar o menor caminho da cidade de onde Succa está para a cidade onde o Noic está.
Vamos comentar dois algoritmos muito conhecidos nesta aula: Algoritmo de Dijkstra e Algoritmo de Floyd-Warshall.
Algoritmo de Dijkstra
Este algoritmo recebe um vértice principal para ser a fonte do grafo e retorna o menor caminho de todos os vértices do grafo para . Abordaremos, para este algoritmo, a solução em , onde é o número de vértices. Há, também, uma solução , que é muito mais rápida para grafos esparsos, mas não abordaremos nesta aula.
O Algoritmo consiste em:
- Definir a distância como para todos os vértices e como para o vértice .
- Em cada passo, encontrar o vértice , que ainda não foi processado, que possua a menor das distâncias. Este vértices agora foi fixado como já tendo a menor distância de para ele.
- Ver para cada vértice , vizinho de , se é melhor manter a distância atual de ou atualizar fazendo o caminho e depois . Repare que o caminho já foi fixado e possivelmente tem conexões no meio.
Vamos dar uma olhada no que seria o pseudo-código desse algoritmo:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// para o algoritmo usaremos uma matriz de adjacencia onde mapa[i][j] | |
// significa a distância de i para j. | |
// A única diferença do normal é que, caso não exista a aresta (i, j), | |
// definiremos mapa[i][j] como sendo igual a infinito. | |
Dijkstra(vértice S): | |
para todo i de 1 a N: distancia[i] := matriz[S][i] | |
distancia[S] := 0 | |
processado[S] := true | |
repetir sempre: | |
vertice u := -1 | |
int menor_distancia := infinito | |
// achar o vértice mais próximo | |
para todo i de 1 a N: | |
se ((processado[i] == false) e (distancia[i] < menor_distancia)): | |
u := i | |
menor_distancia := distancia[i] | |
se (u == -1): // caso não tenha mais nenhum vértice disponível, fim do algoritmo | |
quebra_ciclo | |
processado[u] = true // para impedir que ele seja processado novamente | |
// agora, vamos tentar atualizar os menores caminhos | |
para todo i de 1 a N: | |
distancia[i] = minimo(distancia[i], distancia[u] + mapa[u][i]) |
Olhe o seguinte grafo e sua correspondente matriz de adjacência.
Vamos simular o Dijkstra fazendo .
Primeiro, inicializamos as distâncias:
Nó | Distância | |
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
7 |
O vértice de menor distância é o . Então, selecionamos ele e atualizamos as distâncias dos vértices e , que são seus vizinhos.
Nó | Distância | |
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
7 |
O novo vértice de menor distância é o . Selecionamos então ele e só alteramos a distância do vértice (pois não compensa mudar o ).
Nó | Distância | |
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
7 |
O novo vértice mais próximo é o vértice . Com ele, podemos atualizar a distância do vértice e do vértice .
Nó | Distância | |
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
7 |
O novo vértice mais próximo é o , mas não conseguimos mudar nenhuma distância.
Nó | Distância | |
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
7 |
O novo vértice mais próximo é o , mas também não atualizamos nenhuma distância.
Nó | Distância | |
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
7 |
Repare que não iremos acessar o , apesar de ele ainda não ter sido acessado, por sua distância ser . Vemos que calculamos a menor distância de para todos os outro vértices. O fato de a distância de ao aparecer como sendo significa que não existe caminho do vértice ao vértice , o que pode ser facilmente observado como sendo verdade.
Vamos fazer o código, em C++, para o problema da Viagem de Succa. Vamos supor neste problema que o número de cidades é e que o a duração de cada viagem não passa de minutos.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// viagem_de_succa.cpp | |
// | |
// Created by Lucca Siaudzionis on 06/05/15. | |
// | |
// Viagem de Succa - Noic | |
#include <cstdio> | |
#include <algorithm> // para usar a função min(a, b) | |
using namespace std; | |
//--------------------- | |
#define MAXN 1010 | |
// como não existe o Infinito no computador, usaremos um número bem grande | |
#define INFINITO 999999999 | |
int n, m; // número de vértices e arestas | |
int cidade_noic; // cidade onde está o Noic | |
int cidade_succa; // cidade onde está o Succa | |
int distancia[MAXN]; // o array de distâncias à fonte | |
int processado[MAXN]; // o array que guarda se um vértice foi processado | |
int mapa[MAXN][MAXN]; // nossa matriz de adjacência | |
//--------------------- | |
void Dijkstra(int S){ | |
for(int i = 1;i <= n;i++) distancia[i] = mapa[S][i]; | |
distancia[S] = 0; | |
processado[S] = true; | |
while(true){ // rodar "infinitamente" | |
int davez = -1; | |
int menor = INFINITO; | |
// selecionamos o vértice mais próximo | |
for(int i = 1;i <= n;i++) | |
if(!processado[i] && distancia[i] < menor){ | |
davez = i; | |
menor = distancia[i]; | |
} | |
if(davez == -1) break; // se não achou ninguém, é o fim do algoritmo | |
processado[davez] = true; // marcamos para não voltar para este vértice | |
// agora, tentamos atualizar as distâncias | |
for(int i = 1;i <= n;i++) | |
distancia[i] = min(distancia[i], distancia[davez] + mapa[davez][i]); | |
} | |
} | |
int main(){ | |
scanf("%d %d", &n, &m); | |
scanf("%d %d", &cidade_succa, &cidade_noic); | |
// inicializaremos a matriz para infinito em todas as casas | |
for(int i = 1;i <= n;i++) | |
for(int j = 1;j <= n;j++) | |
mapa[i][j] = INFINITO; | |
for(int i = 1;i <= m;i++){ | |
int x, y, tempo; | |
scanf("%d %d %d", &x, &y, &tempo); | |
// colocamos como recebendo esse valor mínimo para o caso | |
// dois voos entre as mesmas cidades | |
mapa[x][y] = min(mapa[x][y], tempo); | |
mapa[y][x] = min(mapa[y][x], tempo); | |
} | |
Dijkstra(cidade_succa); // rodamos o Dijkstra | |
printf("%d\n", distancia[cidade_noic]); // imprimimos a resposta | |
return 0; | |
} |
Note que a complexidade do algoritmo descrito acima é , pois o loop principal será executado vezes (enquanto houver um vértice que não foi processado) e, nesse loop, encontramos o vértice com menor distância de em &&N&& operações. No entanto, é possível otimizar o algoritmo utilizando uma estrutura de dados do C++ chamada priority queue (veja esta aula para saber mais), que é uma fila capaz de manter seus elementos ordenados de maneira crescente em para cada operação.
Assim, podemos usar uma priority queue que guarda pares da forma , onde &&d&& é a distância do vértice para , organizando os pares de maneira crescente em relação a . De tal forma, o vértice com menor distância de estará no topo da fila, e as operações de inserção e remoção serão feitas em .
A implementação é, então, bastante parecida com a do algoritmo anterior; a principal diferença, além da fila de prioridade, será que vamos utilizar uma lista de adjacência, ao contrário da matriz, uma vez que percorrer a matriz tornaria nosso algoritmo quadrático (complexidade ). Segue abaixo o código em C++:
https://gist.github.com/luciocf/75ea65b3f898767a4f6078db4ada09e9
Perceba que a complexidade do algoritmo é , já que para cada aresta partindo do vértice sendo processado no momento, faremos uma operação na priority queue em .
Algoritmo de Floyd-Warshall
O Algoritmo de Floyd-Warshall, quando executado, retorna a menor distância entre todos os pares de vértices possíveis. Seu tempo de execução é e precisa de de memória. Note que, se quisermos a menor distância para todos os pares de vértices, podemos simplesmente executar o Algoritmo de Dijkstra vezes (uma para cada vértice) e obteremos o mesmo resultado, no mesmo tempo e mesmo uso de memória. A diferença essencial, então, é que o código do Algoritmo de Floyd-Warshall é extremamente simples.
Olhe o pseudo-código do Algoritmo de Floyd-Warshall:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// a matriz distancia[][] irá ser inicializada | |
// como sendo igual a matriz de adjacencia do grafo | |
para todo k de 1 a N: | |
para todo i de 1 a N: | |
para todo j de 1 a N: | |
distancia[i][j] = minimo( distancia[i][j], distancia[i][k] + distancia[k][j] ) |
Como eu disse, bem simples o código.
Vamos então resolver o problema da Viagem de Succa usando o algoritmo de Floyd-Warshall (para poder o tempo ser suficiente, vamos reduzir para ). Vamos aproveitar e dificultar um pouco problema. Succa irá fazer perguntas no formato "Qual o menor tempo de viagem da cidade até a cidade ?".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// viagem_de_succa_floyd.cpp | |
// | |
// Created by Lucca Siaudzionis on 06/05/15. | |
// | |
// Viagem de Succa - Noic | |
#include <cstdio> | |
#include <algorithm> | |
using namespace std; | |
//------------------------- | |
#define MAXN 105 | |
#define INFINITO 999999999 | |
int n, m, q; | |
int mapa[MAXN][MAXN]; | |
int distancia[MAXN][MAXN]; | |
//------------------------- | |
int main(){ | |
scanf("%d %d %d", &n, &m, &q); | |
// inicializaremos a matriz para infinito em todas as casas | |
for(int i = 1;i <= n;i++) | |
for(int j = 1;j <= n;j++) | |
mapa[i][j] = INFINITO; | |
for(int i = 1;i <= m;i++){ | |
int x, y, tempo; | |
scanf("%d %d %d", &x, &y, &tempo); | |
// colocamos como recebendo esse valor mínimo para o caso | |
// dois voos entre as mesmas cidades | |
mapa[x][y] = min(mapa[x][y], tempo); | |
mapa[y][x] = min(mapa[y][x], tempo); | |
} | |
// inicializamos as distâncias | |
for(int i = 1;i <= n;i++) | |
for(int j = 1;j <= n;j++) | |
distancia[i][j] = mapa[i][j]; | |
// Algoritmo de Floyd-Warshall | |
for(int k = 1;k <= n;k++) | |
for(int i = 1;i <= n;i++) | |
for(int j = 1;j <= n;j++) | |
distancia[i][j] = min(distancia[i][j], distancia[i][k] + distancia[k][j]); | |
for(int i = 1;i <= q;i++){ | |
int S, E; | |
scanf("%d %d", &S, &E); // lemos a pergunta | |
printf("%d\n", distancia[S][E]); // imprimimos a resposta | |
} | |
return 0; | |
} |
Esses dois algoritmos são muito conhecidos e é essencial saber usar corretamente os dois. Se o seu problema puder ser resolvido por Floyd-Warshall (não tiver problema com o tempo), é o mais sensato a se fazer devido a facilidade de implementar e debugar o algoritmo.
Algumas últimas observações:
i) No caso de todas as arestas terem peso igual a , pode-se, em vez de fazer o Algoritmo de Dijkstra, simplesmente fazer uma BFS no vértice desejado.
ii) os algoritmos não funcionam no caso de existir arestas com peso negativo.
Tente, agora, resolver os problemas abaixo para colocar em prática seus conhecimentos. Não deixe de tentar resolver também com Dijkstra os problemas que você resolver inicialmente com Floyd-Warshall, para poder melhorar seu treinamento. Caso tenha dificuldade em algum problema ou alguma dúvida sobre a teoria apresentada, volte para a página inicial do curso para preencher o formulário e nos enviar sua dúvida!
Problema 1 - Caminho das Pontes
Problema 3 - Lanche na Empresa
Problema 6 - Empresa de Telecom
As aulas do Curso Noic de Informática são propriedade do Noic e qualquer reprodução sem autorização prévia é terminantemente proibida. Se você tem interesse em reproduzir algum material do Curso Noic de Informática para poder ministrar aulas, você pode nos contatar por esta seção de contato para que possamos fornecer materiais específicos para reprodução.