Hashing de Strings em Rust

Tecnicas de hashing de strings em Rust: hash polinomial, rolling hash, comparacao de substrings em O(1) e deteccao de anagramas com analise de colisoes.

O hashing de strings e uma tecnica fundamental que permite comparar strings e substrings em tempo constante O(1) apos um pre-processamento linear. Em vez de comparar caractere a caractere (O(n)), calculamos um valor numerico (hash) que representa a string e comparamos os numeros. Embora colisoes sejam possiveis, com boas escolhas de parametros a probabilidade e tao baixa que a tecnica e extremamente confiavel na pratica.

As aplicacoes sao vastas: busca de padroes (Rabin-Karp), comparacao de substrings em O(1) para programacao dinamica, deteccao de palindromos, deduplicacao de strings, deteccao de anagramas e verificacao de integridade de dados. Nesta pagina, implementaremos hash polinomial com pre-processamento de prefixos, rolling hash e tecnicas para minimizar colisoes.

Como Funciona

Hash Polinomial

Tratamos cada string como um numero em uma base numerica, onde cada caractere e um “digito”:

hash(s) = s[0]*base^(n-1) + s[1]*base^(n-2) + ... + s[n-1]*base^0  (mod primo)

Exemplo: hash("abc") com base=31, primo=1000000007
  hash = 97*31^2 + 98*31 + 99
       = 97*961  + 98*31  + 99
       = 93217   + 3038   + 99
       = 96354

Na pratica usamos aritmetica modular para evitar overflow:
  hash = ((97 * 31 + 98) * 31 + 99) mod 1000000007
       = ((3007 + 98) * 31 + 99) mod 1000000007
       = (3105 * 31 + 99) mod 1000000007
       = (96255 + 99) mod 1000000007
       = 96354

Pre-processamento de Prefixos

Para comparar substrings em O(1), pre-calculamos o hash de cada prefixo:

String: "abcde"
         0 1 2 3 4

prefix_hash[0] = hash("") = 0
prefix_hash[1] = hash("a") = 97
prefix_hash[2] = hash("ab") = 97*31 + 98 = 3105
prefix_hash[3] = hash("abc") = 3105*31 + 99 = 96354
prefix_hash[4] = hash("abcd") = 96354*31 + 100 = 2986974
prefix_hash[5] = hash("abcde") = 2986974*31 + 101 = 92596295

Para extrair hash("bcd") = hash de s[1..4]:
  hash("bcd") = prefix_hash[4] - prefix_hash[1] * base^3
              = 2986974 - 97 * 29791
              = 2986974 - 2889727
              = 97247

Verificacao direta:
  hash("bcd") = 98*31^2 + 99*31 + 100 = 98*961 + 3069 + 100 = 97247  <<<

Rolling Hash (Hash Deslizante)

Ao deslizar uma janela pelo texto, atualizamos o hash em O(1):

Texto: "abcdef"    janela de tamanho 3

Janela "abc": hash = 97*31^2 + 98*31 + 99 = 96354
                      ^sai            ^entra

Janela "bcd": hash = (96354 - 97*31^2) * 31 + 100
            remover 'a'  deslizar   adicionar 'd'
                   = (96354 - 93217) * 31 + 100
                   = 3137 * 31 + 100
                   = 97347

Visualizacao:
  [a b c] d e f   hash = H1
   a [b c d] e f   hash = (H1 - a*base^2) * base + d = H2
   a b [c d e] f   hash = (H2 - b*base^2) * base + e = H3
   a b c [d e f]   hash = (H3 - c*base^2) * base + f = H4

Complexidade

OperacaoTempoEspaco
Pre-processamento (prefixos)O(n)O(n)
Hash de substring s[l..r]O(1)O(1)
Comparacao de substringsO(1)O(1)
Rolling hash (atualizar)O(1)O(1)
Construir hash completoO(n)O(1)

Comparacao: verificar igualdade de duas substrings sem hashing custa O(min(len1, len2)). Com hashing, a comparacao e O(1) com probabilidade de colisao ~1/primo.

Implementacao em Rust

/// Constantes para hash polinomial.
/// Base prima e modulo primo grande para minimizar colisoes.
const BASE: u64 = 31;
const MOD: u64 = 1_000_000_007;

/// Estrutura para hashing de strings com pre-processamento de prefixos.
/// Permite consultas de hash de substrings em O(1).
struct StringHasher {
    prefix_hash: Vec<u64>,  // hash de cada prefixo
    potencias: Vec<u64>,     // base^i mod MOD pre-calculadas
    n: usize,
}

impl StringHasher {
    /// Constroi o hasher a partir de uma string em O(n).
    fn novo(texto: &str) -> Self {
        let bytes = texto.as_bytes();
        let n = bytes.len();

        let mut prefix_hash = vec![0u64; n + 1];
        let mut potencias = vec![1u64; n + 1];

        // Pre-calcula potencias de base
        for i in 1..=n {
            potencias[i] = potencias[i - 1].wrapping_mul(BASE) % MOD;
        }

        // Pre-calcula hashes de prefixos
        for i in 0..n {
            // Valor do caractere: usamos (byte - b'a' + 1) para evitar hash 0
            // para strings de 'a'. Se a string pode conter qualquer byte, use byte + 1.
            let valor = (bytes[i] as u64).wrapping_add(1);
            prefix_hash[i + 1] = (prefix_hash[i].wrapping_mul(BASE) + valor) % MOD;
        }

        StringHasher {
            prefix_hash,
            potencias,
            n,
        }
    }

    /// Retorna o hash da substring s[esquerda..direita] em O(1).
    /// Indices baseados em 0, intervalo semi-aberto [esquerda, direita).
    fn hash_substring(&self, esquerda: usize, direita: usize) -> u64 {
        assert!(esquerda <= direita && direita <= self.n);

        let hash = (self.prefix_hash[direita]
            + MOD
            - self.prefix_hash[esquerda]
                .wrapping_mul(self.potencias[direita - esquerda])
                % MOD)
            % MOD;

        hash
    }

    /// Compara duas substrings em O(1).
    /// Retorna true se s[l1..r1] == s[l2..r2].
    fn substrings_iguais(&self, l1: usize, r1: usize, l2: usize, r2: usize) -> bool {
        if r1 - l1 != r2 - l2 {
            return false;
        }
        self.hash_substring(l1, r1) == self.hash_substring(l2, r2)
    }
}

/// Hasher com duplo hash para colisoes quase zero.
struct DuploHasher {
    hasher1: StringHasher,
    prefix_hash2: Vec<u64>,
    potencias2: Vec<u64>,
    n: usize,
}

const BASE2: u64 = 37;
const MOD2: u64 = 999_999_937;

impl DuploHasher {
    fn novo(texto: &str) -> Self {
        let bytes = texto.as_bytes();
        let n = bytes.len();

        let hasher1 = StringHasher::novo(texto);

        let mut prefix_hash2 = vec![0u64; n + 1];
        let mut potencias2 = vec![1u64; n + 1];

        for i in 1..=n {
            potencias2[i] = potencias2[i - 1].wrapping_mul(BASE2) % MOD2;
        }

        for i in 0..n {
            let valor = (bytes[i] as u64).wrapping_add(1);
            prefix_hash2[i + 1] = (prefix_hash2[i].wrapping_mul(BASE2) + valor) % MOD2;
        }

        DuploHasher {
            hasher1,
            prefix_hash2,
            potencias2,
            n,
        }
    }

    /// Retorna um par (hash1, hash2) para a substring.
    fn hash_substring(&self, esquerda: usize, direita: usize) -> (u64, u64) {
        let h1 = self.hasher1.hash_substring(esquerda, direita);
        let h2 = (self.prefix_hash2[direita]
            + MOD2
            - self.prefix_hash2[esquerda]
                .wrapping_mul(self.potencias2[direita - esquerda])
                % MOD2)
            % MOD2;

        (h1, h2)
    }

    fn substrings_iguais(&self, l1: usize, r1: usize, l2: usize, r2: usize) -> bool {
        if r1 - l1 != r2 - l2 {
            return false;
        }
        self.hash_substring(l1, r1) == self.hash_substring(l2, r2)
    }
}

fn main() {
    let texto = "abcabcxyzabc";

    println!("Texto: \"{}\"", texto);
    println!();

    let hasher = StringHasher::novo(texto);

    // Compara substrings
    let pares = vec![
        ((0, 3), (3, 6)),   // "abc" vs "abc"
        ((0, 3), (9, 12)),  // "abc" vs "abc"
        ((0, 3), (6, 9)),   // "abc" vs "xyz"
        ((0, 6), (3, 9)),   // "abcabc" vs "abcxyz"
    ];

    println!("Comparacao de substrings (hash simples):");
    for ((l1, r1), (l2, r2)) in &pares {
        let h1 = hasher.hash_substring(*l1, *r1);
        let h2 = hasher.hash_substring(*l2, *r2);
        let iguais = hasher.substrings_iguais(*l1, *r1, *l2, *r2);

        println!(
            "  \"{}\" (hash={}) vs \"{}\" (hash={}) -> {}",
            &texto[*l1..*r1],
            h1,
            &texto[*l2..*r2],
            h2,
            if iguais { "IGUAIS" } else { "DIFERENTES" }
        );
    }

    // Duplo hash
    println!();
    let duplo = DuploHasher::novo(texto);
    println!("Duplo hash para \"abc\"[0..3]: {:?}", duplo.hash_substring(0, 3));
    println!("Duplo hash para \"abc\"[3..6]: {:?}", duplo.hash_substring(3, 6));
    println!("Duplo hash para \"xyz\"[6..9]: {:?}", duplo.hash_substring(6, 9));
}

Exemplo de Execucao

Texto: "abcabcxyzabc"

Comparacao de substrings (hash simples):
  "abc" (hash=96354) vs "abc" (hash=96354) -> IGUAIS
  "abc" (hash=96354) vs "abc" (hash=96354) -> IGUAIS
  "abc" (hash=96354) vs "xyz" (hash=120234) -> DIFERENTES
  "abcabc" (hash=...) vs "abcxyz" (hash=...) -> DIFERENTES

Duplo hash para "abc"[0..3]: (96354, 150904)
Duplo hash para "abc"[3..6]: (96354, 150904)
Duplo hash para "xyz"[6..9]: (120234, 186724)

Variacoes e Otimizacoes

1. Deteccao de Anagramas

Anagramas tem os mesmos caracteres em ordem diferente. Podemos usar um hash invariante a ordem:

use std::collections::HashMap;

/// Hash invariante a ordem para deteccao de anagramas.
/// Usa a soma dos hashes individuais de cada caractere.
fn hash_anagrama(s: &str) -> u64 {
    // Usamos primos distintos para cada caractere para evitar colisoes
    let primos: Vec<u64> = vec![
        2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
        31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
        73, 79, 83, 89, 97, 101,
    ];

    let mut hash: u64 = 1;
    for byte in s.bytes() {
        let idx = (byte.to_ascii_lowercase() - b'a') as usize;
        if idx < primos.len() {
            hash = hash.wrapping_mul(primos[idx]) % MOD;
        }
    }
    hash
}

/// Encontra todos os pares de anagramas em uma lista de strings.
fn encontrar_anagramas(palavras: &[&str]) -> Vec<(String, String)> {
    let mut mapa: HashMap<u64, Vec<&str>> = HashMap::new();

    for &palavra in palavras {
        let hash = hash_anagrama(palavra);
        mapa.entry(hash).or_default().push(palavra);
    }

    let mut pares = Vec::new();
    for grupo in mapa.values() {
        if grupo.len() >= 2 {
            for i in 0..grupo.len() {
                for j in (i + 1)..grupo.len() {
                    // Verifica se realmente sao anagramas (confirma contra colisao)
                    let mut chars1: Vec<char> = grupo[i].chars().collect();
                    let mut chars2: Vec<char> = grupo[j].chars().collect();
                    chars1.sort();
                    chars2.sort();
                    if chars1 == chars2 {
                        pares.push((grupo[i].to_string(), grupo[j].to_string()));
                    }
                }
            }
        }
    }
    pares
}

/// Encontra todas as posicoes onde substrings do texto sao anagramas do padrao.
fn buscar_anagramas_substring(texto: &str, padrao: &str) -> Vec<usize> {
    let texto_bytes = texto.as_bytes();
    let padrao_bytes = padrao.as_bytes();
    let n = texto_bytes.len();
    let m = padrao_bytes.len();

    if m == 0 || m > n {
        return Vec::new();
    }

    // Conta a frequencia de cada caractere no padrao
    let mut freq_padrao = [0i32; 256];
    for &b in padrao_bytes {
        freq_padrao[b as usize] += 1;
    }

    // Janela deslizante
    let mut freq_janela = [0i32; 256];
    let mut posicoes = Vec::new();

    // Inicializa a primeira janela
    for i in 0..m {
        freq_janela[texto_bytes[i] as usize] += 1;
    }

    if freq_janela == freq_padrao {
        posicoes.push(0);
    }

    // Desliza a janela
    for i in 1..=(n - m) {
        // Remove caractere que sai
        freq_janela[texto_bytes[i - 1] as usize] -= 1;
        // Adiciona caractere que entra
        freq_janela[texto_bytes[i + m - 1] as usize] += 1;

        if freq_janela == freq_padrao {
            posicoes.push(i);
        }
    }

    posicoes
}

fn main() {
    // Encontrar anagramas
    let palavras = vec!["roma", "amor", "mora", "ramo", "gato", "toga"];
    let pares = encontrar_anagramas(&palavras);
    println!("Pares de anagramas:");
    for (a, b) in &pares {
        println!("  \"{}\" <-> \"{}\"", a, b);
    }
    println!();

    // Busca de anagramas como substrings
    let texto = "cbaebabacd";
    let padrao = "abc";
    let posicoes = buscar_anagramas_substring(texto, padrao);
    println!(
        "Anagramas de \"{}\" em \"{}\": posicoes {:?}",
        padrao, texto, posicoes
    );
    for pos in &posicoes {
        println!("  posicao {}: \"{}\"", pos, &texto[*pos..*pos + padrao.len()]);
    }
}

2. Deduplicacao de Strings

use std::collections::HashSet;

/// Remove strings duplicadas usando hashing.
/// Mais eficiente que ordenar + dedup para strings longas.
fn deduplicar(strings: &[&str]) -> Vec<String> {
    let mut vistos: HashSet<u64> = HashSet::new();
    let mut resultado = Vec::new();

    for &s in strings {
        let hasher = StringHasher::novo(s);
        let hash = hasher.hash_substring(0, s.len());

        if vistos.insert(hash) {
            resultado.push(s.to_string());
        }
    }

    resultado
}

/// Encontra substrings duplicadas de comprimento k em um texto.
fn substrings_duplicadas(texto: &str, k: usize) -> Vec<String> {
    let n = texto.len();
    if k == 0 || k > n {
        return Vec::new();
    }

    let hasher = StringHasher::novo(texto);
    let mut vistos: HashMap<u64, usize> = HashMap::new(); // hash -> primeira posicao
    let mut duplicadas: HashSet<String> = HashSet::new();

    for i in 0..=(n - k) {
        let hash = hasher.hash_substring(i, i + k);

        if let Some(&pos_anterior) = vistos.get(&hash) {
            // Confirma que realmente sao iguais (contra colisoes)
            if texto[i..i + k] == texto[pos_anterior..pos_anterior + k] {
                duplicadas.insert(texto[i..i + k].to_string());
            }
        } else {
            vistos.insert(hash, i);
        }
    }

    duplicadas.into_iter().collect()
}

3. Verificacao de Palindromos com Hash

/// Estrutura que permite verificar palindromos em O(1) por consulta.
struct PalindromeHasher {
    frente: StringHasher,
    prefix_hash_rev: Vec<u64>,
    potencias_rev: Vec<u64>,
    n: usize,
}

impl PalindromeHasher {
    fn novo(texto: &str) -> Self {
        let n = texto.len();
        let bytes = texto.as_bytes();

        let frente = StringHasher::novo(texto);

        // Calcula hashes da string reversa
        let mut prefix_hash_rev = vec![0u64; n + 1];
        let mut potencias_rev = vec![1u64; n + 1];

        for i in 1..=n {
            potencias_rev[i] = potencias_rev[i - 1].wrapping_mul(BASE) % MOD;
        }

        for i in 0..n {
            let valor = (bytes[n - 1 - i] as u64).wrapping_add(1);
            prefix_hash_rev[i + 1] = (prefix_hash_rev[i].wrapping_mul(BASE) + valor) % MOD;
        }

        PalindromeHasher {
            frente,
            prefix_hash_rev,
            potencias_rev,
            n,
        }
    }

    /// Hash da substring reversa correspondente a s[esquerda..direita].
    fn hash_reverso(&self, esquerda: usize, direita: usize) -> u64 {
        let rev_esq = self.n - direita;
        let rev_dir = self.n - esquerda;

        (self.prefix_hash_rev[rev_dir]
            + MOD
            - self.prefix_hash_rev[rev_esq]
                .wrapping_mul(self.potencias_rev[rev_dir - rev_esq])
                % MOD)
            % MOD
    }

    /// Verifica se s[esquerda..direita] e um palindromo em O(1).
    fn eh_palindromo(&self, esquerda: usize, direita: usize) -> bool {
        self.frente.hash_substring(esquerda, direita)
            == self.hash_reverso(esquerda, direita)
    }
}

fn main() {
    let texto = "abaacaaba";
    let hasher = PalindromeHasher::novo(texto);

    println!("Texto: \"{}\"", texto);
    println!();

    // Testa varias substrings
    let testes = vec![
        (0, 3, "aba"),
        (0, 5, "abaac"),
        (3, 6, "aca"),
        (5, 9, "aaba"),
        (0, 9, "abaacaaba"),
    ];

    println!("Verificacao de palindromos em O(1):");
    for (l, r, desc) in &testes {
        let resultado = hasher.eh_palindromo(*l, *r);
        println!(
            "  s[{}..{}] = \"{}\" -> {}",
            l,
            r,
            desc,
            if resultado { "PALINDROMO" } else { "NAO" }
        );
    }
}

Aplicacoes Praticas

Busca de Padroes com Rolling Hash (Rabin-Karp Simplificado)

/// Busca simples de padrao usando rolling hash.
fn buscar_padrao(texto: &str, padrao: &str) -> Vec<usize> {
    let n = texto.len();
    let m = padrao.len();

    if m == 0 || m > n {
        return Vec::new();
    }

    let hasher_texto = StringHasher::novo(texto);
    let hasher_padrao = StringHasher::novo(padrao);
    let hash_padrao = hasher_padrao.hash_substring(0, m);

    let mut posicoes = Vec::new();

    for i in 0..=(n - m) {
        if hasher_texto.hash_substring(i, i + m) == hash_padrao {
            // Confirma contra colisoes
            if texto[i..i + m] == *padrao {
                posicoes.push(i);
            }
        }
    }

    posicoes
}

Maior Prefixo Comum com Busca Binaria

/// Encontra o comprimento do maior prefixo comum entre
/// s[a..] e s[b..] em O(log n) usando hashing + busca binaria.
fn maior_prefixo_comum(hasher: &StringHasher, a: usize, b: usize, n: usize) -> usize {
    let max_len = n.saturating_sub(a).min(n.saturating_sub(b));

    let mut low = 0;
    let mut high = max_len;

    while low < high {
        let mid = low + (high - low + 1) / 2;
        if hasher.hash_substring(a, a + mid) == hasher.hash_substring(b, b + mid) {
            low = mid;
        } else {
            high = mid - 1;
        }
    }

    low
}

fn main() {
    let texto = "abcabcxabc";
    let hasher = StringHasher::novo(texto);
    let n = texto.len();

    let a = 0; // "abcabcxabc"
    let b = 3; // "abcxabc"

    let lcp = maior_prefixo_comum(&hasher, a, b, n);
    println!(
        "Maior prefixo comum entre s[{}..] e s[{}..]: {} (\"{}\")",
        a,
        b,
        lcp,
        &texto[a..a + lcp]
    );
}

Analise de Colisoes

/// Analisa a taxa de colisoes para um dado conjunto de strings.
fn analisar_colisoes(strings: &[&str]) {
    let mut hashes_simples: HashMap<u64, Vec<&str>> = HashMap::new();
    let mut hashes_duplo: HashMap<(u64, u64), Vec<&str>> = HashMap::new();

    for &s in strings {
        let hasher = StringHasher::novo(s);
        let h1 = hasher.hash_substring(0, s.len());

        let duplo = DuploHasher::novo(s);
        let h2 = duplo.hash_substring(0, s.len());

        hashes_simples.entry(h1).or_default().push(s);
        hashes_duplo.entry(h2).or_default().push(s);
    }

    let colisoes_simples = hashes_simples.values().filter(|v| v.len() > 1).count();
    let colisoes_duplo = hashes_duplo.values().filter(|v| v.len() > 1).count();

    println!("Total de strings: {}", strings.len());
    println!("Hashes unicos (simples): {}", hashes_simples.len());
    println!("Colisoes (simples): {}", colisoes_simples);
    println!("Hashes unicos (duplo):   {}", hashes_duplo.len());
    println!("Colisoes (duplo):   {}", colisoes_duplo);

    // Mostra colisoes se existirem
    for (hash, grupo) in &hashes_simples {
        if grupo.len() > 1 {
            println!("  Colisao hash={}: {:?}", hash, grupo);
        }
    }
}

Comparacao com Outros Algoritmos

TecnicaPre-proc.Comparacao SubstringEspacoColisoes
Comparacao diretaO(1)O(min(n,m))O(1)Nenhuma
Hash simplesO(n)O(1)O(n)~1/primo
Hash duploO(n)O(1)O(n)~1/primo^2
Suffix ArrayO(n*logn)O(m*logn)O(n)Nenhuma
Suffix TreeO(n)O(m)O(n)Nenhuma

O hashing de strings oferece o melhor custo-beneficio: pre-processamento linear, comparacoes em O(1) e implementacao simples. A unica desvantagem sao as colisoes, que podem ser minimizadas com duplo hashing (probabilidade ~10^-18).

Analise de Colisoes

A probabilidade de colisao entre duas strings aleatorias com um unico hash modular e:

P(colisao) = 1/MOD ~ 10^-9  (com MOD = 10^9 + 7)

Para n consultas (paradoxo do aniversario):
P(pelo menos 1 colisao) ~ n^2 / (2 * MOD)

Exemplos:
  1.000 consultas:   P ~ 5 * 10^-4   (raro)
  100.000 consultas: P ~ 5 * 10^0    (provavel!)

Com duplo hash:
  P(colisao) ~ 1/(MOD1 * MOD2) ~ 10^-18
  100.000 consultas: P ~ 5 * 10^-9   (praticamente impossivel)

Recomendacao: use sempre duplo hash em competicoes e aplicacoes criticas. Para uso casual (busca em texto, deduplicacao), hash simples com primo grande e suficiente.

Exercicios Praticos

  1. Maior substring comum: Dadas duas strings, encontre a maior substring comum usando hashing + busca binaria no comprimento. Para cada comprimento candidato, calcule todos os hashes de substrings de ambas as strings e procure correspondencias.

  2. Contagem de substrings distintas: Conte o numero de substrings distintas de uma string. Para cada comprimento k de 1 a n, insira os hashes de todas as substrings de comprimento k em um HashSet e some os tamanhos.

  3. Rotacao lexicograficamente minima: Dada uma string circular, encontre a rotacao que e lexicograficamente menor. Use hashing + busca binaria para comparar prefixos comuns em O(log n).

  4. Verificacao de periodicidade: Dada uma string S, determine se ela e periodica (formada pela repeticao de um padrao menor). Para cada divisor d de n, verifique se hash(S[0..d]) gera toda a string quando repetido.

Veja Tambem