Aula 18 - Distância em Grafos Valorados: Heaps e Dijkstra Otimizado
Universidade Federal do Rio Grande do Sul
Instituto de Informática
Departamento de Informática Teórica
Estes slides utilizam conteúdo adaptado da bibliografia da disciplina e também de notas de aula e slides prévios dos professores Bruno Grisci, Rodrigo Machado e André Grahl.
What is the shortest way to travel from Rotterdam to Groningen, in general: from given city to given city. It is the algorithm for the shortest path, which I designed in about twenty minutes. One morning I was shopping in Amsterdam with my young fiancée, and tired, we sat down on the café terrace to drink a cup of coffee and I was just thinking about whether I could do this, and I then designed the algorithm for the shortest path. As I said, it was a twenty-minute invention. In fact, it was published in ’59, three years later. The publication is still readable, it is, in fact, quite nice. One of the reasons that it is so nice was that I designed it without pencil and paper. I learned later that one of the advantages of designing without pencil and paper is that you are almost forced to avoid all avoidable complexities. Eventually, that algorithm became to my great amazement, one of the cornerstones of my fame.
— Edsger Dijkstra, in an interview with Philip L. Frana, Communications of the ACM, 2001[5]
Exemplos
Fila (Queue): usada na Busca em Largura (BFS)
Organiza os elementos em ordem sequencial, onde inserir no final e remover no início custam \mathcal{O}(1).Pilha (Stack): usada na Busca em Profundidade (DFS)
Permite inserir e remover do topo em tempo constante \mathcal{O}(1).
A escolha da estrutura certa é metade do caminho para um bom algoritmo.
Princípio da Parcimônia
Escolha a estrutura de dados mais simples que suporte todas as operações necessárias da sua aplicação.
Problema do Caminho Mínimo com Origem Única
Entrada:
- Um grafo direcionado G=(V,E)
- Cada arco tem um custo não negativo l_e para e\in E.
- Vértice de origem s.
Saída: Para cada v\in V:
- L(v)= custo do menor caminho entre s-v em G (distância).
Importante:
P. Qual o tempo de execução?
\mathcal{O}(mn).
P. Quais as operações que precisamos?
| Operação | Tempo de Execução |
|---|---|
Inserir (Insert) |
\mathcal{O}(\log n) |
Extrair Mínimo (ExtractMin) |
\mathcal{O}(\log n) |
Encontrar Mínimo (FindMin) |
\mathcal{O}(1) |
Heapificar (Heapify) |
\mathcal{O}(n) |
Remover (Delete) |
\mathcal{O}(\log n) |
Insert e ExtractMin.Também é possível implementar um heap para encontrar o maior valor de forma análoga.
Ideia Intuitiva
Uma primeira ideia seria armazenar as arestas do grafo no heap, substituindo as buscas de mínimo por chamadas a
ExtractMin.
- Essa abordagem funciona, mas há uma versão mais simples e eficiente: armazenar os vértices.
Invariante
Para w \in \textcolor{#6b777d}{V - X}, \textcolor{#9e0300}{\text{dist}}(w) é uma estimativa: o menor score de Dijkstra de uma aresta com início v \in \textcolor{#2e75b6}{X} e fim w, ou +\infty se nenhuma aresta assim existir. \textcolor{#9e0300}{\text{dist}}(w) = \min_{(v, w) \in E,\; v \in \textcolor{#2e75b6}{X}} \underbrace{\big\{\, \textcolor{#9e0300}{\text{dist}}(v) + \ell_{vw} \,\big\}}_{\text{score de Dijkstra}} Para v \in \textcolor{#2e75b6}{X}, \textcolor{#9e0300}{\text{dist}}(v) já é a distância final d(s, v), computada em uma iteração anterior.
Conteúdo do Heap
- Se \textcolor{#9e0300}{\text{dist}}(w) < +\infty, o heap contém o par (\textcolor{#9e0300}{\text{dist}}(w), w).
- O heap também pode conter pares obsoletos (d, w): com w \in \textcolor{#2e75b6}{X}, ou com d > \textcolor{#9e0300}{\text{dist}}(w).
Pares obsoletos de w \in \textcolor{#6b777d}{V - X} têm d > \textcolor{#9e0300}{\text{dist}}(w) e não saem antes do par válido de w.
Logo, o primeiro par válido extraído é o vértice de \textcolor{#6b777d}{V - X} com menor \textcolor{#9e0300}{\text{dist}}.
Mudança da Fronteira
A cada iteração, um vértice u é movido de \textcolor{#6b777d}{V - X} para \textcolor{#2e75b6}{X}, alterando a fronteira:
- Arestas de \textcolor{#2e75b6}{X} para u deixam de cruzar a fronteira.
- Arestas de u para vértices em \textcolor{#6b777d}{V - X} passam a cruzar a fronteira.
Consequência
O invariante exige que, para cada w \in \textcolor{#6b777d}{V - X}, \textcolor{#9e0300}{\text{dist}}(w) = \min_{(v,w) \in E,\; v \in \textcolor{#2e75b6}{X}} \underbrace{\big\{\, \textcolor{#9e0300}{\text{dist}}(v) + \ell_{vw} \,\big\}}_{\text{\footnotesize score de Dijkstra}} As novas arestas que cruzam a fronteira (saindo de u) podem reduzir \textcolor{#9e0300}{\text{dist}}(w) de alguns vértices w.
Quando um vértice u entra em \textcolor{#2e75b6}{X}
- As novas arestas cruzando a fronteira partem de u.
- Basta percorrer a lista de adjacência de u e, para cada aresta (u, v): \text{se } \textcolor{#9e0300}{\text{dist}}(u) + \ell_{uv} < \textcolor{#9e0300}{\text{dist}}(v): \quad \textcolor{#9e0300}{\text{dist}}(v) \gets \textcolor{#9e0300}{\text{dist}}(u) + \ell_{uv}
- Se v \in \textcolor{#2e75b6}{X}, o teste sempre falha: \textcolor{#9e0300}{\text{dist}}(v) já é a menor distância possível.
Como diminuir a distância de v no heap?
- Remover o par antigo com
Deleteexige saber onde v está no vetor do heap (um mapa auxiliar vértice → posição).- Mais simples: apenas insira o novo par (\textcolor{#9e0300}{\text{dist}}(v), v) com
Insert. O par antigo de v continua no heap, mas fica obsoleto.- Ao extrair um par (d, u) com u \in \textcolor{#2e75b6}{X}, basta descartá-lo.
Na primeira vez que u é extraído, d = \textcolor{#9e0300}{\text{dist}}(u): cada nova inserção de u tem distância menor que as anteriores, então o par mais recente é o primeiro a sair.
ExtractMin |
Ação | \textcolor{#3a7a10}{H} ao fim da iteração |
|---|---|---|
| — | inicialização: \textcolor{#9e0300}{\text{dist}}(S) = 0 | (0, S) |
| (0, S) | processa S: \textcolor{#9e0300}{\text{dist}}(A) = 1, \textcolor{#9e0300}{\text{dist}}(B) = 3 | (1, A),\ (3, B) |
| (1, A) | processa A: \textcolor{#9e0300}{\text{dist}}(D) = 6, \textcolor{#9e0300}{\text{dist}}(C) = 5 | (3, B),\ (5, C),\ (6, D) |
| (3, B) | processa B: \textcolor{#9e0300}{\text{dist}}(C) = 4 | (4, C),\ \textcolor{#9aa5ad}{(5, C)},\ (6, D) |
| (4, C) | processa C: \textcolor{#9e0300}{\text{dist}}(E) = 10 | \textcolor{#9aa5ad}{(5, C)},\ (6, D),\ (10, E) |
| \textcolor{#9aa5ad}{(5, C)} | obsoleto: C \in \textcolor{#2e75b6}{X}, descarta | (6, D),\ (10, E) |
| (6, D) | processa D: \textcolor{#9e0300}{\text{dist}}(E) = 8 | (8, E),\ \textcolor{#9aa5ad}{(10, E)} |
| (8, E) | processa E: nenhuma melhora | \textcolor{#9aa5ad}{(10, E)} |
| \textcolor{#9aa5ad}{(10, E)} | obsoleto: E \in \textcolor{#2e75b6}{X}, descarta | \emptyset |
F nunca entra no heap: \textcolor{#9e0300}{\text{dist}}(F) = +\infty.
Observação Importante
Quase todo o trabalho na versão com heap do algoritmo de Dijkstra é feito por operações de heap (
InserteExtractMin).
- Cada operação custa \mathcal{O}(\log k), onde k é o número de pares no heap.
Insert) executa sempre que alguma distância diminui;ExtractMin) executa uma vez para cada par inserido, inclusive os obsoletos.P. Quantas inserções podem acontecer no total?
Responsabilidade por Arestas
Cada aresta (u, v) é examinada uma única vez: quando u é processado (linhas 12 a 15). Pares obsoletos são descartados sem percorrer arestas (linhas 9 e 10).
- Cada aresta gera no máximo um
Insert: no máximo m + 1 pares no heap, contando (0, s);- Cada par é extraído no máximo uma vez: no máximo m + 1 chamadas de
ExtractMin.
Complexidade Total
\text{Operações de heap: } \mathcal{O}(m) \qquad \text{Custo por operação: } \mathcal{O}(\log m) = \mathcal{O}(\log n) \text{pois } m \le n^2 \Rightarrow \log m \le 2 \log n \Rightarrow \text{Tempo total (com a inicialização em } \mathcal{O}(n)\text{): } \boxed{\mathcal{O}((m + n)\log n)}
Mais rápido que \mathcal{O}(mn) da implementação sem heap!
Python 3.12.3, Ubuntu 24.04.2 LTS, Intel® Core™ i7-4810MQ × 8
Python 3.12.3, Ubuntu 24.04.2 LTS, Intel® Core™ i7-4810MQ × 8
Conceito
Um heap pode ser visto como uma árvore binária enraizada, em que cada nó possui 0, 1 ou 2 filhos, e cada nível é preenchido o máximo possível.
Organização dos Níveis
A Propriedade dos Heaps
Para cada objeto x, a chave de x é menor ou igual às chaves dos seus filhos.
| Elemento | Posição no Vetor (começando em 1) |
|---|---|
| Pai de i | \lfloor i/2 \rfloor (para i \geq 2) |
| Filho esquerdo de i | 2i (se 2i \leq n) |
| Filho direito de i | 2i + 1 (se 2i + 1 \leq n) |
O Desafio Ao inserir ou remover um elemento, é preciso:
Estratégia Geral
Mantenha a árvore cheia da forma mais óbvia e depois conserte locais que violam a propriedade de heap.
InsertOperação
InsertDado um heap \textcolor{#3a7a10}{H} e um novo elemento x, adicione x a \textcolor{#3a7a10}{H} respeitando essas duas regras.
- Adicione o novo objeto ao final do heap e aumente o tamanho do heap.
- Repetidamente troque o novo objeto pelo seu pai até a propriedade de heap ser restaurada.
Insert: ExemploInsert: ExemploInsert: ExemploInsert: ExemploInsert: ExemploInsert: ExemploInsert e SiftUp em um HeapCada troca reduz o índice pela metade, garantindo tempo total \mathcal{O}(\log n).
Cada troca pode empurrar a violação da propriedade de heap para cima.
ExtractMinOperação
ExtractMinDado um heap \textcolor{#3a7a10}{H}, remova e devolva de \textcolor{#3a7a10}{H} um objeto com a menor chave.
Estratégia
Assim como na operação
Insert, seguimos o método mais direto:
- Pegamos o último nó da árvore;
- Colocamos seu valor na posição da raiz (substituindo o antigo mínimo);
- Em seguida, corrigimos possíveis violações da propriedade de heap.
ExtractMin: ExemploExtractMin: ExemploDuas violações. Qual escolher? Sempre trocar com o menor valor!
ExtractMin: ExemploExtractMin: ExemploExtractMin e SiftDown em um HeapExtractMin e SiftDown em um HeapSiftDown — min-heap, índices a partir de 1:
Cada troca reduz o nível da árvore, garantindo tempo total \mathcal{O}(\log n).
Durante o SiftDown, no máximo dois pares pai–filho violam a propriedade de heap; cada troca empurra o nó uma camada para baixo até que ele alcance o último nível (ou antes), e os demais pares permanecem válidos, garantindo término e corretude.
Tempo: \Theta(n^2)
Complexidade
O trabalho do HeapSort se resume a 2n operações sobre um heap de no máximo n elementos.
Heapify.O que o HeapSort faz?
O HeapSort ordena um vetor inserindo seus elementos em um heap e removendo-os em ordem crescente.
Lembrando…
- Heapify transforma um vetor qualquer (não ordenado) em um heap válido.
- Cada operação de heap (
Insert,ExtractMin) custa \mathcal{O}(\log n).- O HeapSort completo tem complexidade \mathcal{O}(n \log n).
Desafio
Se o HeapSort é \mathcal{O}(n \log n), como o Heapify pode ser \mathcal{O}(n)?
Ver: https://github.com/BrunoGrisci/projeto-e-analise-de-algoritmos/blob/main/paa1/dijkstra.ipynb
Algoritmo de Bellman-Ford: resolve o problema para grafos com um vértice de origem e arestas que podem ter pesos negativos.
Algoritmo de Floyd-Warshall: determina a distância entre todos os pares de vértices de um grafo com arestas que podem ter pesos negativos (mas não ciclo negativos).
Algoritmo de Johnson: determina a distância entre todos os pares de vértices de um grafo em grafos esparsos. Pode haver arestas negativas, mas não ciclos negativos.
Caminhos mínimos em DAGs: grafo acíclico dirigido.’’
Algoritmo A^*: um algoritmo de busca heurística que calcula o caminho mínimo com um vértice de origem.
?