Estruturas de Dados

Grafos

Aula interativa Prof. Raul de Araújo Lima · Sistemas de Informação · UniCatólica

Python carregando…

Grafos

Redes sociais, mapas, a internet e a grade de disciplinas do curso têm a mesma forma: coisas ligadas a outras coisas. Hoje vamos representar essas ligações no computador e percorrê-las para responder perguntas como “qual o caminho mais curto?” e “em que ordem fazer?”.

Navegação: ← → trocam de etapa · V alterna entre etapas e página única · M sumário · F tela cheia · + − tamanho do texto · dentro do código, Ctrl+Enter executa.

Todo código desta aula pode ser editado e executado aqui mesmo. Altere os exemplos à vontade; o botão ↺ restaura o original. Suas alterações ficam salvas neste navegador; em Progresso você pode baixá-las em um arquivo e continuar em outro computador.

As visualizações mostram os algoritmos passo a passo: escolha a origem, use Próximo passo ou Animar. Nos exercícios, o botão ✓ Verificar testa seu código com várias entradas.

O que é um grafo?

Um grafo G = (V, E) é um conjunto de vértices (V, os objetos) ligados por arestas (E, as relações entre pares de objetos).

SituaçãoVérticesArestas
rede socialpessoasamizades ou “segue”
mapacidades, esquinasestradas, ruas (com distância)
cursodisciplinaspré-requisitos
internetpáginas, roteadoreslinks, cabos

Listas e árvores são casos particulares de grafos. A diferença é que, num grafo geral, qualquer vértice pode se ligar a qualquer outro, inclusive formando ciclos.

Desenhe essa rede no quadro com a turma: 5 vértices e quantas arestas? Quem tem mais amigos?

Vocabulário dos grafos

TermoSignificado
não dirigidoa aresta vale nos dois sentidos (amizade)
dirigido (dígrafo)a aresta tem sentido u → v (“segue”, pré-requisito)
ponderadocada aresta tem um peso (distância, custo, tempo)
graunúmero de arestas de um vértice; no dirigido, grau de entrada e de saída
caminhosequência de vértices ligados por arestas
ciclocaminho que volta ao vértice inicial
conexoexiste caminho entre quaisquer dois vértices
árvoregrafo conexo e sem ciclos: tem exatamente V − 1 arestas
soma dos graus (não dirigido) = 2Esoma dos graus de saída = E

Três formas de guardar o mesmo grafo

O desenho é só para nós. O programa precisa de uma estrutura: matriz de adjacência, lista de adjacência ou lista de arestas. Clique em um vértice e compare.

Troque para dirigido: a matriz deixa de ser simétrica e cada aresta aparece só uma vez na lista. A lista de arestas é simplesmente [(0, 1), (0, 3), (1, 2), (4, 1), …].

Matriz de adjacência

Uma matriz V × V em que m[u][v] = 1 se existe a aresta u–v (ou o peso dela, num grafo ponderado), e 0 caso contrário.

  • Existe a aresta u–v? Basta olhar m[u][v]: O(1).
  • Quais os vizinhos de v? Percorre a linha toda: O(V), mesmo que v tenha 1 vizinho.
  • Memória: V² posições, sempre.

Num grafo não dirigido a matriz é simétrica: m[u][v] == m[v][u].

Lista de adjacência

Para cada vértice, uma lista só com os seus vizinhos. Em Python, um dict de listas (ou uma lista de listas, se os vértices forem 0, 1, …, V−1).

  • Vizinhos de v: direto, O(grau de v). É o que os percursos mais usam.
  • Existe u–v? Procura na lista de u: O(grau de u).
  • Memória: O(V + E).

Num grafo ponderado, cada vizinho vira um par (vizinho, peso).

A lista de arestas ([(u, v), …]) é o formato mais simples de ler da entrada e de ordenar por peso, mas responder “quem são os vizinhos de v” exige olhar todas as E arestas.

Qual representação usar?

OperaçãoMatrizLista de adjacênciaLista de arestas
memóriaO(V²)O(V + E)O(E)
existe u–v?O(1)O(grau u)O(E)
vizinhos de vO(V)O(grau v)O(E)
percorrer o grafo todoO(V²)O(V + E)—

A maioria dos grafos reais é esparsa: cada vértice tem poucos vizinhos comparado com V. Uma rede com 1 milhão de pessoas e 100 amigos por pessoa teria uma matriz de 10¹² posições, quase todas zero.

Regra prática: lista de adjacência por padrão; matriz para grafos pequenos ou densos, ou quando a pergunta “existe u–v?” domina.

Lendo um grafo da entrada

Formato usado nos exercícios desta aula (e em maratonas de programação):

  • primeira linha: V E — número de vértices e de arestas;
  • depois, E linhas u v (ou u v peso);
  • os vértices são numerados de 0 a V − 1.

Como os vértices são números, uma lista de listas basta: g[u] é a lista de vizinhos de u.

Ordenar cada lista de vizinhos deixa a saída determinística: os percursos sempre visitam os vizinhos do menor para o maior. Os exercícios contam com isso.

Busca em largura (BFS)

A BFS explora o grafo em ondas: primeiro a origem, depois todos os vizinhos dela, depois os vizinhos dos vizinhos… Uma fila garante essa ordem.

Os números acima dos vértices são as distâncias em número de arestas a partir da origem. Troque a origem para 6 e preveja a ordem de visita antes de animar.

BFS em Python

  1. Coloque a origem na fila com distância 0.
  2. Enquanto a fila não estiver vazia, tire o primeiro vértice v.
  3. Para cada vizinho w ainda não descoberto: dist[w] = dist[v] + 1 e w entra no fim da fila.

Cada vértice entra na fila uma vez e cada aresta é olhada uma vez (duas, se não dirigido): O(V + E).

Use collections.deque: popleft() é O(1). Com uma list, pop(0) desloca todos os elementos (O(n)) e a BFS fica quadrática.

Teste de mesa da BFS

Acompanhe a fila e o dicionário dist. Repare que um vértice entra em dist no momento em que é descoberto, não quando sai da fila; por isso o 3 não entra duas vezes.

Busca em profundidade (DFS)

A DFS faz o contrário: segue um caminho o mais fundo possível e só volta (backtracking) quando não há vizinho novo. É o jeito de explorar um labirinto com a mão na parede.

Compare com a BFS a partir do mesmo vértice: os vértices visitados são os mesmos, a ordem e as arestas usadas mudam. Os números agora indicam a ordem de visita.

DFS recursiva e com pilha

A versão recursiva é a mais curta: visitar v é marcar v e chamar a DFS para cada vizinho não visitado. A pilha de chamadas do Python guarda o caminho de volta.

Em grafos grandes, a recursão pode estourar o limite do Python (cerca de 1000 chamadas). Aí trocamos a pilha de chamadas por uma pilha explícita (list com append/pop).

BFSDFS
estruturafila (FIFO)pilha (LIFO) ou recursão
explorapor camadas de distânciaum caminho até o fim
bom paramenor caminho sem pesoscomponentes, ciclos, ordem topológica
custoO(V + E)O(V + E)

Componentes conexas

Um grafo pode estar dividido em “ilhas” sem ligação entre si: as componentes conexas.

Para encontrá-las, percorra a partir de cada vértice ainda não visitado. Cada novo percurso (BFS ou DFS, tanto faz) descobre uma componente inteira.

Apesar do laço sobre todos os vértices, cada vértice e cada aresta são tratados uma vez só no total: O(V + E).

Aplicações: grupos de amigos, regiões de uma imagem com a mesma cor (o “balde de tinta” dos editores), verificar se uma rede de computadores está toda ligada.

Detectando ciclos

Num grafo não dirigido, durante a DFS, encontrar um vizinho já visitado que não é o vértice de onde viemos significa que há dois caminhos até ele: existe um ciclo.

Por isso a pilha guarda pares (vértice, pai): a aresta de volta para o pai não conta como ciclo.

Uma árvore é justamente um grafo conexo sem ciclos. Ela tem sempre V − 1 arestas: uma a menos e ficaria desconexa, uma a mais e formaria um ciclo.

Em grafos dirigidos a regra muda (um vértice visitado por outro ramo não forma ciclo). Veremos um jeito simples de detectar ciclos dirigidos na ordenação topológica.

Qual estrutura, qual algoritmo?

Leia a situação para a turma, colete as respostas e só então clique.

Menor caminho sem pesos

Na BFS, um vértice é descoberto pela primeira vez pelo caminho com menos arestas. Guardando quem descobriu cada vértice (o pai), dá para reconstruir esse caminho.

Para montar a rota até t, siga os pais de t até a origem e inverta a lista.

Funciona quando todas as arestas “custam” o mesmo: número de estações, de conexões entre pessoas, de cliques entre páginas.

Se o destino for encontrado, dá para parar a BFS ali mesmo (o break), pois a distância dele já é definitiva.

Labirintos: grafos implícitos

Nem sempre o grafo aparece como lista de arestas. Num mapa em grade, cada casa livre é um vértice e seus vizinhos são as casas livres acima, abaixo, à esquerda e à direita.

Não precisamos montar a lista de adjacência: calculamos os vizinhos na hora, somando os deslocamentos (±1, 0) e (0, ±1) e conferindo se a casa existe e não é parede.

A BFS a partir do início espalha as distâncias como uma mancha de tinta (flood fill). O mapa impresso mostra, em cada casa, quantos passos ela está do S.

Experimente abrir ou fechar paredes (#) no mapa e veja o número de passos até E mudar.

Arestas com peso: algoritmo de Dijkstra

Com pesos diferentes, o caminho com menos arestas pode não ser o mais curto. O Dijkstra sempre fixa o vértice pendente mais próximo da origem e tenta melhorar (relaxar) as distâncias dos vizinhos dele.

Repare no vértice 1: a aresta direta 0–1 custa 4, mas o caminho 0 → 2 → 1 custa 1 + 2 = 3. O Dijkstra descobre isso ao relaxar as arestas do 2.

Dijkstra em Python

  1. dist[s] = 0 e ∞ para os demais; a fila de prioridade começa com (0, s).
  2. Tire o par de menor distância (d, u). Se d for maior que dist[u], é uma entrada antiga: ignore.
  3. Para cada vizinho v com peso p: se dist[u] + p < dist[v], atualize e coloque (dist[v], v) na fila.

O heapq implementa a fila de prioridade (um heap, visto na aula de ordenação). Custo: O((V + E) log V).

O Dijkstra exige pesos não negativos. Com pesos negativos, uma distância “definitiva” poderia melhorar depois, e o algoritmo erra. Nesse caso usa-se Bellman-Ford.

Ordenação topológica

Em que ordem cursar disciplinas com pré-requisitos? Num grafo dirigido sem ciclos (DAG), uma ordem topológica coloca cada vértice depois de todos os que apontam para ele.

Algoritmo de Kahn: repetidamente retire um vértice com grau de entrada 0 e apague as setas que saem dele. Troque para o grafo com ciclo e veja onde o algoritmo trava.

Algoritmo de Kahn em Python

  1. Calcule o grau de entrada de cada vértice.
  2. Coloque os de grau 0 em prontos.
  3. Retire um pronto, acrescente-o à ordem e diminua o grau de entrada de cada vizinho; quem chegar a 0 fica pronto.
  4. Se a ordem terminar com menos de V vértices, há um ciclo.

Pode haver várias ordens válidas. Usar um heapq em prontos escolhe sempre o menor número, deixando a resposta única. Custo: O(V + E), ou O((V + E) log V) com o heap.

Outras aplicações: ordem de compilação de arquivos, instalação de pacotes com dependências, etapas de um projeto, células de uma planilha que dependem de outras.

Desafio: quantas ilhas?

No mapa, "1" é terra e "0" é água. Uma ilha é um grupo de terras ligadas na horizontal ou na vertical (diagonal não conta).

Conte quantas ilhas existem. Para o mapa ao lado, a resposta é 5.

É o problema das componentes conexas num grafo implícito em grade: para cada terra ainda não visitada, conte uma ilha nova e “pinte” toda a ilha com uma BFS ou DFS.

vizinhos de (i, j) → (i±1, j), (i, j±1)
Mostrar uma solução

Qual será a saída?

Leia o código, escreva sua resposta e só então clique. Errou? Copie o código para o laboratório e execute.

Prática 1: representação e componentes

Nos exercícios, o grafo vem no formato V E seguido de E linhas u v, com vértices de 0 a V − 1. ✓ Verificar testa seu código com vários grafos, inclusive sem arestas.

1. Graus dos vértices básico

Leia um grafo não dirigido e mostre o grau de cada vértice (Grau de v: g, de 0 a V − 1). Por fim, mostre Maior grau: g (vértice v); em caso de empate, o vértice de menor número.

EntradaSaída esperada
5 5
0 1
0 2
1 2
1 3
3 4
Grau de 0: 2
Grau de 1: 3
Grau de 2: 2
Grau de 3: 2
Grau de 4: 1
Maior grau: 3 (vértice 1)
Mostrar uma solução

2. Componentes conexas médio

Leia um grafo não dirigido e mostre quantas componentes conexas ele tem (Componentes: k) e quantos vértices tem a maior delas (Maior componente: m vértices). Vértices isolados formam componentes de 1 vértice.

EntradaSaída esperada
6 3
0 1
1 2
3 4
Componentes: 3
Maior componente: 3 vértices
Mostrar uma solução

Prática 2: BFS e menor caminho

Os dois exercícios pedem o menor número de passos: é a BFS, num grafo lido da entrada e num grafo implícito em grade.

3. Distância entre dois vértices básico

Leia um grafo não dirigido e, na última linha, dois vértices s t. Mostre Distância: d, o menor número de arestas de s até t, ou Sem caminho.

EntradaSaída esperada
6 6
0 1
0 2
1 3
2 3
3 4
4 5
0 5
Distância: 4
Mostrar uma solução

4. Saída do labirinto médio

Leia L C e depois L linhas do mapa: . livre, # parede, S início e E saída. Andando nas 4 direções, mostre Passos: d (o menor número de passos de S até E) ou Sem saída.

EntradaSaída esperada
3 4
S..E
.##.
....
Passos: 3
Mostrar uma solução

Prática 3: grafos dirigidos e ponderados

Agora as arestas têm sentido: a linha u v significa apenas u → v.

5. Ordem das disciplinas médio

Cada linha u v diz que a disciplina u é pré-requisito de v. Mostre uma ordem para cursar todas (Ordem: a b c …), escolhendo sempre a de menor número entre as disponíveis. Se houver ciclo, mostre Impossível: há ciclo.

EntradaSaída esperada
6 6
0 1
0 2
2 4
3 4
1 5
4 5
Ordem: 0 1 2 3 4 5
Mostrar uma solução

6. Menores distâncias com pesos desafio

Leia um grafo dirigido com linhas u v peso (pesos não negativos) e, na última linha, a origem s. Para cada vértice, de 0 a V − 1, mostre Distância até v: d ou Distância até v: inalcançável. Use o Dijkstra com heapq.

EntradaSaída esperada
3 2
0 1 5
1 2 1
0
Distância até 0: 0
Distância até 1: 5
Distância até 2: 6
Mostrar uma solução

Laboratório livre

Espaço para testar ideias durante a aula. Sugestões: monte o grafo das suas disciplinas e gere uma ordem topológica; descubra se uma rede de amigos tem alguém a mais de 3 “apertos de mão” de você; adapte o labirinto para contar também os movimentos na diagonal.