Segment Tree em Rust: Consultas em Intervalos

Aprenda Segment Tree em Rust: range queries, construção, atualização, lazy propagation, consulta de mínimo/soma em intervalos e exemplos práticos com código completo.

A Segment Tree (Árvore de Segmentos) é uma estrutura de dados que permite realizar consultas e atualizações em intervalos de um array em tempo O(log n). Ela é essencial para problemas como: “qual é a soma dos elementos do índice 2 ao 7?” ou “qual é o mínimo entre as posições 3 e 10?”. Com lazy propagation, também suporta atualizações em intervalos inteiros de forma eficiente.


Conceito e Teoria

O Problema de Range Query

Dado um array de n elementos, queremos responder repetidamente:

  • Range Query: qual é o resultado de uma operação (soma, mínimo, máximo, GCD…) em um intervalo [l, r]?
  • Point Update: atualizar o valor em uma posição específica
  • Range Update: atualizar todos os valores em um intervalo [l, r]
  Array:  [2, 5, 1, 4, 9, 3, 7, 6]

  Consulta soma [1, 5]:
  5 + 1 + 4 + 9 + 3 = 22

  Sem Segment Tree: O(n) por consulta
  Com Segment Tree: O(log n) por consulta!

Estrutura da Árvore

A Segment Tree divide o array recursivamente em metades, armazenando o resultado da operação em cada nó:

  Array original: [2, 5, 1, 4, 9, 3, 7, 6]
  Índices:         0  1  2  3  4  5  6  7

  Segment Tree (soma):

                        [37]                    nó 1: soma [0,7]
                       /    \
                  [12]        [25]              nós 2,3: [0,3] e [4,7]
                 /    \      /    \
              [7]     [5]  [12]   [13]          nós 4-7: pares
             /  \    / \   / \    / \
           [2] [5] [1][4] [9][3] [7][6]        nós 8-15: folhas

  Cada nó armazena a soma do seu intervalo.
  Nó i tem filhos 2*i (esquerdo) e 2*i+1 (direito).

Operação de Consulta (Query)

  Consulta soma [2, 5] no array [2, 5, 1, 4, 9, 3, 7, 6]:

                        [37] [0,7]
                       /    \
                  [12] [0,3] [25] [4,7]
                 /    \      /    \
              [7]    [5]  [12]   [13]
  [0,1]      /  \    / \   / \    / \   [6,7]
           [2] [5] [1][4] [9][3] [7][6]
                    ^  ^   ^  ^
                   [2][3] [4][5]   <- intervalo [2,5]

  Combinamos: [1] + [4] + [9] + [3] = 17
  Ou pelo nó [5] (soma [2,3]) + [12] (soma [4,5]) = 5 + 12 = 17

Operação de Atualização (Update)

  Atualizar posição 3: valor 4 -> 10

  Apenas os nós no caminho da folha até a raiz são atualizados:

                     [37]->[43]                   +6 em cada nó
                       /    \                      no caminho
                [12]->[18]   [25]
                 /    \
              [7]  [5]->[11]
                    / \
                  [1] [4]->[10]   <- folha atualizada

  Apenas O(log n) = 3 nós atualizados (não o array inteiro!)

Lazy Propagation

Para atualizações em intervalos, usamos “preguiça” – adiamos as atualizações até que sejam necessárias:

  "Some 5 a todos os elementos em [0, 3]"

  SEM lazy: atualiza 4 folhas = O(n)
  COM lazy: marca o nó [0,3] como "pendente +5"

                        [37]
                       /    \
                  [12]       [25]
                 lazy:+5
                 /    \
              [7]     [5]      <- Não atualiza agora!
             /  \    / \
           [2] [5] [1][4]     <- Folhas não tocadas

  Quando uma consulta ou atualização precisar descer abaixo de [0,3],
  a propagação acontece naquele momento ("push down").

Complexidade

OperaçãoComplexidadeObservação
ConstruirO(n)Percorre cada elemento uma vez
Consulta (query)O(log n)Visita O(log n) nós
Atualização pontualO(log n)Atualiza caminho raiz-folha
Atualização em intervaloO(log n)Com lazy propagation
EspaçoO(n)Exatamente 4n (ou 2próxima pot.

Implementação em Rust

Segment Tree para Soma com Atualização Pontual

/// Segment Tree para consultas de soma em intervalos.
/// Suporta atualização pontual e consulta de intervalo em O(log n).
struct SegmentTree {
    /// Array interno da árvore (indexado a partir de 1)
    arvore: Vec<i64>,
    /// Tamanho do array original
    n: usize,
}

impl SegmentTree {
    /// Constrói a Segment Tree a partir de um array.
    fn construir(dados: &[i64]) -> Self {
        let n = dados.len();
        let mut arvore = vec![0i64; 4 * n]; // Espaço suficiente

        fn build(arvore: &mut Vec<i64>, dados: &[i64], no: usize, inicio: usize, fim: usize) {
            if inicio == fim {
                // Nó folha: armazena o valor diretamente
                arvore[no] = dados[inicio];
                return;
            }

            let meio = (inicio + fim) / 2;
            build(arvore, dados, 2 * no, inicio, meio);       // Filho esquerdo
            build(arvore, dados, 2 * no + 1, meio + 1, fim);  // Filho direito

            // O nó pai armazena a soma dos filhos
            arvore[no] = arvore[2 * no] + arvore[2 * no + 1];
        }

        if n > 0 {
            build(&mut arvore, dados, 1, 0, n - 1);
        }

        SegmentTree { arvore, n }
    }

    /// Consulta a soma no intervalo [l, r] (0-indexado).
    fn consultar(&self, l: usize, r: usize) -> i64 {
        fn query(arvore: &[i64], no: usize, inicio: usize, fim: usize, l: usize, r: usize) -> i64 {
            // Caso 1: intervalo completamente fora
            if r < inicio || fim < l {
                return 0; // Elemento neutro da soma
            }

            // Caso 2: intervalo completamente dentro
            if l <= inicio && fim <= r {
                return arvore[no];
            }

            // Caso 3: intervalo parcialmente sobreposto
            let meio = (inicio + fim) / 2;
            let esq = query(arvore, 2 * no, inicio, meio, l, r);
            let dir = query(arvore, 2 * no + 1, meio + 1, fim, l, r);
            esq + dir
        }

        if self.n == 0 {
            return 0;
        }
        query(&self.arvore, 1, 0, self.n - 1, l, r)
    }

    /// Atualiza o valor na posição pos para novo_valor.
    fn atualizar(&mut self, pos: usize, novo_valor: i64) {
        fn update(
            arvore: &mut Vec<i64>,
            no: usize,
            inicio: usize,
            fim: usize,
            pos: usize,
            valor: i64,
        ) {
            if inicio == fim {
                arvore[no] = valor;
                return;
            }

            let meio = (inicio + fim) / 2;
            if pos <= meio {
                update(arvore, 2 * no, inicio, meio, pos, valor);
            } else {
                update(arvore, 2 * no + 1, meio + 1, fim, pos, valor);
            }

            // Recalcula o nó pai
            arvore[no] = arvore[2 * no] + arvore[2 * no + 1];
        }

        update(&mut self.arvore, 1, 0, self.n - 1, pos, novo_valor);
    }
}

Segment Tree com Lazy Propagation

/// Segment Tree com Lazy Propagation para atualizações em intervalos.
/// Suporta: somar um valor a todo um intervalo [l, r] em O(log n).
struct SegmentTreeLazy {
    arvore: Vec<i64>,
    lazy: Vec<i64>,     // Atualizações pendentes
    n: usize,
}

impl SegmentTreeLazy {
    fn construir(dados: &[i64]) -> Self {
        let n = dados.len();
        let mut st = SegmentTreeLazy {
            arvore: vec![0; 4 * n],
            lazy: vec![0; 4 * n],
            n,
        };

        if n > 0 {
            st.build(dados, 1, 0, n - 1);
        }
        st
    }

    fn build(&mut self, dados: &[i64], no: usize, inicio: usize, fim: usize) {
        if inicio == fim {
            self.arvore[no] = dados[inicio];
            return;
        }
        let meio = (inicio + fim) / 2;
        self.build(dados, 2 * no, inicio, meio);
        self.build(dados, 2 * no + 1, meio + 1, fim);
        self.arvore[no] = self.arvore[2 * no] + self.arvore[2 * no + 1];
    }

    /// Propaga a atualização pendente para os filhos ("push down")
    fn propagar(&mut self, no: usize, inicio: usize, fim: usize) {
        if self.lazy[no] != 0 {
            let meio = (inicio + fim) / 2;

            // Atualiza os filhos
            self.arvore[2 * no] += self.lazy[no] * (meio - inicio + 1) as i64;
            self.arvore[2 * no + 1] += self.lazy[no] * (fim - meio) as i64;

            // Passa a preguiça para os filhos
            self.lazy[2 * no] += self.lazy[no];
            self.lazy[2 * no + 1] += self.lazy[no];

            // Limpa a preguiça do nó atual
            self.lazy[no] = 0;
        }
    }

    /// Soma `valor` a todos os elementos no intervalo [l, r].
    fn atualizar_intervalo(&mut self, l: usize, r: usize, valor: i64) {
        self.update_range(1, 0, self.n - 1, l, r, valor);
    }

    fn update_range(
        &mut self,
        no: usize,
        inicio: usize,
        fim: usize,
        l: usize,
        r: usize,
        valor: i64,
    ) {
        if r < inicio || fim < l {
            return; // Fora do intervalo
        }

        if l <= inicio && fim <= r {
            // Intervalo completamente coberto: aplica lazy
            self.arvore[no] += valor * (fim - inicio + 1) as i64;
            self.lazy[no] += valor;
            return;
        }

        // Propaga antes de descer
        self.propagar(no, inicio, fim);

        let meio = (inicio + fim) / 2;
        self.update_range(2 * no, inicio, meio, l, r, valor);
        self.update_range(2 * no + 1, meio + 1, fim, l, r, valor);

        self.arvore[no] = self.arvore[2 * no] + self.arvore[2 * no + 1];
    }

    /// Consulta a soma no intervalo [l, r].
    fn consultar(&mut self, l: usize, r: usize) -> i64 {
        self.query_range(1, 0, self.n - 1, l, r)
    }

    fn query_range(
        &mut self,
        no: usize,
        inicio: usize,
        fim: usize,
        l: usize,
        r: usize,
    ) -> i64 {
        if r < inicio || fim < l {
            return 0;
        }

        if l <= inicio && fim <= r {
            return self.arvore[no];
        }

        self.propagar(no, inicio, fim);

        let meio = (inicio + fim) / 2;
        let esq = self.query_range(2 * no, inicio, meio, l, r);
        let dir = self.query_range(2 * no + 1, meio + 1, fim, l, r);
        esq + dir
    }
}

Segment Tree Genérica para Mínimo

/// Segment Tree para consultas de mínimo em intervalos (Range Minimum Query).
struct SegmentTreeMin {
    arvore: Vec<i64>,
    n: usize,
}

impl SegmentTreeMin {
    fn construir(dados: &[i64]) -> Self {
        let n = dados.len();
        let mut arvore = vec![i64::MAX; 4 * n];

        fn build(arvore: &mut Vec<i64>, dados: &[i64], no: usize, inicio: usize, fim: usize) {
            if inicio == fim {
                arvore[no] = dados[inicio];
                return;
            }
            let meio = (inicio + fim) / 2;
            build(arvore, dados, 2 * no, inicio, meio);
            build(arvore, dados, 2 * no + 1, meio + 1, fim);
            arvore[no] = arvore[2 * no].min(arvore[2 * no + 1]); // Mínimo!
        }

        if n > 0 {
            build(&mut arvore, dados, 1, 0, n - 1);
        }
        SegmentTreeMin { arvore, n }
    }

    fn consultar_minimo(&self, l: usize, r: usize) -> i64 {
        fn query(arvore: &[i64], no: usize, inicio: usize, fim: usize, l: usize, r: usize) -> i64 {
            if r < inicio || fim < l {
                return i64::MAX; // Elemento neutro do mínimo
            }
            if l <= inicio && fim <= r {
                return arvore[no];
            }
            let meio = (inicio + fim) / 2;
            let esq = query(arvore, 2 * no, inicio, meio, l, r);
            let dir = query(arvore, 2 * no + 1, meio + 1, fim, l, r);
            esq.min(dir)
        }

        if self.n == 0 { return i64::MAX; }
        query(&self.arvore, 1, 0, self.n - 1, l, r)
    }
}

Exemplo Prático: Sistema de Análise de Temperaturas

fn main() {
    // === Segment Tree para Soma ===
    println!("=== Análise de Temperaturas Diárias ===\n");

    // Temperaturas médias diárias (em °C x 10 para usar inteiros)
    let temperaturas: Vec<i64> = vec![225, 240, 195, 280, 310, 255, 200, 185,
                                       270, 295, 320, 290, 245, 210, 190];
    // Representam: 22.5, 24.0, 19.5, 28.0, 31.0, 25.5, 20.0, 18.5, ...

    let n = temperaturas.len();
    let mut st = SegmentTree::construir(&temperaturas);

    // Soma de temperaturas na primeira semana (índices 0-6)
    let soma_semana1 = st.consultar(0, 6);
    println!("Soma das temperaturas (semana 1): {:.1}°C", soma_semana1 as f64 / 10.0);
    println!("Média da semana 1: {:.1}°C", soma_semana1 as f64 / 70.0);

    // Soma da segunda semana
    let soma_semana2 = st.consultar(7, 13);
    println!("Média da semana 2: {:.1}°C", soma_semana2 as f64 / 70.0);

    // Atualiza temperatura do dia 3 (correção de sensor)
    println!("\nCorrigindo temperatura do dia 4: 28.0 -> 26.5°C");
    st.atualizar(3, 265);
    let nova_soma = st.consultar(0, 6);
    println!("Nova média da semana 1: {:.1}°C", nova_soma as f64 / 70.0);

    // === Segment Tree para Mínimo ===
    println!("\n=== Consulta de Temperatura Mínima ===\n");

    let st_min = SegmentTreeMin::construir(&temperaturas);

    let min_semana1 = st_min.consultar_minimo(0, 6);
    println!("Menor temperatura da semana 1: {:.1}°C", min_semana1 as f64 / 10.0);

    let min_total = st_min.consultar_minimo(0, n - 1);
    println!("Menor temperatura do período: {:.1}°C", min_total as f64 / 10.0);

    // Consultas variadas
    for (l, r) in [(0, 3), (4, 7), (8, 11), (12, 14)] {
        let min = st_min.consultar_minimo(l, r);
        let soma = st.consultar(l, r);
        let qtd = (r - l + 1) as f64;
        println!(
            "  Dias {}-{}: min={:.1}°C, média={:.1}°C",
            l + 1, r + 1,
            min as f64 / 10.0,
            soma as f64 / (qtd * 10.0)
        );
    }

    // === Lazy Propagation ===
    println!("\n=== Lazy Propagation: Ajuste em Lote ===\n");

    let vendas: Vec<i64> = vec![100, 150, 200, 120, 180, 160, 140, 190];
    let mut st_lazy = SegmentTreeLazy::construir(&vendas);

    println!("Vendas totais (dias 1-4): {}", st_lazy.consultar(0, 3));
    println!("Vendas totais (dias 5-8): {}", st_lazy.consultar(4, 7));

    // Promoção: +50 unidades vendidas nos dias 3-6
    println!("\nAplicando promoção nos dias 3-6 (+50 unidades)...");
    st_lazy.atualizar_intervalo(2, 5, 50);

    println!("Vendas totais (dias 1-4): {}", st_lazy.consultar(0, 3));
    println!("Vendas totais (dias 5-8): {}", st_lazy.consultar(4, 7));
    println!("Vendas totais (todos): {}", st_lazy.consultar(0, 7));
}

Exercícios

Exercício 1: Range Maximum Query

Implemente uma Segment Tree para consultas de máximo em intervalos. Suporte atualização pontual e consulta de máximo.

struct SegmentTreeMax {
    arvore: Vec<i64>,
    n: usize,
}

impl SegmentTreeMax {
    fn construir(dados: &[i64]) -> Self { todo!() }
    fn consultar_maximo(&self, l: usize, r: usize) -> i64 { todo!() }
    fn atualizar(&mut self, pos: usize, valor: i64) { todo!() }
}

Exercício 2: Segment Tree com GCD

Implemente uma Segment Tree que computa o MDC (Máximo Divisor Comum / GCD) de um intervalo. A operação de combinação é gcd(a, b) em vez de soma ou min.

fn gcd(a: i64, b: i64) -> i64 {
    if b == 0 { a.abs() } else { gcd(b, a % b) }
}

struct SegmentTreeGCD {
    arvore: Vec<i64>,
    n: usize,
}

impl SegmentTreeGCD {
    fn construir(dados: &[i64]) -> Self { todo!() }
    fn consultar_gcd(&self, l: usize, r: usize) -> i64 { todo!() }
    fn atualizar(&mut self, pos: usize, valor: i64) { todo!() }
}

// Teste:
// dados = [12, 8, 6, 18, 24]
// gcd([0, 2]) = gcd(12, 8, 6) = 2
// gcd([2, 4]) = gcd(6, 18, 24) = 6

Exercício 3: Segment Tree com Atribuição em Intervalo

Modifique a Segment Tree com lazy propagation para suportar a operação de atribuição em vez de soma: “defina todos os elementos em [l, r] como o valor x”.

struct SegmentTreeAtribuicao {
    arvore: Vec<i64>,
    lazy: Vec<Option<i64>>,  // None = sem pendência, Some(x) = definir como x
    n: usize,
}

impl SegmentTreeAtribuicao {
    fn construir(dados: &[i64]) -> Self { todo!() }
    fn atribuir_intervalo(&mut self, l: usize, r: usize, valor: i64) { todo!() }
    fn consultar_soma(&mut self, l: usize, r: usize) -> i64 { todo!() }
}

Conclusão

A Segment Tree e uma estrutura fundamental para problemas que envolvem:

  • Consultas em intervalos (soma, minimo, maximo, GCD, etc.) em O(log n)
  • Atualizacoes pontuais em O(log n) – muito melhor que O(n) de forca bruta
  • Atualizacoes em intervalos com lazy propagation em O(log n)
  • Persistencia: versoes anteriores podem ser mantidas com Segment Tree persistente

E amplamente utilizada em programacao competitiva, sistemas de banco de dados (range queries), analise de series temporais e computacao geometrica. Embora a implementacao seja mais extensa que a Fenwick Tree (proxima pagina), a Segment Tree e mais geral e pode resolver qualquer problema de range query que combine com uma operacao associativa.


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