Fenwick Tree (BIT) em Rust: Árvore Indexada Binária

Aprenda Fenwick Tree (Binary Indexed Tree) em Rust: prefix sum, point update, o truque binário lowbit, comparação com Segment Tree e exemplos práticos com código completo.

A Fenwick Tree, também conhecida como Binary Indexed Tree (BIT), é uma estrutura de dados elegante que suporta consultas de soma de prefixo e atualizações pontuais em O(log n). Inventada por Peter Fenwick em 1994, ela se destaca pela simplicidade de implementação – usando apenas operações de bits – e pelo baixo uso de memória. Para muitos problemas, ela é a alternativa preferida à Segment Tree por ser mais simples e ligeiramente mais rápida.


Conceito e Teoria

O Problema de Prefix Sum

Dado um array, queremos calcular somas de prefixos eficientemente e permitir atualizações:

  Array:       [3, 2, 5, 1, 4, 7, 2, 6]
  Índice:       1  2  3  4  5  6  7  8   (1-indexado)

  Prefix Sum:
  sum(1) = 3
  sum(3) = 3 + 2 + 5 = 10
  sum(5) = 3 + 2 + 5 + 1 + 4 = 15
  sum(8) = 3 + 2 + 5 + 1 + 4 + 7 + 2 + 6 = 30

  Soma do intervalo [l, r] = sum(r) - sum(l-1)
  soma(3, 6) = sum(6) - sum(2) = 22 - 5 = 17

O Truque Binário: lowbit

O segredo da Fenwick Tree está na operação lowbit – o bit menos significativo de um número:

  lowbit(x) = x & (-x)    // Em complemento de 2

  Exemplos:
  ┌──────┬──────────┬──────────┬─────────┐
  │  x   │ Binário  │   -x     │ lowbit  │
  ├──────┼──────────┼──────────┼─────────┤
  │  1   │ 0001     │ 1111     │   1     │
  │  2   │ 0010     │ 1110     │   2     │
  │  3   │ 0011     │ 1101     │   1     │
  │  4   │ 0100     │ 1100     │   4     │
  │  5   │ 0101     │ 1011     │   1     │
  │  6   │ 0110     │ 1010     │   2     │
  │  7   │ 0111     │ 1001     │   1     │
  │  8   │ 1000     │ 1000     │   8     │
  └──────┴──────────┴──────────┴─────────┘

Estrutura da Fenwick Tree

Cada posição i da Fenwick Tree armazena a soma de lowbit(i) elementos terminando em i:

  Array original: [_, 3, 2, 5, 1, 4, 7, 2, 6]
  Índice:          0  1  2  3  4  5  6  7  8

  BIT[i] armazena soma de lowbit(i) elementos:

  ┌───────┬──────────┬──────────────────────────────────┐
  │ Índice│ lowbit(i)│ BIT[i] = soma de quais elementos │
  ├───────┼──────────┼──────────────────────────────────┤
  │   1   │    1     │ a[1]         = 3                 │
  │   2   │    2     │ a[1..=2]     = 5                 │
  │   3   │    1     │ a[3]         = 5                 │
  │   4   │    4     │ a[1..=4]     = 11                │
  │   5   │    1     │ a[5]         = 4                 │
  │   6   │    2     │ a[5..=6]     = 11                │
  │   7   │    1     │ a[7]         = 2                 │
  │   8   │    8     │ a[1..=8]     = 30                │
  └───────┴──────────┴──────────────────────────────────┘

Visualização da Responsabilidade de Cada Nó

  Índice:  1    2    3    4    5    6    7    8
  Array:  [3]  [2]  [5]  [1]  [4]  [7]  [2]  [6]

  BIT[1] = [3]
           └──┐
  BIT[2] = [3, 2]
              │
  BIT[3] =      [5]
              └──┐
  BIT[4] = [3, 2, 5, 1]
                       │
  BIT[5] =              [4]
                       └──┐
  BIT[6] =              [4, 7]
                             │
  BIT[7] =                    [2]
                             └──┐
  BIT[8] = [3, 2, 5, 1, 4, 7, 2, 6]

  Árvore de responsabilidades:
            8
        ┌───┴───┐
        4       ├───┐
      ┌─┴─┐    6    │
      2   │   ┌┴┐   │
     ┌┴┐  3  5  │   7
     1          │

Query: Soma de Prefixo

Para calcular sum(i), subtraímos repetidamente lowbit(i) de i:

  sum(7):
    i = 7 (0111)  -> BIT[7] = a[7]     = 2
    i = 6 (0110)  -> BIT[6] = a[5..=6] = 11
    i = 4 (0100)  -> BIT[4] = a[1..=4] = 11
    i = 0          -> PARE
    Total: 2 + 11 + 11 = 24 ✓ (3+2+5+1+4+7+2 = 24)

  Caminho: 7 -> 6 -> 4 -> 0 (apenas 3 passos = O(log n))

Update: Atualização Pontual

Para atualizar a[i] += delta, somamos lowbit(i) a i repetidamente:

  update(3, +10):   (a[3] passa de 5 para 15)
    i = 3 (0011)  -> BIT[3] += 10
    i = 4 (0100)  -> BIT[4] += 10
    i = 8 (1000)  -> BIT[8] += 10
    i = 16         -> PARE (> n)

  Caminho: 3 -> 4 -> 8 (apenas 3 passos = O(log n))

Complexidade

OperaçãoComplexidadeObservação
ConstruirO(n)Com técnica otimizada
Construir (ingênuo)O(n log n)n atualizações pontuais
Consulta prefixo sum(i)O(log n)Percorre bits de i
Consulta intervaloO(log n)sum(r) - sum(l-1)
Atualização pontualO(log n)Percorre bits de i
EspaçoO(n)Apenas um array extra

Comparação: Fenwick Tree vs Segment Tree

  ┌──────────────────────┬─────────────┬────────────────┐
  │    Critério           │ Fenwick Tree│ Segment Tree   │
  ├──────────────────────┼─────────────┼────────────────┤
  │ Linhas de código      │    ~20      │    ~60-100     │
  │ Constante oculta      │   Menor     │    Maior       │
  │ Memória               │   n+1       │    4n          │
  │ Prefix sum + update   │     ✓✓      │      ✓         │
  │ Range update + query  │     ✓*      │      ✓✓        │
  │ Range min/max query   │     ✗       │      ✓✓        │
  │ Lazy propagation      │     ✗       │      ✓✓        │
  │ Operações não-inversas│     ✗       │      ✓✓        │
  └──────────────────────┴─────────────┴────────────────┘
  * Com técnica de 2 BITs

Implementação em Rust

Fenwick Tree Completa

/// Fenwick Tree (Binary Indexed Tree).
/// Suporta prefix sum query e point update em O(log n).
struct FenwickTree {
    /// Array interno (1-indexado, posição 0 não é usada)
    bit: Vec<i64>,
    /// Tamanho do array original
    n: usize,
}

impl FenwickTree {
    /// Cria uma Fenwick Tree para um array de tamanho n (inicializado com zeros).
    fn novo(n: usize) -> Self {
        FenwickTree {
            bit: vec![0; n + 1],
            n,
        }
    }

    /// Constrói a Fenwick Tree a partir de um array existente em O(n).
    fn construir(dados: &[i64]) -> Self {
        let n = dados.len();
        let mut bit = vec![0i64; n + 1];

        // Copia os dados (1-indexado)
        for i in 0..n {
            bit[i + 1] = dados[i];
        }

        // Propagação eficiente em O(n)
        for i in 1..=n {
            let pai = i + lowbit(i);
            if pai <= n {
                bit[pai] += bit[i];
            }
        }

        FenwickTree { bit, n }
    }

    /// Atualiza: a[pos] += delta (pos é 1-indexado)
    fn atualizar(&mut self, mut pos: usize, delta: i64) {
        while pos <= self.n {
            self.bit[pos] += delta;
            pos += lowbit(pos);
        }
    }

    /// Consulta a soma do prefixo: a[1] + a[2] + ... + a[pos]
    fn prefix_sum(&self, mut pos: usize) -> i64 {
        let mut soma: i64 = 0;
        while pos > 0 {
            soma += self.bit[pos];
            pos -= lowbit(pos);
        }
        soma
    }

    /// Consulta a soma do intervalo [l, r] (1-indexado, inclusivo)
    fn soma_intervalo(&self, l: usize, r: usize) -> i64 {
        if l > r {
            return 0;
        }
        self.prefix_sum(r) - self.prefix_sum(l - 1)
    }

    /// Define o valor na posição pos como novo_valor
    fn definir(&mut self, pos: usize, novo_valor: i64) {
        let valor_atual = self.soma_intervalo(pos, pos); // Valor atual
        let delta = novo_valor - valor_atual;
        self.atualizar(pos, delta);
    }
}

/// Retorna o bit menos significativo: lowbit(x) = x & (-x)
fn lowbit(x: usize) -> usize {
    x & x.wrapping_neg()
}

Fenwick Tree 2D (para matrizes)

/// Fenwick Tree bidimensional para consultas de soma em sub-matrizes.
struct FenwickTree2D {
    bit: Vec<Vec<i64>>,
    linhas: usize,
    colunas: usize,
}

impl FenwickTree2D {
    fn novo(linhas: usize, colunas: usize) -> Self {
        FenwickTree2D {
            bit: vec![vec![0; colunas + 1]; linhas + 1],
            linhas,
            colunas,
        }
    }

    /// Atualiza: matriz[lin][col] += delta
    fn atualizar(&mut self, mut lin: usize, col: usize, delta: i64) {
        while lin <= self.linhas {
            let mut c = col;
            while c <= self.colunas {
                self.bit[lin][c] += delta;
                c += lowbit(c);
            }
            lin += lowbit(lin);
        }
    }

    /// Consulta a soma da sub-matriz [1,1] até [lin,col]
    fn prefix_sum(&self, mut lin: usize, col: usize) -> i64 {
        let mut soma: i64 = 0;
        while lin > 0 {
            let mut c = col;
            while c > 0 {
                soma += self.bit[lin][c];
                c -= lowbit(c);
            }
            lin -= lowbit(lin);
        }
        soma
    }

    /// Consulta a soma da sub-matriz [l1,c1] até [l2,c2]
    fn soma_submatriz(&self, l1: usize, c1: usize, l2: usize, c2: usize) -> i64 {
        self.prefix_sum(l2, c2)
            - self.prefix_sum(l1 - 1, c2)
            - self.prefix_sum(l2, c1 - 1)
            + self.prefix_sum(l1 - 1, c1 - 1)
    }
}

Fenwick Tree com Range Update e Point Query

/// Fenwick Tree modificada que suporta atualização em intervalo
/// e consulta pontual.
///
/// Truque: armazena as diferenças ao invés dos valores.
/// update(l, r, val): BIT.add(l, val); BIT.add(r+1, -val)
/// query(pos): prefix_sum(pos) = valor em pos
struct FenwickTreeRangeUpdate {
    bit: Vec<i64>,
    n: usize,
}

impl FenwickTreeRangeUpdate {
    fn novo(n: usize) -> Self {
        FenwickTreeRangeUpdate {
            bit: vec![0; n + 2], // +2 para evitar overflow no r+1
            n,
        }
    }

    fn adicionar(&mut self, mut pos: usize, delta: i64) {
        while pos <= self.n {
            self.bit[pos] += delta;
            pos += lowbit(pos);
        }
    }

    /// Soma delta a todos os elementos no intervalo [l, r]
    fn atualizar_intervalo(&mut self, l: usize, r: usize, delta: i64) {
        self.adicionar(l, delta);
        if r + 1 <= self.n {
            self.adicionar(r + 1, -delta);
        }
    }

    /// Consulta o valor na posição pos
    fn consultar(&self, mut pos: usize) -> i64 {
        let mut soma: i64 = 0;
        while pos > 0 {
            soma += self.bit[pos];
            pos -= lowbit(pos);
        }
        soma
    }
}

Exemplo Prático: Contagem de Inversões e Tabela de Frequências

/// Conta o número de inversões em um array.
/// Uma inversão é um par (i, j) onde i < j mas a[i] > a[j].
/// Complexidade: O(n log n) usando Fenwick Tree.
fn contar_inversoes(arr: &[i32]) -> i64 {
    if arr.is_empty() {
        return 0;
    }

    // Encontra o valor máximo para dimensionar a BIT
    let max_val = *arr.iter().max().unwrap() as usize;
    let mut bit = FenwickTree::novo(max_val + 1);
    let mut inversoes: i64 = 0;

    // Processa da direita para a esquerda
    for &val in arr.iter().rev() {
        let v = val as usize;
        // Conta quantos elementos menores que val já foram inseridos
        // (estão à direita de val no array original)
        if v > 0 {
            inversoes += bit.prefix_sum(v - 1);
        }
        // Registra a presença de val
        bit.atualizar(v, 1);
    }

    inversoes
}

/// Tabela de frequências com consultas de intervalo
struct TabelaFrequencias {
    bit: FenwickTree,
    max_valor: usize,
}

impl TabelaFrequencias {
    fn nova(max_valor: usize) -> Self {
        TabelaFrequencias {
            bit: FenwickTree::novo(max_valor),
            max_valor,
        }
    }

    /// Registra uma ocorrência do valor
    fn registrar(&mut self, valor: usize) {
        assert!(valor >= 1 && valor <= self.max_valor);
        self.bit.atualizar(valor, 1);
    }

    /// Quantos valores no intervalo [l, r] foram registrados?
    fn contar_intervalo(&self, l: usize, r: usize) -> i64 {
        self.bit.soma_intervalo(l, r)
    }

    /// Quantos valores são menores ou iguais a x?
    fn contar_ate(&self, x: usize) -> i64 {
        self.bit.prefix_sum(x)
    }

    /// Total de valores registrados
    fn total(&self) -> i64 {
        self.bit.prefix_sum(self.max_valor)
    }

    /// Encontra o k-ésimo menor valor (busca binária na BIT)
    fn k_esimo(&self, mut k: i64) -> Option<usize> {
        let mut pos = 0;
        let mut bit_mask = 1;

        // Encontra a maior potência de 2 <= max_valor
        while bit_mask <= self.max_valor {
            bit_mask <<= 1;
        }
        bit_mask >>= 1;

        // Busca binária na estrutura da BIT
        while bit_mask > 0 {
            let proximo = pos + bit_mask;
            if proximo <= self.max_valor && self.bit.bit[proximo] < k {
                k -= self.bit.bit[proximo];
                pos = proximo;
            }
            bit_mask >>= 1;
        }

        if pos + 1 <= self.max_valor {
            Some(pos + 1)
        } else {
            None
        }
    }
}

fn main() {
    // === Contagem de Inversões ===
    println!("=== Contagem de Inversões ===\n");

    let arrays = vec![
        vec![2, 4, 1, 3, 5],
        vec![5, 4, 3, 2, 1],
        vec![1, 2, 3, 4, 5],
    ];

    for arr in &arrays {
        let inv = contar_inversoes(arr);
        println!("  {:?} -> {} inversões", arr, inv);
    }

    // === Fenwick Tree Básica ===
    println!("\n=== Fenwick Tree: Vendas Mensais ===\n");

    // Vendas mensais (1 = janeiro, 12 = dezembro)
    let vendas = vec![120, 95, 150, 180, 200, 170, 140, 160, 190, 220, 250, 300];
    let mut bit = FenwickTree::construir(&vendas);

    println!("Vendas acumuladas:");
    println!("  Jan-Mar (Q1): {}", bit.soma_intervalo(1, 3));
    println!("  Abr-Jun (Q2): {}", bit.soma_intervalo(4, 6));
    println!("  Jul-Set (Q3): {}", bit.soma_intervalo(7, 9));
    println!("  Out-Dez (Q4): {}", bit.soma_intervalo(10, 12));
    println!("  Ano total:    {}", bit.prefix_sum(12));

    // Atualização: correção das vendas de março
    println!("\nCorrigindo vendas de março: 150 -> 175");
    bit.atualizar(3, 25); // delta = 175 - 150 = 25
    println!("  Novo Q1: {}", bit.soma_intervalo(1, 3));
    println!("  Novo total: {}", bit.prefix_sum(12));

    // === Tabela de Frequências ===
    println!("\n=== Tabela de Frequências: Notas de Alunos ===\n");

    let mut freq = TabelaFrequencias::nova(10); // Notas de 1 a 10

    // Registra notas dos alunos
    let notas = vec![7, 8, 6, 9, 7, 5, 8, 10, 7, 6, 8, 9, 4, 7, 8];
    for &nota in &notas {
        freq.registrar(nota);
    }

    println!("Distribuição de notas:");
    for nota in 1..=10 {
        let qtd = freq.contar_intervalo(nota, nota);
        let barra = "#".repeat(qtd as usize);
        println!("  Nota {:>2}: {:>2} {}", nota, qtd, barra);
    }

    println!("\nEstatísticas:");
    println!("  Total de alunos: {}", freq.total());
    println!("  Aprovados (nota >= 6): {}", freq.contar_intervalo(6, 10));
    println!("  Reprovados (nota < 6): {}", freq.contar_ate(5));

    // Mediana
    let n = freq.total();
    let mediana_pos = (n + 1) / 2;
    if let Some(mediana) = freq.k_esimo(mediana_pos) {
        println!("  Mediana ({}º valor): {}", mediana_pos, mediana);
    }

    // === Range Update ===
    println!("\n=== Range Update: Bonificação Salarial ===\n");

    // Salários de 8 funcionários (em centenas de R$)
    let n = 8;
    let mut salarios = FenwickTreeRangeUpdate::novo(n);

    // Define salários iniciais
    let iniciais = vec![30, 35, 28, 40, 32, 45, 38, 50];
    for (i, &val) in iniciais.iter().enumerate() {
        salarios.atualizar_intervalo(i + 1, i + 1, val);
    }

    println!("Salários iniciais (x100 R$):");
    for i in 1..=n {
        print!("  [{}]={}", i, salarios.consultar(i));
    }
    println!();

    // Aumento de R$ 500 para funcionários 3 a 6
    println!("\nAumento de R$ 500 para funcionários 3-6:");
    salarios.atualizar_intervalo(3, 6, 5);

    for i in 1..=n {
        print!("  [{}]={}", i, salarios.consultar(i));
    }
    println!();
}

Exercícios

Exercício 1: Soma de Intervalo com Atualizações

Implemente um programa que recebe um array e uma sequência de operações: “U i v” (atualizar posição i com valor v) e “Q l r” (consultar soma do intervalo [l,r]). Use Fenwick Tree para responder cada consulta em O(log n).

fn processar_operacoes(dados: &[i64], operacoes: &[(&str, usize, i64)]) -> Vec<i64> {
    // Retorna as respostas das consultas Q
    todo!()
}

// Teste:
// dados = [1, 3, 5, 7, 9]
// operações: [("Q", 1, 3), ("U", 2, 10), ("Q", 1, 3)]
// => [9, 16]  (1+3+5=9, depois 1+10+5=16)

Exercício 2: Contagem de Inversões com Coordenadas Comprimidas

A implementação de contagem de inversões mostrada funciona apenas para valores pequenos. Modifique-a para funcionar com valores arbitrários usando compressão de coordenadas: mapeie os valores para o intervalo [1, n] preservando a ordem relativa.

fn contar_inversoes_grandes(arr: &[i64]) -> i64 {
    // Dica: ordene os valores únicos e use busca binária
    // para mapear cada valor para sua posição na ordenação
    todo!()
}

// Teste:
// [1000000, 1, 500000, 2] -> 4 inversões

Exercício 3: Soma 2D com Fenwick Tree

Use a FenwickTree2D para resolver o seguinte problema: dada uma matriz n x m com atualizações pontuais, responda consultas de soma em sub-matrizes.

fn soma_submatriz_dinamica(
    n: usize,
    m: usize,
    atualizacoes: &[(usize, usize, i64)],     // (lin, col, valor)
    consultas: &[(usize, usize, usize, usize)] // (l1, c1, l2, c2)
) -> Vec<i64> {
    // Retorna as respostas das consultas
    todo!()
}

Conclusão

A Fenwick Tree e uma joia da ciencia da computacao – surpreendentemente simples e extremamente eficiente:

  • Apenas ~20 linhas de codigo para uma implementacao completa
  • O(log n) para consultas e atualizacoes, com constante muito baixa
  • Memoria minima: apenas n+1 posicoes (vs 4n da Segment Tree)
  • O truque do lowbit (x & -x) e a chave de toda a elegancia
  • Ideal para prefix sums, contagem de inversoes e tabelas de frequencia

A principal limitacao e que so funciona com operacoes inversiveis (soma, XOR) – para minimo/maximo, a Segment Tree continua sendo necessaria. Mas para todos os problemas que ela resolve, a Fenwick Tree e a escolha mais pratica e performatica.


Conteudo produzido pela Equipe Rust Brasil para rustlang.com.br