Algoritmo de Prim para o problema MST

Esta página trata do problema das árvores geradoras de peso mínimo em grafos. Trata-se de uma generalização do problema da árvore geradora discutido no capítulo Árvores geradoras de grafos. O problema desta página é conhecido pelas iniciais MST de minimum spanning tree. A página trata, em particular, do célebre algoritmo de Prim para o problema da MST.

Esta página é inspirada no capítulo 23 de CLRS.

a-250-node-euclidean-graph-and-its-mst-1.png

Árvores geradoras de peso mínimo

Suponha que cada aresta vw de um grafo tem um peso (= weight) numérico f(vw), que pode ser positivo, negativo, ou nulo. Diremos que f é uma função-peso definida sobre os arestas do grafo. O peso de um sub­grafo T do grafo é a soma dos pesos das arestas de T e será denotado por f(T).

Uma MST (= minimum spanning tree) de um grafo com função-peso f é uma árvore geradora T do grafo que minimiza f(T). Considere o seguinte

Problema da MST:  Dado um grafo G com função-peso f nas arestas, encontrar uma MST de G.

Uma instância do problema da MST tem solução se e somente se G é conexo. Por isso, vamos nos restringir desde já a grafos conexos.

Sedgewick-Wayne/trace-of-prims-algorithm-lazy-version-x.png

Exemplo A.  Cada linha representa uma aresta do grafo. O peso de cada aresta é proporcional ao comprimento geométrico do segmento de reta que representa a aresta na figura.

vw)  4 5 4 7 5 7 0 7 1 5 0 4 2 3 1 7 0 2 1 2 1 3 2 7 6 2 3 6 6 0 6 4
f(vw35 37 28 16 32 38 17 19 26 36 29 34 40 52 58 93

O conjunto de arestas  0 7 7 1 7 5 5 4 0 2 2 3 2 6  define uma árvore geradora do grafo. O peso dessa árvore é 35 + 28 + 16 + 17 + 19 + 26 + 40 = 181. Nenhuma outra árvore geradora tem peso menor, embora isso não seja óbvio.

Exercícios 1

  1. Seja r um vértice de um grafo conexo G. Seja f uma função-peso nas arestas de G. Explique a diferença conceitual entre uma árvore geradora de peso mínimo e uma árvore de busca em largura com raiz r? Dê exemplos.
  2. ★ Mostre que o seguinte algoritmo não resolve o problema da MST:  Para cada vértice v, escolha um aresta de peso mínimo dentre as que incidem em v.
  3. Mostre que o seguinte algoritmo não resolve o problema da MST.

    Seja a1, a2, … , am a sequência das arestas doi grafo em ordem crescente de pesos. No início da primeira iteração, pinte um vértice qualquer de preto e os demais vértices de branco. Para i = 1, 2, … , m faça o seguinte: se uma ponta de ai é preta e outra é branca, acrescente ai à árvore e pinte de preto a ponta que era branca.

O algoritmo de Prim

O célebre algoritmo de Prim, descoberto por R. C. Prim em 1957, resolve o problema da MST de maneira eficiente. (E. W. Dijkstra também descobriu o algoritmo.)

Nossa primeira descrição do algoritmo de Prim será feita num nível de abstração bastante alto. O algoritmo é iterativo. Cada iteração começa com uma subárvore T de G. Na primeira iteração, T tem um único vértice e nenhuma aresta. Imagine que os vértices de T são pretos e todos os demais são brancos. Diremos que a franja de T é o conjunto de todas as arestas de G que têm uma ponta preta e outra branca. O processo iterativo pode então ser descrito assim:

enquanto a franja não estiver vazia,
.oo escolha um aresta da franja que tenha peso mínimo,
.oo seja vw a aresta escolhida,
.oo troque T por T + vw.

No fim de cada iteração, a ponta de vw que era branca fica preta e portanto a franja se altera. Quando a franja ficar vazia, T é uma árvore geradora de peso mínimo, a menos que G não seja conexo.

O algoritmo tem caráter guloso, pois escolhe o aresta vw com base em considerações locais, sem qualquer preocupação com o objetivo global de obter uma árvore de peso mínimo.

O algoritmo está correto.  Suponha que G é conexo, pois do contrário a instância do problema não tem solução. Nesse caso, é claro que o algoritmo produz uma árvore geradora. Resta mostrar que essa árvore é ótima, ou seja, tem peso mínimo. Para isso, basta provar que, no início de cada iteração,

T é sub­grafo de alguma MST de G. (*)

Eis um esboço da prova. Suponha que no início de uma certa iteração T é sub­grafo de uma MST M. Durante esta iteração, uma aresta vw será acrescentada a T. Se essa aresta já está em M então T + vw é sub­grafo de M, como queríamos provar.

Sedgewick-Wayne/BasicTree-x1.png

Suponha agora que vw não está em M. A árvore M tem um único caminho, digamos C, de vw. Algum aresta xy de C está na franja de T e portanto f(xy) ≥ f(vw). Observe que

M + vwxy

é uma árvore geradora. O peso dessa nova árvore não é maior que o peso de M. Concluímos que a nova árvore geradora é tão ótima quanto M. Finalmente, observe que T + vw é subgrafo de M + vw − xy.  Assim, no início da próxima iteração, T é subgrafo de uma MST, como queríamos provar.

Exercícios 2

  1. Diga o quê o algoritmo de Prim faz.
  2. Preencha os detalhes do esboço da prova de (*).
  3. [CLRS 23.2-8]  Considere a seguinte ideia que promete resolver problema da MST pelo método da divisão e conquista:

    Seja (W1, W2) uma bipartição do conjunto de vértices tal que W1⎮−⎮W2 vale −1, 0 ou 1.
    Seja T1 uma árvore geradora de peso mínimo no grafo induzido por W1. Seja xy um aresta de peso mínimo dentre as que têm uma ponta em W1 e outra em W2. Seja T2 uma árvore geradora de peso mínimo no grafo induzido por W2. Devolva T1 + xy + T2.

    Esse algoritmo está correto?

Implementação do algoritmo de Prim

Tal como descrito na seção anterior, o algoritmo de Prim é ineficiente, uma vez que recalcula a franja de T a cada iteração (mesmo que ela tenha mudado muito pouco em relação à iteração anterior). Para que o algoritmo seja mais rápido, é preciso inventar uma representação eficiente da franja. É o que faremos a seguir.

No início de cada iteração, temos um conjunto Q de vértices e um vetor chave que associa um número a cada vértice do grafo. (O complemento de Q é o conjunto de vértices da subárvore T que está em construção.) 

No início de cada iteração, a franja é o conjunto de todas as arestas que têm uma ponta no complemento Q de Q e outra em Q. Para cada vértice w de Q, o número chave[w] é o peso da aresta mais leve dentre todas as arestas da franja que têm ponta w. O processo iterativo pode ser descrito assim:

enquanto Q não estiver vazio,
.oo escolha q em Q que minimize chave[q],
.oo remova q de Q,
.oo para cada aresta da forma qw,
.ooooo se chave[w] > f(qw),
.oooooooo faça chave[w] := f(qw).

Para reescrever o algoritmo de maneira mais formal e detalhada, vamos supor que o grafo G é conexo e representado por um vetor Adj de listas de adjacência. O conjunto Q será organizado como uma fila-com-prioridades. A árvore T será representada por um vetor de pais como o da página Distâncias e busca em largura.

Prim (G, f )
11 . V := V(G)
12 . para cada w em V
13 .ooo chave[w] := ∞
14 . escolha qualquer r em V
15 . chave[r] := 0
16 . Q := Nova-Fila-com-Prioridades ( )
17 . para cada w em V
18 .ooo Insere-na-Fila (w, Q)
19 . enquanto Q não está vazia
10 .ooo q := Extraia-Min (Q)
11 .ooo para cada w em Adj[q]
12 .oooooo se w ∈ Q e f(qw) < chave[w]
13 .ooooooooo Diminua-Chave (w, Q, f(qw))
14 .ooooooooo pai[w] := q
15 . B := {vw : v = pai[w], w ∈ V, w ≠ r }
16 . devolva (V, B)

Algumas observações que ajudam a entender o código:

Desempenho.  O consumo de tempo do algoritmo Prim depende da implementação da fila-com-prioridades Q. Uma análise análoga à que fizemos no caso do algoritmo Dijkstra mostra que

Note que o algoritmo Prim é assintoticamente mais rápido com a fila simples que com a fila sofisticada se nos restingirmos a grafos densos. (No caso da fila simples, nem é necessário construir a fila explícitamente: basta examinar todos os vértices de Q em cada iteração.)

Exercícios 3

  1. Execute o algoritmo Prim sobre o grafo definido pela matriz de pesos abaixo, com - representando . Tome r = 2.
      1 2 3 4 5 6 7 8 9
    1 - 3 - 4 5 - - - -
    2 3 - 9 - 6 - - - -
    3 - 9 - - 8 2 - - -
    4 4 - - - 6 - 6 7 -
    5 5 6 8 6 - 9 - 8 -
    6 - - 2 - 9 - - - -
    7 - - - 6 - - - 8 -
    8 - - - 7 8 - 8 - -
    9 - - - - - - - - -
  2. ★ Escreva uma versão do algoritmo Prim que calcule apenas o peso de uma árvore de peso mínimo. Escreva uma versão enxuta, sem variáveis e comandos supérfluos.
  3. Mostre que depois de cada execução da linha 10 de Prim tem-se chave[q] < ∞.
  4. Grafo desconexo.  Modifique a definição de árvore geradora de modo que toda instância do problema da MST (mesmo aquenas em que o grafo é desconexo) tenha solução. Escreva uma versão do algoritmo Prim para essa versão modificada do problema.
  5. Mostre que o algoritmo Prim consome Ω((n+m) lg n) unidades de tempo no pior caso, sendo n o número de vértices e m o número de arestas de G.
  6. ★ Use o algoritmo de Prim para resolver o seguinte problema: Dado um grafo conexo G e uma função-peso sobre os arestas de G, encontrar uma árvore geradora de G que tenha peso máximo.
  7. ★ [CLRS] Matriz de pesos. Seja G um grafo e f uma função-peso definida sobre os arestas de G. A matriz de pesos de G é a matriz M definida da seguinte maneira: para cada par (v, w) de vértices, M[v, w] vale f(vw) se vw é um aresta e vale ∞ em caso contrário. Escreva uma versão do algoritmo Prim para grafos representados por matriz de pesos. Não use uma fila-com-prioridades explícita. (Vale a pena usar uma fila-com-prioridades sofisticada?) Quanto tempo essa versão consome? A versão é linear? linearítmica? quadrática?
  8. [CLRS3] Bottleneck spanning tree. Dê um algoritmo que resolva o seguinte problema: dado um grafo conexo com função-peso f, encontrar uma árvore geradora cujo aresta mais pesado é o mais leve possível, ou seja, encontrar uma árvore geradora cujo peso seja min maxvw f(vw), sendo o mínimo tomado sobre todas as árvores geradoras e o máximo sobre todos os arestas da árvore). (A chain is only as strong as its weakest link.