Projeto e Análise de Algoritmos I

Aula 21 - Árvores Geradoras de Custo Mínimo: Algoritmo de Kruskal e Estrutura Union-Find

Lucas Nunes Alegre

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.

Árvore Geradora Mínima

Relembrando: Árvore Geradora Mínima

Entrada: Um grafo não direcionado G=(V,E) e um custo c_e para cada aresta e\in E.

  • Assume representação por lista de adjacência.


Saída: Uma árvore T=(V,E'), com E'\subseteq E, de custo mínimo (soma dos custos das arestas em E') que alcança todos os vértices.

  • T não possui ciclos.
  • T é conectado – contém um caminho entre todo par de vértices.

Relembrando: Abordagem Gulosa

Abordagem Gulosa: Constrói a solução passo a passo aplicando decisões locais que otimizam algum critério.


P. Qual decisão?

  1. Adiciona a T a aresta com menor custo que não forma um ciclo. (Kruskal, hoje)
  2. Adiciona a T o vértice que minimiza o custo de conexão. (Prim)
  3. Deleta de G a aresta com maior custo mantendo G conectado. (Reverse-Delete, não visto na disciplina.)
  4. Outras regras.


Propriedade do Corte. Considere uma aresta e de G. Suponha que existe um corte(A,B) tal que e é a aresta de menor custo de G que o corta. Então e pertence à árvore geradora mínima de G.

Algoritmo de Kruskal

Algoritmo de Kruskal

\begin{algorithmic} \Procedure{AlgoritmoKruskal}{$G=(V,E)$} \State sort($E$) \Comment{Ordena arestas por ordem crescente de custo.} \State $\textcolor{#3a7a10}{T}=\emptyset$ \For{$i=1$ até $m$} \If{$\textcolor{#3a7a10}{T}\cup e_i$ não tem ciclos} \State Adiciona $e_i$ a $\textcolor{#3a7a10}{T}$. \EndIf \EndFor \State \textbf{Retorne} $\textcolor{#3a7a10}{T}$ \EndProcedure \end{algorithmic}

Usando listas/vetores: O(mn)

Union-Find: O(m\log{}n).

Algoritmo de Kruskal: Exemplo de Execução

Ordem: A–B (1), C–A (1), C–D (2), D–A (2), E–G (2), C–F (3), D–B (3), D–F (4), E–B (4), D–E (5), D–G (5), F–G (6).

1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
1 1 2 3 2 3 5 4 5 4 2 6 A B C D E F G
aresta atual   \textcolor{#3a7a10}{T}   rejeitada
# aresta decisão componentes |\textcolor{#3a7a10}{T}| W(\textcolor{#3a7a10}{T})
1 A–B (1) aceita \{A,B\}\;\{C\}\;\{D\}\;\{E\}\;\{F\}\;\{G\} 1 1
2 C–A (1) aceita \{A,B,C\}\;\{D\}\;\{E\}\;\{F\}\;\{G\} 2 2
3 C–D (2) aceita \{A,B,C,D\}\;\{E\}\;\{F\}\;\{G\} 3 4
4 D–A (2) rejeita (ciclo) \{A,B,C,D\}\;\{E\}\;\{F\}\;\{G\} 3 4
5 E–G (2) aceita \{A,B,C,D\}\;\{E,G\}\;\{F\} 4 6
6 C–F (3) aceita \{A,B,C,D,F\}\;\{E,G\} 5 9
7 D–B (3) rejeita (ciclo) \{A,B,C,D,F\}\;\{E,G\} 5 9
8 D–F (4) rejeita (ciclo) \{A,B,C,D,F\}\;\{E,G\} 5 9
9 E–B (4) aceita \{A,B,C,D,E,F,G\} 6 13
Fim: |T| = 6 = |V|-1; AGM com peso W(\textcolor{#3a7a10}{T}) = 13. Na aula anterior, Prim encontrou A\text{–}D no lugar de C\text{–}D: AGMs não são necessariamente únicas.

Visualização do Algoritmo de Kruskal

lucasalegre.github.io/mst-visualizer  ·  visualgo.net/en/mst

Corretude do Algoritmo de Kruskal

Corretude do Algoritmo de Kruskal

Invariante do algoritmo. Em cada passo, o conjunto T construído por Kruskal é um subconjunto de alguma Árvore Geradora Mínima.

Justificativa com a propriedade do corte.

  • Quando Kruskal escolhe uma aresta e=(u,v), ela conecta duas componentes distintas A e B da floresta atual.
  • Essa escolha define um corte no grafo: corte(A,B).
  • A aresta e é a menor entre as que cruzam esse corte, por ordem crescente.
  • Pela Propriedade do corte, e é segura: está em alguma AGM.

Conclusão.

  • Cada aresta adicionada é segura, então o conjunto T está contido em uma AGM.
  • Quando T tem n-1 arestas, ele é uma árvore. Portanto, T é uma AGM. \blacksquare

Union-Find

Union-Find: Motivação

No Kruskal: para cada aresta e=(u,v), precisamos saber se \textcolor{#3a7a10}{T}\cup\{e\} tem ciclo.

A B C D E F ciclo! ok

Observação. \textcolor{#3a7a10}{T} é sempre uma floresta. Então

T\cup\{(u,v)\} tem ciclo \iff u e v já estão na mesma componente de T.

Basta manter a partição de V nas componentes de T e saber:

  • consultar se dois vértices estão no mesmo conjunto;
  • unir dois conjuntos quando aceitamos uma aresta.
Operação O que faz No Kruskal
Inicialize(V) cria n conjuntos unitários \{v\} T=\emptyset: cada vértice isolado
Find(v) devolve o representante do conjunto de v ciclo \iff Find(u) = Find(v)
Union(u,v) une os conjuntos de u e de v aceitar (u,v) funde duas componentes

Union-Find: Representação

Ideia: cada conjunto é uma árvore enraizada em que cada elemento aponta para o seu pai. A raiz é o representante do conjunto.

1 2 3 4 5 6 7 v pai 1 1 2 1 3 1 4 2 5 4 6 6 7 6
  • Um único vetor pai[1..n].
  • pai[v] = v \iff v é raiz (representante).
  • Seguindo pai a partir de v, chegamos ao representante do conjunto de v.

Atenção: as árvores da Union-Find não são as árvores de \textcolor{#3a7a10}{T}: elas só registram quem está junto com quem.

\begin{algorithmic} \Procedure{Inicialize}{$V$} \For{$v \in V$} \State $\texttt{pai[v]} \gets v$ \Comment{$n$ raízes} \EndFor \EndProcedure \end{algorithmic}

Union-Find: Find e Union (Versão Simples)

1 2 3 4 5 6 7 Find(5) devolve 1
\begin{algorithmic} \Procedure{Find}{$v$} \While{$\texttt{pai[v]} \neq v$} \State $v \gets \texttt{pai[v]}$ \Comment{sobe um nível} \EndWhile \State \textbf{Retorne} $v$ \EndProcedure \end{algorithmic}
\begin{algorithmic} \Procedure{Union}{$u, v$} \State $r \gets \textbf{Find}(u)$; $s \gets \textbf{Find}(v)$ \If{$r \neq s$} \State $\texttt{pai[r]} \gets s$ \Comment{pendura $r$ em $s$} \EndIf \EndProcedure \end{algorithmic}

Custo: Find e Union custam O(\text{altura da árvore}).

P. Qual pode ser a altura de uma árvore?

Union-Find: Árvores Degeneradas

A versão simples não controla a altura. Considere a sequência

\texttt{Union}(1,2),\ \texttt{Union}(1,3),\ \texttt{Union}(1,4),\ \dots,\ \texttt{Union}(1,n).

⋯ 1 2 3 4 5 n altura n − 1: Find(1) percorre todos os n nós
  • Union(1,k) executa Find(1), que percorre k-1 ponteiros: total \Theta(n^2).
  • Cada Find pode custar \Theta(n) \Rightarrow laço do Kruskal em O(mn), igual a usar listas.

Ideia: ao unir, pendurar a árvore mais baixa na mais alta.

Union-Find: União por Rank

Cada raiz r guarda \texttt{rank[r]}, a altura da sua árvore (inicialmente 0).

Antes de Union(3,6)

1 rank 1 2 3 4 rank 1 5 6

Depois

1 rank 2 2 3 4 5 6
  • Ranks diferentes: a raiz de menor rank aponta para a de maior rank. Nenhum rank muda.
  • Empate: qualquer uma vira a raiz, e o seu rank aumenta em 1. É o único caso em que a altura cresce.

União por Rank: Altura O(\log n)

Lema. Com união por rank, uma árvore cuja raiz tem rank k tem altura k e pelo menos 2^k nós.

Prova. Indução no número de operações Union.

  • Base: após Inicialize, cada árvore tem rank 0, altura 0 e 1 = 2^0 nó.
  • Ranks k_1 < k_2: a árvore menor fica abaixo da raiz de rank k_2; a nova altura é \max(k_1+1,\, k_2) = k_2. Rank e altura não mudam, e o número de nós só cresce.
  • Empate k_1 = k_2 = k: a nova raiz tem rank k+1 e altura k+1, com pelo menos 2^k + 2^k = 2^{k+1} nós. \blacksquare

Corolário. 2^k \le n \Rightarrow k \le \log_2 n. Toda árvore tem altura \le \log_2 n, então Find e Union custam O(\log n).

No Kruskal: 2m Find + (n-1) Union = O(m \log n), mesmo custo da ordenação.

Union-Find: Compressão de Caminho

Ideia: o Find já percorre o caminho até a raiz; aproveitamos para fazer todos os nós visitados apontarem diretamente para ela.

Antes de Find(8)

1 4 5 6 7 8 9

Depois

1 4 5 6 7 8 9
\begin{algorithmic} \Procedure{Find}{$v$} \Comment{com compressão} \If{$\texttt{pai[v]} \neq v$} \State $\texttt{pai[v]} \gets \textbf{Find}(\texttt{pai[v]})$ \EndIf \State \textbf{Retorne} $\texttt{pai[v]}$ \EndProcedure \end{algorithmic}
  • A subárvore de 9 sobe junto com 7: os próximos Find a partir de 9 também ficam mais curtos.
  • A compressão não atualiza rank: ele passa a ser apenas um limitante superior da altura, e o Lema continua valendo.

Union-Find: Exemplo Passo a Passo

Arestas, na ordem: (1,2),\ (3,4),\ (2,3),\ (5,6),\ (1,4),\ (4,5). Union por rank (empate: a raiz de u vence) + compressão de caminho.

1 rank 0 2 rank 0 3 rank 0 4 rank 0 5 rank 0 6 rank 0 v pai 1 1 2 2 3 3 4 4 5 5 6 6
1 rank 1 2 3 rank 0 4 rank 0 5 rank 0 6 rank 0 v pai 1 1 2 1 3 3 4 4 5 5 6 6
1 rank 1 2 3 rank 1 4 5 rank 0 6 rank 0 v pai 1 1 2 1 3 3 4 3 5 5 6 6
1 rank 2 2 3 4 5 rank 0 6 rank 0 v pai 1 1 2 1 3 1 4 3 5 5 6 6
1 rank 2 2 3 4 5 rank 1 6 v pai 1 1 2 1 3 1 4 3 5 5 6 5
1 rank 2 2 3 4 5 rank 1 6 v pai 1 1 2 1 3 1 4 1 5 5 6 5
1 rank 2 2 3 4 5 6 v pai 1 1 2 1 3 1 4 1 5 1 6 5
extremos da aresta   ponteiro alterado   raiz
# aresta raízes decisão mudança
1 (1,2) 1,\ 2 une \texttt{pai[2]}\gets 1,\ \texttt{rank[1]}\gets 1
2 (3,4) 3,\ 4 une \texttt{pai[4]}\gets 3,\ \texttt{rank[3]}\gets 1
3 (2,3) 1,\ 3 une \texttt{pai[3]}\gets 1,\ \texttt{rank[1]}\gets 2
4 (5,6) 5,\ 6 une \texttt{pai[6]}\gets 5,\ \texttt{rank[5]}\gets 1
5 (1,4) 1,\ 1 rejeita (ciclo) \text{compressão: }\texttt{pai[4]}\gets 1
6 (4,5) 1,\ 5 une \texttt{pai[5]}\gets 1\ \ (\text{rank } 2 > 1)
Passo 5: o \textbf{Find}(4) percorre 4 \to 3 \to 1 e já comprime o caminho. As raízes são iguais, logo (1,4) fecharia um ciclo em \textcolor{#3a7a10}{T}.

Union-Find: Implementação Final

\begin{algorithmic} \Procedure{Inicialize}{$V$} \For{$v \in V$} \State $\texttt{pai[v]} \gets v$ \State $\texttt{rank[v]} \gets 0$ \EndFor \EndProcedure \end{algorithmic}
\begin{algorithmic} \Procedure{Find}{$v$} \Comment{com compressão} \If{$\texttt{pai[v]} \neq v$} \State $\texttt{pai[v]} \gets \textbf{Find}(\texttt{pai[v]})$ \EndIf \State \textbf{Retorne} $\texttt{pai[v]}$ \EndProcedure \end{algorithmic}
\begin{algorithmic} \Procedure{Union}{$u, v$} \Comment{por rank} \State $r \gets \textbf{Find}(u)$ \State $s \gets \textbf{Find}(v)$ \If{$r=s$} \State \textbf{Retorne} falso \EndIf \If{$\texttt{rank[r]} < \texttt{rank[s]}$} \State troque $r$ e $s$ \EndIf \State $\texttt{pai[s]} \gets r$ \If{$\texttt{rank[r]} = \texttt{rank[s]}$} \State $\texttt{rank[r]} \gets \texttt{rank[r]} + 1$ \EndIf \State \textbf{Retorne} verdadeiro \EndProcedure \end{algorithmic}

Union-Find: Resumo de Custos

Estratégia Custo por operação Laço do Kruskal
versão simples O(n) O(mn)
união por rank O(\log n) O(m\log n)
união por rank + compressão O(\alpha(n)) amortizado O(m\,\alpha(n))

Teorema (Tarjan, 1975). Com as duas heurísticas, qualquer sequência de q operações sobre n elementos custa O(q\,\alpha(n)).

  • \alpha(n) é a inversa da função de Ackermann: \alpha(n) \le 4 para qualquer n menor que o número de átomos do universo observável.
  • Amortizado: um Find isolado ainda pode ser caro, mas o custo médio ao longo da sequência é O(\alpha(n)).
  • No Kruskal, a ordenação O(m\log n) já domina: a compressão importa quando as arestas já vêm ordenadas.

Visualização interativa: visualgo.net/en/ufds

Union-Find: Inversa de Ackermann

A_0(j) = j+1, \quad A_k(j) = A_{k-1}^{(j+1)}(j) (A_{k-1} aplicada j+1 vezes), \quad \alpha(n) = \min\{k : A_k(1) \ge n\}.

A_0(1) = 2,\quad A_1(1) = 3,\quad A_2(1) = 7,\quad A_3(1) = 2047,\quad A_4(1) \gg 10^{80}.

0 4 8 12 16 1 4 16 64 256 1024 4096 16384 65536 n (escala logarítmica) log2 n α(n) α(2048) = 4

O próximo degrau, \alpha(n) = 5, só ocorre para n > 10^{80}: na prática, \alpha(n) \le 4.

Kruskal com Union-Find

Kruskal com Union-Find

Entrada: grafo G=(V,E) com custo c_e para cada e\in E.

Saída: árvore geradora mínima T.

\begin{algorithmic} \Procedure{KruskalUF}{$G=(V,E),c$} \State $\textcolor{#3a7a10}{T} \gets \emptyset$ \State ordene $E$ em ordem crescente de custo \State $\mathit{UF} \gets \textbf{Inicialize}(V)$ \Comment{um conjunto por vértice} \For{cada $(u,v) \in E$, na ordem escolhida} \If{$\mathit{UF}.\textbf{Find}(u) \neq \mathit{UF}.\textbf{Find}(v)$} \State $\textcolor{#3a7a10}{T} \gets \textcolor{#3a7a10}{T} \cup \{(u,v)\}$ \State $\mathit{UF}.\textbf{Union}(u,v)$ \EndIf \If{$|\textcolor{#3a7a10}{T}| = |V|-1$} \State \textbf{Retorne} $\textcolor{#3a7a10}{T}$ \EndIf \EndFor \State \textbf{Retorne} $\textcolor{#3a7a10}{T}$ \EndProcedure \end{algorithmic}

Kruskal com Union-Find: Complexidade

Etapa Custo Justificativa
ordenar as arestas O(m\log m) ordenação por custo
inicializar Union-Find O(n) um conjunto por vértice
percorrer as arestas O(m) uma passada por E
operações Union-Find O(m\alpha(n)) O(\alpha(n)) amortizado por operação

No laço, há no máximo 2m chamadas de Find e no máximo n-1 chamadas de Union. T(n,m) = O(m\log m) + O(n) + O(m\alpha(n)).

Como \alpha(n) cresce extremamente devagar, o gargalo é a ordenação. Em grafo simples, m \leq n^2, logo \log m \leq 2\log n.

\boxed{T(n,m)=O(m\log m)=O(m\log n)}

\text{Se as arestas já estão ordenadas: } O(m\alpha(n)+n).

Implementação em Python

Implementação em Python

Ver também: arvore_geradora_minima.ipynb

Estado-da-Arte

Estado-da-Arte

Ano Algoritmo Complexidade
1975 Yao O(m\log\log n)
1976 Cheriton-Tarjan O(m\log\log n)
1984 Fredman-Tarjan O(m\log^{*} n)
1997 Chazelle O(m\alpha(n)\log\alpha(n))
2000 Chazelle O(m\alpha(n))
2002 Pettie-Ramachandran Ótimo
? ? O(m)


Observação: O(m) algoritmo randomizado por Karger-Klein-Tarjan (1995).

?