Algoritmos em Grafos
Algoritmos em Grafos
Algoritmos em Grafos
Um grafo é uma estrutura matemática que descreve conexões entre objetos. É comum
representarmos um grafo geometricamente. Os objetos (chamados de nós ou vértices) são
representados por pontos e a existência de conexões entre eles (chamadas de arestas ou
arcos) são representadas por linhas.
Exemplo: Ana, Beatriz, Carlos e Dora estão em um grupo de WhatsApp. Ana e Beatriz, Ana e
Carlos, e Carlos e Dora se conhecem pessoalmente. Utilizando as iniciais dos nomes para
nomear as pessoas, essas conexões são representadas pelo grafo de vértices A, B, C e D e de
arestas AB, BC e CD, representado geometricamente abaixo:
No exemplo anterior, as conexões entre os objetos são mútuas (ou seja, correspondem a um
par não ordenado de objetos). Há situações, no entanto, em que é desejável modelar conexões
de um objeto para outro (e não conexões entre objetos). Neste caso, essas conexões
correspondem a um par ordenado de objetos e são representados por uma seta; grafos assim
são chamados de grafos orientados ou direcionados.
Exemplo: O grafo abaixo ilustra as ruas de um bairro. Os vértices são as esquinas e as arestas
correspondem a ruas e indicam o sentido permitido de tráfego. Quando a rua é de mão dupla,
há setas em ambos os sentidos.
Em muitas situações envolvendo grafos, é natural associar um valor a cada aresta do grafo. Na
situação anterior, podemos, por exemplo, associar a cada aresta o tempo gasto no
deslocamento ao longo de cada aresta; esta é a informação essencial para o problema de
encontrar a rota ótima entre dois pontos em uma cidade, que resolvemos diariamente em
nossos celulares. Examinamos este problema em mais detalhe a seguir.
2. O problema do caminho mínimo
Dado um grafo direcionado, com um custo associado a cada aresta, desejamos encontrar o
caminho de menor custo conectando um ponto de origem e um ponto de destino. No caso da
rota ótima em uma cidade, o custo associado a uma aresta é o tempo de deslocamento ao
longo dela; em outras situações, pode ser o comprimento ou o custo de uma passagem entre
os extremos da aresta. A figura abaixo mostra um exemplo, em que o vértice de origem é A e o
de destino é F.
Embora tenhamos dito acima que temos um grafo direcionado, o problema se aplica também a
grafos não direcionados, já que essa situação é um caso particular do anterior, bastando
considerar que a cada aresta não direcionada correspondem arestas direcionadas em ambas as
direções.
O problema do caminho mínimo admite uma solução por força-bruta, que consiste em
enumerar cada sequência de pontos intermediários entre A e B, verificar se cada uma delas é
possível e, em caso afirmativo, calcular seu custo. No caso do nosso exemplo, em que há 6
vértices, a tabela abaixo mostra a quantidade de caminhos a serem examinados e quais deles
de fato existem no grafo, com seu custo.
Mas será que este é um algoritmo prático para resolver problemas de tamanho real? Para
responder a esta pergunta, vamos contar quantos caminhos o algoritmo tem que examinar em
um grafo com |V| vértices, em que há n = |V| ─ 2 possíveis intermediários. Vamos contar
separadamente os caminhos com as diversas possibilidades para o número de vértices
intermediários.
𝑛!
• Passando por n ─3 vértices: 𝑛. (𝑛 ─1). …. 4 = 3!
• ...
𝑛!
• Passando por 1 vértice: 𝑛 = (𝑛−1)!
𝑛!
• Passando por nenhum vértice: 1 = 𝑛!
(
𝑛! 1 + 1 +
1
2!
+
1
3!
+…+
1
𝑛! )
O valor entre parênteses se aproxima do número n quando n cresce. De modo geral, para todo
n > 1, esse valor está entre 2 e 3 e, portanto, o número de caminhos está entre 2n! e 3n!.
5. O Algoritmo de Dijkstra
Este algoritmo, que resolve de modo eficiente o problema de encontrar o caminho mínimo
entre um vértice de origem e um vértice de destino em um grafo, direcionado ou não, com um
custo não negativo associado a cada aresta, foi concebido por Edsger Dijkstra em 1956. Na
verdade, ele acha o melhor caminho para ir da origem a cada um dos demais vértices. A ideia
fundamental é a de que é imediato encontrar o caminho mínimo para um dos vértices, aquele
que está ligado, com custo mínimo, diretamente à origem. No nosso exemplo, a origem é A e o
vértice diretamente ligado à origem com custo mínimo é B. Como os custos são não negativos,
não é possível encontrar um caminho alternativo melhor para B, pois ele passaria
necessariamente por C e teria um custo maior. Esta ideia leva ao seguinte algoritmo:
Algoritmo de Dijkstra
• Atribua valor zero à estimativa do custo mínimo para a origem e infinito às
demais estimativas.
• Considere que todos os vértices estão abertos.
• Enquanto houver vértice aberto:
• seja v um vértice ainda aberto cuja estimativa de custo seja a menor
dentre todos os vértices abertos (no passo inicial, esse vértice é a
origem);
• feche o vértice v
• para todo vértice w ainda aberto para o qual existe a aresta vw:
• some a estimativa do vértice v com o custo da aresta vw;
• caso esta soma seja melhor que a estimativa anterior para o
vértice w, substitua-a e anote v como precedente de w.
Ao final do algoritmo, encontramos o custo do caminho mínimo da origem até cada um dos
demais vértices. Além disso, os caminhos podem ser reconstruídos utilizando o precedente de
cada vértice.
Na aula gravada, fazemos o passo a passo do algoritmo de Dijkstra para o nosso exemplo. As
telas para as diversas etapas são dadas abaixo.
⇒ ⇒
⇒ ⇒
⇒ ⇒
7. Implementação em Python
O algoritmo pode ser facilmente implementado usando uma linguagem de programação.
Abaixo, vemos como fazê-lo em Python. Se desejar, você pode executá-lo em
https://www.onlinegdb.com/online_python_compiler.
# Criando a matriz de adjacências
INF = float('inf')
adj = [[INF, 3, 5, INF, INF, INF],
[INF, INF, 1, 2, 5, INF],
[INF, 2, INF, INF, 3, INF],
[INF, INF, INF, INF, 1, 4],
[INF, INF, INF, 2, INF, 2],
[INF, INF, INF, INF, INF, INF]]
numero_de_vertices = len(adj[0])
fechado = [0]
distancia = [INF]
anterior = [-1]
for i in range(numero_de_vertices-1):
fechado.append(0)
distancia.append(INF)
anterior.append(-1)