Grafo com Matriz de Adjacência em Rust

Aprenda grafos com matriz de adjacência em Rust: representação 2D, grafos ponderados, grafos densos, verificação de aresta em O(1) e exemplo prático de conexões aéreas.

A matriz de adjacência é uma representação de grafos usando um array bidimensional onde matriz[i][j] indica a existência (e possivelmente o peso) de uma aresta entre os vértices i e j. Enquanto a lista de adjacência é preferida para grafos esparsos, a matriz de adjacência brilha em grafos densos, onde a maioria dos pares de vértices está conectada, e quando precisamos de verificação de aresta em O(1).


Conceito e Teoria

Representação como Matriz

Um grafo com n vértices é representado por uma matriz n x n:

  Grafo Não-Direcionado:          Matriz de Adjacência:

      0 ─── 1                       0  1  2  3
      │   / │                    0 [ 0  1  1  0 ]
      │  /  │                    1 [ 1  0  1  1 ]
      │ /   │                    2 [ 1  1  0  1 ]
      2 ─── 3                    3 [ 0  1  1  0 ]

  Simétrica: matriz[i][j] == matriz[j][i]

Grafo Direcionado

      0 ──→ 1                     0  1  2  3
      │       ↘                0 [ 0  1  1  0 ]
      ↓        3               1 [ 0  0  0  1 ]
      2 ──→ ↗                  2 [ 0  0  0  1 ]
                               3 [ 0  0  0  0 ]

  NÃO simétrica: matriz[0][1]=1 mas matriz[1][0]=0

Grafo Ponderado

  Grafo com pesos:                Matriz de Pesos:

      0 ──5── 1                       0    1    2    3
      │      / │                  0 [  0    5    3   ∞  ]
      3    2   7                  1 [  5    0    2    7  ]
      │  /     │                  2 [  3    2    0    4  ]
      2 ──4── 3                   3 [  ∞    7    4    0  ]

  ∞ = sem aresta (representado como i32::MAX ou Option<i64>)
  Diagonal = 0 (distância de um vértice a si mesmo)

Quando Usar Matriz vs Lista

  ┌──────────────────┬──────────────────────┬─────────────────────┐
  │    Critério       │  Matriz de Adjacência │  Lista de Adjacência │
  ├──────────────────┼──────────────────────┼─────────────────────┤
  │ Verificar aresta  │  O(1) ✓✓             │  O(grau) ✗          │
  │ Listar vizinhos   │  O(V) ✗              │  O(grau) ✓✓         │
  │ Adicionar aresta  │  O(1) ✓✓             │  O(1) ✓✓            │
  │ Remover aresta    │  O(1) ✓✓             │  O(grau) ✗          │
  │ Espaço            │  O(V²) ✗             │  O(V + E) ✓✓        │
  │ Grafos densos     │  ✓✓ Ideal            │  ✗ Desperdício      │
  │ Grafos esparsos   │  ✗ Desperdício       │  ✓✓ Ideal           │
  │ Floyd-Warshall    │  ✓✓ Necessário       │  Precisa converter  │
  │ Cache locality    │  ✓✓ Array contíguo   │  ✗ Ponteiros        │
  └──────────────────┴──────────────────────┴─────────────────────┘

  Regra prática:
  - Denso (E ≈ V²): use MATRIZ
  - Esparso (E << V²): use LISTA

Visualização do Uso de Memória

  Grafo com 5 vértices e 4 arestas (esparso):

  Matriz: 5 x 5 = 25 células armazenadas
  ┌───┬───┬───┬───┬───┐
  │ 0 │ 1 │ 0 │ 0 │ 0 │   Maioria é 0 (desperdício!)
  │ 1 │ 0 │ 1 │ 0 │ 0 │
  │ 0 │ 1 │ 0 │ 1 │ 0 │
  │ 0 │ 0 │ 1 │ 0 │ 1 │
  │ 0 │ 0 │ 0 │ 1 │ 0 │
  └───┴───┴───┴───┴───┘

  Lista: apenas 8 entradas (4 arestas x 2 direções)
  0: [1]
  1: [0, 2]
  2: [1, 3]
  3: [2, 4]
  4: [3]

Complexidade

OperaçãoComplexidadeObservação
Verificar aresta (i,j)O(1)Acesso direto: matriz[i][j]
Adicionar arestaO(1)Atribuição direta
Remover arestaO(1)Zera a posição
Listar vizinhos de vO(V)Percorre toda a linha
BFS / DFSO(V²)Precisa varrer linhas inteiras
EspaçoO(V²)Sempre V² independente de E
Floyd-WarshallO(V³)Representação natural para o algo

Implementação em Rust

Grafo com Matriz de Adjacência

/// Grafo usando matriz de adjacência.
/// Suporta grafos direcionados/não-direcionados e ponderados.
struct GrafoMatriz {
    /// Matriz de adjacência: None = sem aresta, Some(peso) = aresta com peso
    matriz: Vec<Vec<Option<i64>>>,
    /// Número de vértices
    num_vertices: usize,
    /// Se o grafo é direcionado
    direcionado: bool,
    /// Número de arestas
    num_arestas: usize,
}

impl GrafoMatriz {
    /// Cria um grafo vazio com n vértices
    fn novo(num_vertices: usize, direcionado: bool) -> Self {
        GrafoMatriz {
            matriz: vec![vec![None; num_vertices]; num_vertices],
            num_vertices,
            direcionado,
            num_arestas: 0,
        }
    }

    /// Adiciona uma aresta com peso entre os vértices u e v
    fn adicionar_aresta(&mut self, u: usize, v: usize, peso: i64) {
        assert!(u < self.num_vertices && v < self.num_vertices);

        self.matriz[u][v] = Some(peso);
        if !self.direcionado {
            self.matriz[v][u] = Some(peso);
        }
        self.num_arestas += 1;
    }

    /// Adiciona aresta sem peso (peso = 1)
    fn adicionar_aresta_simples(&mut self, u: usize, v: usize) {
        self.adicionar_aresta(u, v, 1);
    }

    /// Remove a aresta entre u e v
    fn remover_aresta(&mut self, u: usize, v: usize) {
        if self.matriz[u][v].is_some() {
            self.matriz[u][v] = None;
            if !self.direcionado {
                self.matriz[v][u] = None;
            }
            self.num_arestas -= 1;
        }
    }

    /// Verifica se existe aresta entre u e v — O(1)
    fn tem_aresta(&self, u: usize, v: usize) -> bool {
        self.matriz[u][v].is_some()
    }

    /// Retorna o peso da aresta entre u e v
    fn peso_aresta(&self, u: usize, v: usize) -> Option<i64> {
        self.matriz[u][v]
    }

    /// Retorna os vizinhos do vértice v
    fn vizinhos(&self, v: usize) -> Vec<usize> {
        (0..self.num_vertices)
            .filter(|&u| self.matriz[v][u].is_some())
            .collect()
    }

    /// Retorna vizinhos com pesos
    fn vizinhos_com_peso(&self, v: usize) -> Vec<(usize, i64)> {
        (0..self.num_vertices)
            .filter_map(|u| self.matriz[v][u].map(|peso| (u, peso)))
            .collect()
    }

    /// Grau do vértice (número de arestas incidentes)
    fn grau(&self, v: usize) -> usize {
        self.matriz[v].iter().filter(|x| x.is_some()).count()
    }

    /// Grau de entrada (para grafos direcionados)
    fn grau_entrada(&self, v: usize) -> usize {
        (0..self.num_vertices)
            .filter(|&u| self.matriz[u][v].is_some())
            .count()
    }

    /// Grau de saída (para grafos direcionados)
    fn grau_saida(&self, v: usize) -> usize {
        self.grau(v) // É o mesmo que percorrer a linha
    }

    /// Exibe a matriz
    fn exibir(&self) {
        let tipo = if self.direcionado { "Direcionado" } else { "Não-direcionado" };
        println!("Grafo {} ({} vértices, {} arestas):", tipo, self.num_vertices, self.num_arestas);

        // Cabeçalho
        print!("     ");
        for j in 0..self.num_vertices {
            print!("{:>5}", j);
        }
        println!();

        // Linhas da matriz
        for i in 0..self.num_vertices {
            print!("  {} [", i);
            for j in 0..self.num_vertices {
                match self.matriz[i][j] {
                    Some(peso) => print!("{:>5}", peso),
                    None => print!("    ."),
                }
            }
            println!("  ]");
        }
    }
}

Floyd-Warshall: Caminhos Mínimos entre Todos os Pares

/// Floyd-Warshall: encontra o menor caminho entre TODOS os pares de vértices.
/// Complexidade: O(V³)
fn floyd_warshall(grafo: &GrafoMatriz) -> Vec<Vec<i64>> {
    let n = grafo.num_vertices;
    let inf = i64::MAX / 2; // Evita overflow na soma

    // Inicializa a matriz de distâncias
    let mut dist = vec![vec![inf; n]; n];

    // Distância de cada vértice a si mesmo é 0
    for i in 0..n {
        dist[i][i] = 0;
    }

    // Copia as arestas do grafo
    for i in 0..n {
        for j in 0..n {
            if let Some(peso) = grafo.peso_aresta(i, j) {
                dist[i][j] = peso;
            }
        }
    }

    // Algoritmo principal: para cada vértice intermediário k
    for k in 0..n {
        for i in 0..n {
            for j in 0..n {
                // Se passar por k é mais curto, atualiza
                if dist[i][k] + dist[k][j] < dist[i][j] {
                    dist[i][j] = dist[i][k] + dist[k][j];
                }
            }
        }
    }

    dist
}

/// Exibe a matriz de distâncias de forma formatada
fn exibir_distancias(dist: &[Vec<i64>], nomes: &[&str]) {
    let n = nomes.len();
    let inf = i64::MAX / 2;

    print!("{:>12}", "");
    for nome in nomes {
        print!("{:>10}", nome);
    }
    println!();

    for i in 0..n {
        print!("{:>12}", nomes[i]);
        for j in 0..n {
            if dist[i][j] >= inf {
                print!("{:>10}", "∞");
            } else {
                print!("{:>10}", dist[i][j]);
            }
        }
        println!();
    }
}

Verificação de Propriedades do Grafo

impl GrafoMatriz {
    /// Verifica se o grafo é completo (todas as arestas possíveis existem)
    fn eh_completo(&self) -> bool {
        for i in 0..self.num_vertices {
            for j in 0..self.num_vertices {
                if i != j && self.matriz[i][j].is_none() {
                    return false;
                }
            }
        }
        true
    }

    /// Calcula a densidade do grafo (proporção de arestas existentes)
    fn densidade(&self) -> f64 {
        let max_arestas = if self.direcionado {
            self.num_vertices * (self.num_vertices - 1)
        } else {
            self.num_vertices * (self.num_vertices - 1) / 2
        };

        if max_arestas == 0 {
            return 0.0;
        }

        self.num_arestas as f64 / max_arestas as f64
    }

    /// Transposição do grafo (inverte direção das arestas)
    fn transpor(&self) -> GrafoMatriz {
        let mut transposto = GrafoMatriz::novo(self.num_vertices, self.direcionado);
        for i in 0..self.num_vertices {
            for j in 0..self.num_vertices {
                if let Some(peso) = self.matriz[i][j] {
                    transposto.matriz[j][i] = Some(peso);
                }
            }
        }
        transposto.num_arestas = self.num_arestas;
        transposto
    }
}

Exemplo Prático: Conexões Aéreas entre Cidades

use std::collections::VecDeque;

/// Sistema de conexões aéreas entre cidades
struct MalhaAerea {
    grafo: GrafoMatriz,
    cidades: Vec<String>,
}

impl MalhaAerea {
    fn nova(cidades: Vec<&str>) -> Self {
        let n = cidades.len();
        MalhaAerea {
            grafo: GrafoMatriz::novo(n, false),
            cidades: cidades.into_iter().map(String::from).collect(),
        }
    }

    /// Encontra o índice de uma cidade pelo nome
    fn indice_cidade(&self, nome: &str) -> Option<usize> {
        self.cidades.iter().position(|c| c == nome)
    }

    /// Adiciona um voo entre duas cidades com o custo em reais
    fn adicionar_voo(&mut self, de: &str, para: &str, custo: i64) {
        if let (Some(i), Some(j)) = (self.indice_cidade(de), self.indice_cidade(para)) {
            self.grafo.adicionar_aresta(i, j, custo);
        }
    }

    /// Encontra o voo mais barato entre duas cidades (Floyd-Warshall)
    fn menor_custo(&self, de: &str, para: &str) -> Option<i64> {
        let dist = floyd_warshall(&self.grafo);
        let inf = i64::MAX / 2;

        if let (Some(i), Some(j)) = (self.indice_cidade(de), self.indice_cidade(para)) {
            if dist[i][j] < inf {
                Some(dist[i][j])
            } else {
                None
            }
        } else {
            None
        }
    }

    /// Lista todos os destinos diretos de uma cidade
    fn destinos_diretos(&self, cidade: &str) -> Vec<(String, i64)> {
        match self.indice_cidade(cidade) {
            Some(idx) => {
                self.grafo
                    .vizinhos_com_peso(idx)
                    .into_iter()
                    .map(|(v, peso)| (self.cidades[v].clone(), peso))
                    .collect()
            }
            None => vec![],
        }
    }

    /// Encontra a cidade mais conectada (hub principal)
    fn hub_principal(&self) -> (&str, usize) {
        let mut melhor_idx = 0;
        let mut melhor_grau = 0;

        for i in 0..self.cidades.len() {
            let grau = self.grafo.grau(i);
            if grau > melhor_grau {
                melhor_grau = grau;
                melhor_idx = i;
            }
        }

        (&self.cidades[melhor_idx], melhor_grau)
    }

    /// Encontra menor número de conexões (escalas) entre duas cidades
    fn menor_escalas(&self, de: &str, para: &str) -> Option<usize> {
        let (idx_de, idx_para) = match (self.indice_cidade(de), self.indice_cidade(para)) {
            (Some(a), Some(b)) => (a, b),
            _ => return None,
        };

        // BFS para encontrar menor número de arestas
        let n = self.cidades.len();
        let mut dist = vec![usize::MAX; n];
        let mut fila = VecDeque::new();

        dist[idx_de] = 0;
        fila.push_back(idx_de);

        while let Some(atual) = fila.pop_front() {
            if atual == idx_para {
                return Some(dist[atual]);
            }
            for vizinho in self.grafo.vizinhos(atual) {
                if dist[vizinho] == usize::MAX {
                    dist[vizinho] = dist[atual] + 1;
                    fila.push_back(vizinho);
                }
            }
        }

        None
    }

    /// Exibe a tabela completa de distâncias
    fn tabela_custos(&self) {
        let dist = floyd_warshall(&self.grafo);
        let nomes: Vec<&str> = self.cidades.iter().map(|s| s.as_str()).collect();
        println!("\n=== Tabela de Menores Custos (R$) ===");
        exibir_distancias(&dist, &nomes);
    }

    /// Exibe informações da malha aérea
    fn exibir_info(&self) {
        println!("╔═══════════════════════════════════════════╗");
        println!("║         Malha Aérea Brasileira            ║");
        println!("╠═══════════════════════════════════════════╣");
        println!("║ Cidades: {}                               ", self.cidades.len());
        println!("║ Rotas:   {}                               ", self.grafo.num_arestas);
        println!("║ Densidade: {:.1}%                         ", self.grafo.densidade() * 100.0);

        let (hub, grau) = self.hub_principal();
        println!("║ Hub principal: {} ({} conexões)           ", hub, grau);
        println!("╚═══════════════════════════════════════════╝");
    }
}

fn main() {
    let cidades = vec![
        "São Paulo", "Rio de Janeiro", "Brasília",
        "Salvador", "Manaus", "Porto Alegre",
    ];

    let mut malha = MalhaAerea::nova(cidades);

    // Adicionando voos com custos em reais
    malha.adicionar_voo("São Paulo", "Rio de Janeiro", 350);
    malha.adicionar_voo("São Paulo", "Brasília", 500);
    malha.adicionar_voo("São Paulo", "Porto Alegre", 450);
    malha.adicionar_voo("São Paulo", "Salvador", 600);
    malha.adicionar_voo("Rio de Janeiro", "Salvador", 550);
    malha.adicionar_voo("Brasília", "Manaus", 800);
    malha.adicionar_voo("Brasília", "Salvador", 400);
    malha.adicionar_voo("Salvador", "Manaus", 900);

    malha.exibir_info();

    // Destinos diretos
    println!("\nDestinos diretos de São Paulo:");
    for (cidade, custo) in malha.destinos_diretos("São Paulo") {
        println!("  -> {} (R$ {})", cidade, custo);
    }

    // Menor custo
    println!("\nMenor custo Porto Alegre -> Manaus:");
    match malha.menor_custo("Porto Alegre", "Manaus") {
        Some(custo) => println!("  R$ {} (possivelmente com conexões)", custo),
        None => println!("  Sem rota disponível"),
    }

    // Escalas
    println!("\nMenor número de escalas Porto Alegre -> Manaus:");
    match malha.menor_escalas("Porto Alegre", "Manaus") {
        Some(n) => println!("  {} voo(s) / {} escala(s)", n, n.saturating_sub(1)),
        None => println!("  Sem rota disponível"),
    }

    // Tabela completa
    malha.tabela_custos();

    // Exibe a matriz
    println!("\n=== Matriz de Adjacência ===");
    malha.grafo.exibir();
}

Exercícios

Exercício 1: Detecção de Ciclo de Peso Negativo

Modifique o algoritmo Floyd-Warshall para detectar ciclos de peso negativo. Um ciclo de peso negativo existe se, após a execução, algum dist[i][i] < 0.

fn tem_ciclo_negativo(grafo: &GrafoMatriz) -> bool {
    // Dica: execute Floyd-Warshall e verifique a diagonal
    todo!()
}

Exercício 2: Caminho com Reconstrução

Estenda Floyd-Warshall para reconstruir o caminho mais curto entre dois vértices. Use uma matriz de predecessores next[i][j] que indica o próximo vértice no caminho de i a j.

fn floyd_warshall_com_caminho(grafo: &GrafoMatriz) -> (Vec<Vec<i64>>, Vec<Vec<Option<usize>>>) {
    // Retorna (dist, next) onde next[i][j] é o próximo vértice no caminho i->j
    todo!()
}

fn reconstruir_caminho(next: &[Vec<Option<usize>>], de: usize, para: usize) -> Vec<usize> {
    todo!()
}

Exercício 3: Componentes Fortemente Conectados

Para um grafo direcionado, implemente a verificação de se o grafo é fortemente conectado (existe caminho entre todo par de vértices). Use Floyd-Warshall para verificar se todas as distâncias são finitas.

fn eh_fortemente_conectado(grafo: &GrafoMatriz) -> bool {
    // Dica: após Floyd-Warshall, verifique se dist[i][j] < ∞ para todo i, j
    todo!()
}

Conclusão

A matriz de adjacência é a escolha ideal quando:

  • O grafo é denso (muitas arestas relativas ao número de vértices)
  • Você precisa de verificação de aresta em O(1) constante
  • Algoritmos como Floyd-Warshall que naturalmente operam sobre matrizes
  • O número de vértices é pequeno a médio (V < ~5000 para caber na memória)

A principal desvantagem e o custo de O(V^2) de espaço, que torna a representação impraticável para grafos muito grandes e esparsos. Nestes casos, prefira a lista de adjacência.

Na próxima página, veremos o Union-Find (Disjoint Set), uma estrutura especializada para rastrear conectividade em grafos.


Conteúdo produzido pela Equipe Rust Brasil para rustlang.com.br