Árvores de Abrangência Mínima

Aprenda sobre árvores de abrangência mínima e os algoritmos de Prim e Kruskal para encontrá-las.

Definição

Uma Árvore de Abrangência Mínima (Minimum Spanning Tree - MST) é um subgrafo de um grafo ponderado conectado que:

  • Conecta todos os vértices
  • É uma árvore (sem ciclos)
  • Possui o menor peso total possível entre todas as árvores de abrangência

Propriedades da MST:

  • Conectividade: Liga todos os vértices do grafo original
  • Minimalidade: Possui exatamente V-1 arestas (onde V é o número de vértices)
  • Peso mínimo: A soma dos pesos das arestas é mínima
  • Unicidade: Pode não ser única, mas o peso total sempre é o mesmo

Abaixo implementamos os algoritmos de Prim e Kruskal para encontrar MSTs:

C
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.h>
 
#define MAX_VERTICES 100
#define MAX_ARESTAS 1000
#define INFINITO INT_MAX
 
// Estrutura para representar uma aresta
typedef struct Aresta {
    int destino;
    int peso;
    struct Aresta* proxima;
} Aresta;
 
// Estrutura para representar um vértice
typedef struct Vertice {
    int valor;
    Aresta* adjacentes;
} Vertice;
 
// Estrutura do grafo ponderado
typedef struct GrafoPonderado {
    Vertice vertices[MAX_VERTICES];
    int numVertices;
    bool direcionado;
} GrafoPonderado;
 
// Estrutura para aresta na lista de arestas (Kruskal)
typedef struct ArestaLista {
    int origem;
    int destino;
    int peso;
} ArestaLista;
 
// Estrutura para Union-Find (Kruskal)
typedef struct UnionFind {
    int pai[MAX_VERTICES];
    int rank[MAX_VERTICES];
} UnionFind;
 
// Estrutura para heap mínimo (Prim)
typedef struct {
    int vertice;
    int peso;
} ItemHeap;
 
typedef struct {
    ItemHeap itens[MAX_VERTICES];
    int tamanho;
    int posicao[MAX_VERTICES]; // Mapear vértice para posição no heap
} HeapMinimo;
 
// Funções auxiliares do grafo
GrafoPonderado* criarGrafo(int numVertices, bool direcionado) {
    GrafoPonderado* grafo = (GrafoPonderado*)malloc(sizeof(GrafoPonderado));
    grafo->numVertices = numVertices;
    grafo->direcionado = direcionado;
    
    for (int i = 0; i < numVertices; i++) {
        grafo->vertices[i].valor = i;
        grafo->vertices[i].adjacentes = NULL;
    }
    
    return grafo;
}
 
void adicionarAresta(GrafoPonderado* grafo, int origem, int destino, int peso) {
    Aresta* novaAresta = (Aresta*)malloc(sizeof(Aresta));
    novaAresta->destino = destino;
    novaAresta->peso = peso;
    novaAresta->proxima = grafo->vertices[origem].adjacentes;
    grafo->vertices[origem].adjacentes = novaAresta;
    
    if (!grafo->direcionado) {
        Aresta* arestaReversa = (Aresta*)malloc(sizeof(Aresta));
        arestaReversa->destino = origem;
        arestaReversa->peso = peso;
        arestaReversa->proxima = grafo->vertices[destino].adjacentes;
        grafo->vertices[destino].adjacentes = arestaReversa;
    }
}
 
// ==================== FUNÇÕES DO HEAP MÍNIMO ====================
 
void trocarItens(HeapMinimo* heap, int i, int j) {
    ItemHeap temp = heap->itens[i];
    heap->itens[i] = heap->itens[j];
    heap->itens[j] = temp;
    
    // Atualizar posições
    heap->posicao[heap->itens[i].vertice] = i;
    heap->posicao[heap->itens[j].vertice] = j;
}
 
void heapifyUp(HeapMinimo* heap, int indice) {
    while (indice > 0) {
        int pai = (indice - 1) / 2;
        if (heap->itens[indice].peso >= heap->itens[pai].peso) break;
        
        trocarItens(heap, indice, pai);
        indice = pai;
    }
}
 
void heapifyDown(HeapMinimo* heap, int indice) {
    int menor = indice;
    int esquerda = 2 * indice + 1;
    int direita = 2 * indice + 2;
    
    if (esquerda < heap->tamanho && 
        heap->itens[esquerda].peso < heap->itens[menor].peso) {
        menor = esquerda;
    }
    
    if (direita < heap->tamanho && 
        heap->itens[direita].peso < heap->itens[menor].peso) {
        menor = direita;
    }
    
    if (menor != indice) {
        trocarItens(heap, indice, menor);
        heapifyDown(heap, menor);
    }
}
 
void inserirHeap(HeapMinimo* heap, int vertice, int peso) {
    heap->itens[heap->tamanho].vertice = vertice;
    heap->itens[heap->tamanho].peso = peso;
    heap->posicao[vertice] = heap->tamanho;
    
    heapifyUp(heap, heap->tamanho);
    heap->tamanho++;
}
 
ItemHeap extrairMinimo(HeapMinimo* heap) {
    ItemHeap minimo = heap->itens[0];
    heap->tamanho--;
    
    if (heap->tamanho > 0) {
        heap->itens[0] = heap->itens[heap->tamanho];
        heap->posicao[heap->itens[0].vertice] = 0;
        heapifyDown(heap, 0);
    }
    
    heap->posicao[minimo.vertice] = -1; // Marcar como removido
    return minimo;
}
 
void diminuirChave(HeapMinimo* heap, int vertice, int novoPeso) {
    int pos = heap->posicao[vertice];
    if (pos == -1 || novoPeso >= heap->itens[pos].peso) return;
    
    heap->itens[pos].peso = novoPeso;
    heapifyUp(heap, pos);
}
 
bool heapVazio(HeapMinimo* heap) {
    return heap->tamanho == 0;
}
 
// ==================== ALGORITMO DE PRIM ====================
 
void algoritmoEPrim(GrafoPonderado* grafo, int origem) {
    HeapMinimo heap;
    heap.tamanho = 0;
    
    int chave[MAX_VERTICES]; // Peso mínimo para conectar cada vértice à MST
    int pai[MAX_VERTICES];   // Pai de cada vértice na MST
    bool naMST[MAX_VERTICES]; // Vértices já incluídos na MST
    
    // Inicializar arrays
    for (int i = 0; i < grafo->numVertices; i++) {
        chave[i] = INFINITO;
        pai[i] = -1;
        naMST[i] = false;
        heap.posicao[i] = -1;
    }
    
    // Iniciar com o vértice de origem
    chave[origem] = 0;
    inserirHeap(&heap, origem, 0);
    
    printf("=== Executando Algoritmo de Prim ===\n");
    printf("Vértice inicial: %d\n\n", origem);
    
    ArestaLista mst[MAX_VERTICES - 1]; // Armazenar arestas da MST
    int numArestasMST = 0;
    int pesoTotal = 0;
    
    while (!heapVazio(&heap)) {
        ItemHeap atual = extrairMinimo(&heap);
        int u = atual.vertice;
        
        naMST[u] = true;
        pesoTotal += chave[u];
        
        if (pai[u] != -1) {
            mst[numArestasMST].origem = pai[u];
            mst[numArestasMST].destino = u;
            mst[numArestasMST].peso = chave[u];
            numArestasMST++;
            
            printf("Adicionando aresta (%d, %d) com peso %d à MST\n", 
                   pai[u], u, chave[u]);
        }
        
        // Atualizar chaves dos vértices adjacentes
        Aresta* aresta = grafo->vertices[u].adjacentes;
        while (aresta != NULL) {
            int v = aresta->destino;
            int peso = aresta->peso;
            
            if (!naMST[v] && peso < chave[v]) {
                printf("  Atualizando chave do vértice %d: %d -> %d\n", 
                       v, chave[v], peso);
                
                pai[v] = u;
                chave[v] = peso;
                
                if (heap.posicao[v] == -1) {
                    inserirHeap(&heap, v, peso);
                } else {
                    diminuirChave(&heap, v, peso);
                }
            }
            
            aresta = aresta->proxima;
        }
        printf("\n");
    }
    
    printf("=== Resultado do Algoritmo de Prim ===\n");
    printf("Arestas da MST:\n");
    for (int i = 0; i < numArestasMST; i++) {
        printf("(%d, %d) - peso: %d\n", 
               mst[i].origem, mst[i].destino, mst[i].peso);
    }
    printf("Peso total da MST: %d\n\n", pesoTotal);
}
 
// ==================== UNION-FIND (para Kruskal) ====================
 
void inicializarUnionFind(UnionFind* uf, int n) {
    for (int i = 0; i < n; i++) {
        uf->pai[i] = i;
        uf->rank[i] = 0;
    }
}
 
int encontrar(UnionFind* uf, int x) {
    if (uf->pai[x] != x) {
        uf->pai[x] = encontrar(uf, uf->pai[x]); // Compressão de caminho
    }
    return uf->pai[x];
}
 
bool unir(UnionFind* uf, int x, int y) {
    int raizX = encontrar(uf, x);
    int raizY = encontrar(uf, y);
    
    if (raizX == raizY) {
        return false; // Já estão no mesmo conjunto (criaria ciclo)
    }
    
    // Union by rank
    if (uf->rank[raizX] < uf->rank[raizY]) {
        uf->pai[raizX] = raizY;
    } else if (uf->rank[raizX] > uf->rank[raizY]) {
        uf->pai[raizY] = raizX;
    } else {
        uf->pai[raizY] = raizX;
        uf->rank[raizX]++;
    }
    
    return true;
}
 
// ==================== ALGORITMO DE KRUSKAL ====================
 
int compararArestas(const void* a, const void* b) {
    ArestaLista* arestaA = (ArestaLista*)a;
    ArestaLista* arestaB = (ArestaLista*)b;
    return arestaA->peso - arestaB->peso;
}
 
void obterTodasArestas(GrafoPonderado* grafo, ArestaLista arestas[], int* numArestas) {
    *numArestas = 0;
    
    for (int u = 0; u < grafo->numVertices; u++) {
        Aresta* aresta = grafo->vertices[u].adjacentes;
        while (aresta != NULL) {
            int v = aresta->destino;
            
            // Para grafo não-direcionado, adicionar apenas uma vez (u < v)
            if (grafo->direcionado || u < v) {
                arestas[*numArestas].origem = u;
                arestas[*numArestas].destino = v;
                arestas[*numArestas].peso = aresta->peso;
                (*numArestas)++;
            }
            
            aresta = aresta->proxima;
        }
    }
}
 
void algoritmoKruskal(GrafoPonderado* grafo) {
    ArestaLista arestas[MAX_ARESTAS];
    int numArestas;
    
    // Obter todas as arestas do grafo
    obterTodasArestas(grafo, arestas, &numArestas);
    
    // Ordenar arestas por peso
    qsort(arestas, numArestas, sizeof(ArestaLista), compararArestas);
    
    printf("=== Executando Algoritmo de Kruskal ===\n");
    printf("Arestas ordenadas por peso:\n");
    for (int i = 0; i < numArestas; i++) {
        printf("(%d, %d) - peso: %d\n", 
               arestas[i].origem, arestas[i].destino, arestas[i].peso);
    }
    printf("\n");
    
    // Inicializar Union-Find
    UnionFind uf;
    inicializarUnionFind(&uf, grafo->numVertices);
    
    ArestaLista mst[MAX_VERTICES - 1];
    int numArestasMST = 0;
    int pesoTotal = 0;
    
    printf("Processando arestas:\n");
    
    for (int i = 0; i < numArestas && numArestasMST < grafo->numVertices - 1; i++) {
        int u = arestas[i].origem;
        int v = arestas[i].destino;
        int peso = arestas[i].peso;
        
        printf("Considerando aresta (%d, %d) - peso: %d\n", u, v, peso);
        
        // Verificar se adicionar esta aresta criaria um ciclo
        if (encontrar(&uf, u) != encontrar(&uf, v)) {
            // Não cria ciclo, adicionar à MST
            mst[numArestasMST] = arestas[i];
            numArestasMST++;
            pesoTotal += peso;
            
            unir(&uf, u, v);
            printf("  Aresta ACEITA - Adicionada à MST\n");
        } else {
            printf("  Aresta REJEITADA - Criaria ciclo\n");
        }
        printf("\n");
    }
    
    printf("=== Resultado do Algoritmo de Kruskal ===\n");
    printf("Arestas da MST:\n");
    for (int i = 0; i < numArestasMST; i++) {
        printf("(%d, %d) - peso: %d\n", 
               mst[i].origem, mst[i].destino, mst[i].peso);
    }
    printf("Peso total da MST: %d\n\n", pesoTotal);
}
 
// ==================== COMPARAÇÃO DOS ALGORITMOS ====================
 
void compararAlgoritmosMST(GrafoPonderado* grafo) {
    printf("=== COMPARAÇÃO: PRIM vs KRUSKAL ===\n\n");
    
    printf("ALGORITMO DE PRIM:\n");
    printf("==================\n");
    algoritmoEPrim(grafo, 0);
    
    printf("ALGORITMO DE KRUSKAL:\n");
    printf("=====================\n");
    algoritmoKruskal(grafo);
    
    printf("ANÁLISE COMPARATIVA:\n");
    printf("===================\n");
    printf("Ambos os algoritmos encontram uma MST válida.\n");
    printf("O peso total deve ser o mesmo em ambos os casos.\n");
    printf("As arestas podem ser diferentes se existirem múltiplas MSTs.\n\n");
}
 
// ==================== VERIFICAÇÃO DE MST ====================
 
bool verificarMST(GrafoPonderado* grafo, ArestaLista mst[], int numArestas, int pesoEsperado) {
    printf("=== Verificando se é uma MST válida ===\n");
    
    // Verificar se tem exatamente V-1 arestas
    if (numArestas != grafo->numVertices - 1) {
        printf("ERRO: MST deve ter exatamente %d arestas, mas tem %d\n", 
               grafo->numVertices - 1, numArestas);
        return false;
    }
    
    // Verificar se é conectada usando Union-Find
    UnionFind uf;
    inicializarUnionFind(&uf, grafo->numVertices);
    
    int pesoTotal = 0;
    for (int i = 0; i < numArestas; i++) {
        unir(&uf, mst[i].origem, mst[i].destino);
        pesoTotal += mst[i].peso;
    }
    
    // Verificar se todos os vértices estão conectados
    int raizPrincipal = encontrar(&uf, 0);
    for (int i = 1; i < grafo->numVertices; i++) {
        if (encontrar(&uf, i) != raizPrincipal) {
            printf("ERRO: Grafo não está conectado na MST\n");
            return false;
        }
    }
    
    printf("✓ Número correto de arestas: %d\n", numArestas);
    printf("✓ Grafo está conectado\n");
    printf("✓ Peso total: %d\n", pesoTotal);
    
    if (pesoTotal == pesoEsperado) {
        printf("✓ Peso corresponde ao esperado\n");
    } else {
        printf("⚠ Peso difere do esperado (%d)\n", pesoEsperado);
    }
    
    printf("MST é VÁLIDA!\n\n");
    return true;
}
 
// ==================== APLICAÇÕES ESPECIAIS ====================
 
void encontrarMSTComRestricoes(GrafoPonderado* grafo, int pesoMaximo) {
    printf("=== MST com Restrição de Peso Máximo ===\n");
    printf("Peso máximo permitido por aresta: %d\n\n", pesoMaximo);
    
    ArestaLista arestas[MAX_ARESTAS];
    int numArestas;
    
    obterTodasArestas(grafo, arestas, &numArestas);
    qsort(arestas, numArestas, sizeof(ArestaLista), compararArestas);
    
    UnionFind uf;
    inicializarUnionFind(&uf, grafo->numVertices);
    
    ArestaLista mst[MAX_VERTICES - 1];
    int numArestasMST = 0;
    int pesoTotal = 0;
    
    for (int i = 0; i < numArestas && numArestasMST < grafo->numVertices - 1; i++) {
        if (arestas[i].peso <= pesoMaximo) {
            int u = arestas[i].origem;
            int v = arestas[i].destino;
            
            if (encontrar(&uf, u) != encontrar(&uf, v)) {
                mst[numArestasMST] = arestas[i];
                numArestasMST++;
                pesoTotal += arestas[i].peso;
                unir(&uf, u, v);
                
                printf("Adicionada: (%d, %d) - peso: %d\n", 
                       u, v, arestas[i].peso);
            }
        }
    }
    
    if (numArestasMST == grafo->numVertices - 1) {
        printf("\nMST com restrições encontrada!\n");
        printf("Peso total: %d\n\n", pesoTotal);
    } else {
        printf("\nNão é possível formar MST com a restrição dada.\n");
        printf("Arestas encontradas: %d (necessárias: %d)\n\n", 
               numArestasMST, grafo->numVertices - 1);
    }
}
 
void analisarCentralidade(GrafoPonderado* grafo) {
    printf("=== Análise de Centralidade na MST ===\n");
    
    // Executar Kruskal para obter MST
    ArestaLista arestas[MAX_ARESTAS];
    int numArestas;
    
    obterTodasArestas(grafo, arestas, &numArestas);
    qsort(arestas, numArestas, sizeof(ArestaLista), compararArestas);
    
    UnionFind uf;
    inicializarUnionFind(&uf, grafo->numVertices);
    
    int grauMST[MAX_VERTICES] = {0}; // Grau de cada vértice na MST
    
    for (int i = 0; i < numArestas && numArestas < grafo->numVertices - 1; i++) {
        int u = arestas[i].origem;
        int v = arestas[i].destino;
        
        if (encontrar(&uf, u) != encontrar(&uf, v)) {
            grauMST[u]++;
            grauMST[v]++;
            unir(&uf, u, v);
        }
    }
    
    printf("Grau dos vértices na MST:\n");
    for (int i = 0; i < grafo->numVertices; i++) {
        printf("Vértice %d: grau %d", i, grauMST[i]);
        if (grauMST[i] == 1) {
            printf(" (folha)");
        } else if (grauMST[i] > 2) {
            printf(" (hub)");
        }
        printf("\n");
    }
    printf("\n");
}
 
// Função para liberar memória do grafo
void liberarGrafo(GrafoPonderado* grafo) {
    for (int i = 0; i < grafo->numVertices; i++) {
        Aresta* atual = grafo->vertices[i].adjacentes;
        while (atual != NULL) {
            Aresta* temp = atual;
            atual = atual->proxima;
            free(temp);
        }
    }
    free(grafo);
}
 
// ==================== FUNÇÃO PRINCIPAL ====================
 
int main() {
    printf("=== DEMONSTRAÇÃO DE ÁRVORES DE ABRANGÊNCIA MÍNIMA ===\n\n");
    
    // Exemplo 1: Grafo básico
    printf("EXEMPLO 1: Grafo básico\n");
    printf("=======================\n");
    
    GrafoPonderado* grafo1 = criarGrafo(6, false);
    
    // Adicionar arestas (grafo conectado)
    adicionarAresta(grafo1, 0, 1, 4);
    adicionarAresta(grafo1, 0, 2, 2);
    adicionarAresta(grafo1, 1, 2, 1);
    adicionarAresta(grafo1, 1, 3, 5);
    adicionarAresta(grafo1, 2, 3, 8);
    adicionarAresta(grafo1, 2, 4, 10);
    adicionarAresta(grafo1, 3, 4, 2);
    adicionarAresta(grafo1, 3, 5, 6);
    adicionarAresta(grafo1, 4, 5, 3);
    
    printf("Arestas do grafo:\n");
    printf("(0,1):4, (0,2):2, (1,2):1, (1,3):5, (2,3):8\n");
    printf("(2,4):10, (3,4):2, (3,5):6, (4,5):3\n\n");
    
    compararAlgoritmosMST(grafo1);
    
    // Exemplo 2: Análises especiais
    printf("EXEMPLO 2: Análises Especiais\n");
    printf("==============================\n");
    
    encontrarMSTComRestricoes(grafo1, 5);
    analisarCentralidade(grafo1);
    
    // Exemplo 3: Grafo com pesos iguais
    printf("EXEMPLO 3: Grafo com múltiplas MSTs possíveis\n");
    printf("==============================================\n");
    
    GrafoPonderado* grafo2 = criarGrafo(4, false);
    adicionarAresta(grafo2, 0, 1, 1);
    adicionarAresta(grafo2, 0, 2, 1);
    adicionarAresta(grafo2, 1, 2, 1);
    adicionarAresta(grafo2, 1, 3, 1);
    adicionarAresta(grafo2, 2, 3, 1);
    
    printf("Todas as arestas têm peso 1 - múltiplas MSTs possíveis\n\n");
    compararAlgoritmosMST(grafo2);
    
    // Exemplo 4: Demonstração de eficiência
    printf("EXEMPLO 4: Análise de Complexidade\n");
    printf("===================================\n");
    printf("Complexidades dos algoritmos:\n");
    printf("• Prim com heap binário: O(E log V)\n");
    printf("• Prim com heap de Fibonacci: O(E + V log V)\n");
    printf("• Kruskal: O(E log E) = O(E log V)\n\n");
    
    printf("Escolha do algoritmo:\n");
    printf("• Grafo denso (E ≈ V²): Prefira Prim\n");
    printf("• Grafo esparso (E ≈ V): Ambos são eficientes\n");
    printf("• Arestas já ordenadas: Kruskal pode ser mais rápido\n\n");
    
    liberarGrafo(grafo1);
    liberarGrafo(grafo2);
    
    return 0;
}

Análise dos Algoritmos

1. Algoritmo de Prim

Características:

  • Complexidade: O(E log V) com heap binário, O(E + V log V) com heap de Fibonacci
  • Estratégia: Algoritmo ganancioso que cresce a MST um vértice por vez
  • Vantagem: Eficiente para grafos densos
  • Implementação: Utiliza heap mínimo para selecionar próxima aresta

Funcionamento:

  1. Inicia com um vértice qualquer
  2. Sempre adiciona a aresta de menor peso que conecta a MST atual a um novo vértice
  3. Mantém um heap com os vértices não incluídos e suas chaves (menor peso de conexão)

2. Algoritmo de Kruskal

Características:

  • Complexidade: O(E log E) = O(E log V)
  • Estratégia: Considera todas as arestas em ordem crescente de peso
  • Vantagem: Eficiente para grafos esparsos
  • Implementação: Utiliza Union-Find para detectar ciclos

Funcionamento:

  1. Ordena todas as arestas por peso crescente
  2. Para cada aresta em ordem, adiciona à MST se não criar ciclo
  3. Usa Union-Find para verificar eficientemente se há ciclo

3. Comparação dos Algoritmos

AspectoPrimKruskal
ComplexidadeO(E log V)O(E log V)
Grafos densos✅ Melhor❌ Pior
Grafos esparsos✅ Bom✅ Melhor
ImplementaçãoMais complexaMais simples
Estruturas auxiliaresHeap mínimoUnion-Find
ParalelizaçãoDifícilPossível

Propriedades das MSTs

Teorema do Corte (Cut Theorem)

Para qualquer corte em um grafo, a aresta de menor peso que cruza o corte está em alguma MST.

Teorema do Ciclo (Cycle Theorem)

Para qualquer ciclo em um grafo, a aresta de maior peso no ciclo não está em nenhuma MST (exceto se houver empate).

Unicidade

Uma MST é única se e somente se todos os pesos das arestas forem distintos.

Variações e Extensões

1. MST com Restrições

  • Limitação de grau máximo por vértice
  • Restrição de peso máximo por aresta
  • Inclusão obrigatória de certas arestas

2. MST Dinâmica

  • Atualização eficiente quando arestas são adicionadas/removidas
  • Manutenção da MST em grafos que mudam com o tempo

3. k-MST (k Árvores de Abrangência Mínima)

  • Encontrar as k menores árvores de abrangência distintas
  • Aplicações em sistemas com redundância

Exemplos de contexto de uso:

Aplicações práticas das MSTs:

  1. Redes de Telecomunicações:

    • Conectar cidades com cabo de fibra ótica minimizando custo total
    • Projeto de redes de internet com menor latência total
  2. Redes Elétricas:

    • Distribuição de energia elétrica com menor custo de cabeamento
    • Planejamento de redes de transmissão
  3. Transporte e Logística:

    • Construção de estradas conectando todas as cidades com menor custo
    • Planejamento de rotas de entrega eficientes
  4. Análise de Clusters:

    • Agrupamento hierárquico em machine learning
    • Segmentação de imagens baseada em similaridade
  5. Bioinformática:

    • Construção de árvores filogenéticas
    • Análise de redes de interação proteica
  6. Jogos e Simulações:

    • Geração de mapas e terrenos conectados
    • Pathfinding em ambientes complexos

As MSTs são fundamentais em qualquer problema onde precisamos conectar todos os elementos de um conjunto com o menor custo total possível.

Esta página foi útil?

Seu feedback ajuda a melhorar a documentação

Encontrou um erro ou quer contribuir?

Ajude a melhorar esta documentação editando-a no GitHub.

Editar no GitHub