Algoritmo de Manacher em Rust

Algoritmo de Manacher em Rust para encontrar a maior substring palindromica em tempo linear O(n) com diagramas visuais e comparacoes.

O algoritmo de Manacher resolve o problema de encontrar a maior substring palindromica em tempo linear O(n). Um palindromo e uma string que se le da mesma forma da esquerda para a direita e da direita para a esquerda (como “arara” ou “reviver”). Enquanto a abordagem de expandir ao redor de cada centro custa O(n^2) no pior caso, Manacher aproveita informacoes de palindromos ja descobertos para evitar comparacoes redundantes.

O algoritmo e notavel por sua elegancia: usa um truque de transformacao para tratar palindromos de comprimento par e impar de forma uniforme, e mantem uma “fronteira direita” que avanca monotonicamente pelo texto. Foi publicado por Glenn Manacher em 1975 e permanece como a solucao otima para este problema classico.

Como Funciona

O Truque da Transformacao

Para tratar uniformemente palindromos pares e impares, inserimos um caractere separador (ex.: ‘#’) entre cada caractere e nas extremidades:

Original:    "abba"
Transformada: "#a#b#b#a#"

Palindromo impar "aba":    -> "#a#b#a#"    (centro no 'b')
Palindromo par   "abba":   -> "#a#b#b#a#"  (centro no '#' entre os b's)

Agora TODOS os palindromos na string transformada sao impares!

Array P (Raio do Palindromo)

Para cada posicao i na string transformada, P[i] armazena o raio do maior palindromo centrado em i:

Transformada: # a # b # b # a #
Indice:       0 1 2 3 4 5 6 7 8
P:            0 1 0 1 4 1 0 1 0

Verificacao de P[4]=4:
  Centro: indice 4 (caractere '#')
  Palindromo: T[0..8] = "#a#b#b#a#"
  Raio 4: 4 caracteres para cada lado
  Corresponde ao palindromo original "abba" (comprimento = P[4] = 4)

O Algoritmo: Espelho e Fronteira

A sacada do Manacher: ao calcular P[i], usamos palindromos ja descobertos:

String: # a # b # a # b # a #
        0 1 2 3 4 5 6 7 8 9 10

Suponha que ja calculamos ate i=7 e encontramos:
  Centro C=5, Borda direita R=10

  P = [0, 1, 0, 3, 0, 5, 0, ?, ?, ?, ?]

Para i=7:
  Espelho de i em relacao a C:  espelho = 2*C - i = 2*5 - 7 = 3
  P[espelho] = P[3] = 3

  i esta dentro da borda R (i < R=10)
  Logo: P[i] >= min(P[espelho], R - i) = min(3, 10-7) = 3

  Diagrama:
  # a # b # a # b # a #
  0 1 2 3 4 5 6 7 8 9 10
          |<--- C=5 --->|  R=10
      |esp|     |  i  |
      3         7

  O palindromo ao redor de i=7 espelha o palindromo ao redor de i=3
  (ambos estao dentro do palindromo grande centrado em C=5)

  P[7] comeca com 3 e tenta expandir alem (mas nao consegue)
  P[7] = 3

Se P[i] + i > R, atualizamos C=i e R=P[i]+i (nova fronteira)

Trace Completo

Texto original: "abaaba"
Transformada T: "#a#b#a#a#b#a#"
                 0 1 2 3 4 5 6 7 8 9 10 11 12

C=0, R=0

i=1: R=0, i>=R -> expande manualmente
     T[0]=='#'==T[2]? sim  -> P[1]=1
     T[-1]? nao            -> P[1]=1
     C=1, R=2

i=2: espelho=0, min(P[0], R-2)=min(0,0)=0
     expande: nada          -> P[2]=0

i=3: i>R=2 -> expande manualmente
     T[2]=='#'==T[4]? sim  -> P[3]=1
     T[1]=='a'==T[5]? sim  -> P[3]=2
     T[0]=='#'==T[6]? sim  -> P[3]=3
     T[-1]? nao            -> P[3]=3
     C=3, R=6

i=4: espelho=2, min(P[2], R-4)=min(0,2)=0
     expande: nada          -> P[4]=0

i=5: espelho=1, min(P[1], R-5)=min(1,1)=1
     T[3]=='b'==T[7]? nao  -> P[5]=1
     (nao atualiza C,R pois 5+1=6=R)

i=6: espelho=0, min(P[0], R-6)=min(0,0)=0
     expande: T[5]=='a'==T[7]? sim  -> P[6]=1
     T[4]=='#'==T[8]? sim          -> P[6]=2
     T[3]=='b'==T[9]? sim          -> P[6]=3
     T[2]=='#'==T[10]? sim         -> P[6]=4
     T[1]=='a'==T[11]? sim         -> P[6]=5
     T[0]=='#'==T[12]? sim         -> P[6]=6
     C=6, R=12

...continuando...

P = [0, 1, 0, 3, 0, 1, 6, 1, 0, 3, 0, 1, 0]

Maior P[i] = 6, na posicao 6
Palindromo na string original: centro = (6-1)/2 = 2 (... nao, melhor calcular)
  inicio_original = (6 - P[6]) / 2 = (6 - 6) / 2 = 0
  comprimento = P[6] = 6
  palindromo = "abaaba"

Complexidade

CasoTempoEspaco
MelhorO(n)O(n)
MedioO(n)O(n)
PiorO(n)O(n)

A borda direita R nunca diminui e avanca no maximo n vezes no total. Cada posicao e visitada no maximo uma vez na expansao, garantindo O(n) amortizado. O espaco O(n) e para a string transformada e o array P.

Implementacao em Rust

/// Encontra a maior substring palindromica usando o algoritmo de Manacher.
/// Retorna a substring palindromica e sua posicao no texto original.
fn manacher(texto: &str) -> (String, usize) {
    let bytes = texto.as_bytes();
    let n = bytes.len();

    if n == 0 {
        return (String::new(), 0);
    }

    // Constroi a string transformada com separadores '#'
    // "#a#b#c#" para "abc"
    let mut t: Vec<u8> = Vec::with_capacity(2 * n + 1);
    for &b in bytes {
        t.push(b'#');
        t.push(b);
    }
    t.push(b'#');

    let t_len = t.len();
    let mut p = vec![0usize; t_len]; // raio do palindromo em cada posicao
    let mut centro = 0usize; // centro do palindromo com maior borda direita
    let mut borda_dir = 0usize; // borda direita desse palindromo

    for i in 0..t_len {
        // Usa o espelho se estamos dentro da borda direita
        if i < borda_dir {
            let espelho = 2 * centro - i;
            p[i] = p[espelho].min(borda_dir - i);
        }

        // Tenta expandir alem do valor inicial
        let mut esq = i as isize - p[i] as isize - 1;
        let mut dir = i + p[i] + 1;

        while esq >= 0 && dir < t_len && t[esq as usize] == t[dir] {
            p[i] += 1;
            esq -= 1;
            dir += 1;
        }

        // Atualiza centro e borda se expandimos alem
        if i + p[i] > borda_dir {
            centro = i;
            borda_dir = i + p[i];
        }
    }

    // Encontra o maior palindromo
    let mut max_raio = 0;
    let mut max_centro = 0;

    for i in 0..t_len {
        if p[i] > max_raio {
            max_raio = p[i];
            max_centro = i;
        }
    }

    // Converte de volta para coordenadas do texto original
    let inicio = (max_centro - max_raio) / 2;
    let comprimento = max_raio;

    (texto[inicio..inicio + comprimento].to_string(), inicio)
}

/// Encontra TODOS os palindromos centrados em cada posicao.
/// Retorna um vetor de (inicio, comprimento) para cada palindromo maximal.
fn todos_palindromos(texto: &str) -> Vec<(usize, usize)> {
    let bytes = texto.as_bytes();
    let n = bytes.len();

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

    let mut t: Vec<u8> = Vec::with_capacity(2 * n + 1);
    for &b in bytes {
        t.push(b'#');
        t.push(b);
    }
    t.push(b'#');

    let t_len = t.len();
    let mut p = vec![0usize; t_len];
    let mut centro = 0usize;
    let mut borda_dir = 0usize;

    for i in 0..t_len {
        if i < borda_dir {
            let espelho = 2 * centro - i;
            p[i] = p[espelho].min(borda_dir - i);
        }

        let mut esq = i as isize - p[i] as isize - 1;
        let mut dir = i + p[i] + 1;

        while esq >= 0 && dir < t_len && t[esq as usize] == t[dir] {
            p[i] += 1;
            esq -= 1;
            dir += 1;
        }

        if i + p[i] > borda_dir {
            centro = i;
            borda_dir = i + p[i];
        }
    }

    // Coleta palindromos com comprimento >= 2
    let mut palindromos = Vec::new();

    for i in 0..t_len {
        if p[i] >= 2 {
            let inicio = (i - p[i]) / 2;
            let comprimento = p[i];
            palindromos.push((inicio, comprimento));
        }
    }

    palindromos
}

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

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

    // Maior palindromo
    let (palindromo, posicao) = manacher(texto);
    println!(
        "Maior palindromo: \"{}\" (posicao {}, comprimento {})",
        palindromo,
        posicao,
        palindromo.len()
    );
    println!();

    // Todos os palindromos
    let todos = todos_palindromos(texto);
    println!("Todos os palindromos (comprimento >= 2):");
    for (inicio, comprimento) in &todos {
        println!(
            "  posicao {}: \"{}\"",
            inicio,
            &texto[*inicio..*inicio + *comprimento]
        );
    }
    println!();

    // Mais exemplos
    let exemplos = vec![
        "racecar",
        "babad",
        "cbbd",
        "forgeeksskeegfor",
    ];

    println!("Mais exemplos:");
    for ex in &exemplos {
        let (pal, pos) = manacher(ex);
        println!("  \"{}\" -> \"{}\" (pos {})", ex, pal, pos);
    }
}

Exemplo de Execucao

Texto: "abaaba"

Maior palindromo: "abaaba" (posicao 0, comprimento 6)

Todos os palindromos (comprimento >= 2):
  posicao 0: "aba"
  posicao 0: "abaaba"
  posicao 2: "aa"
  posicao 3: "aba"

Mais exemplos:
  "racecar" -> "racecar" (pos 0)
  "babad" -> "bab" (pos 0)
  "cbbd" -> "bb" (pos 1)
  "forgeeksskeegfor" -> "geeksskeeg" (pos 3)

Visualizacao dos palindromos em “abaaba”:

a b a a b a
0 1 2 3 4 5

Palindromos encontrados:
  [a b a] . . .       "aba"  (centro=1, raio=1)
  [a b a a b a]       "abaaba" (centro=2.5, raio=3)
  . . [a a] . .       "aa"   (centro=2.5, raio=1)
  . . . [a b a]       "aba"  (centro=4, raio=1)

Variacoes e Otimizacoes

1. Abordagem O(n^2): Expandir ao Redor do Centro (para Comparacao)

/// Encontra o maior palindromo expandindo ao redor de cada centro.
/// Complexidade: O(n^2) no pior caso, mas simples de implementar.
fn maior_palindromo_expansao(texto: &str) -> (String, usize) {
    let bytes = texto.as_bytes();
    let n = bytes.len();

    if n == 0 {
        return (String::new(), 0);
    }

    let mut melhor_inicio = 0;
    let mut melhor_comprimento = 1;

    // Funcao auxiliar para expandir ao redor de um centro
    let expandir = |mut esq: isize, mut dir: usize| -> (usize, usize) {
        while esq >= 0 && dir < n && bytes[esq as usize] == bytes[dir] {
            esq -= 1;
            dir += 1;
        }
        let inicio = (esq + 1) as usize;
        let comprimento = dir - inicio;
        (inicio, comprimento)
    };

    for i in 0..n {
        // Palindromo impar (centro em i)
        let (inicio, comp) = expandir(i as isize, i);
        if comp > melhor_comprimento {
            melhor_inicio = inicio;
            melhor_comprimento = comp;
        }

        // Palindromo par (centro entre i e i+1)
        if i + 1 < n {
            let (inicio, comp) = expandir(i as isize, i + 1);
            if comp > melhor_comprimento {
                melhor_inicio = inicio;
                melhor_comprimento = comp;
            }
        }
    }

    (
        texto[melhor_inicio..melhor_inicio + melhor_comprimento].to_string(),
        melhor_inicio,
    )
}

2. Contar Total de Substrings Palindromicas

/// Conta o numero total de substrings palindromicas (incluindo repeticoes).
fn contar_palindromos(texto: &str) -> usize {
    let bytes = texto.as_bytes();
    let n = bytes.len();

    if n == 0 {
        return 0;
    }

    // Constroi string transformada
    let mut t: Vec<u8> = Vec::with_capacity(2 * n + 1);
    for &b in bytes {
        t.push(b'#');
        t.push(b);
    }
    t.push(b'#');

    let t_len = t.len();
    let mut p = vec![0usize; t_len];
    let mut centro = 0;
    let mut borda_dir = 0;

    for i in 0..t_len {
        if i < borda_dir {
            let espelho = 2 * centro - i;
            p[i] = p[espelho].min(borda_dir - i);
        }

        let mut esq = i as isize - p[i] as isize - 1;
        let mut dir = i + p[i] + 1;

        while esq >= 0 && dir < t_len && t[esq as usize] == t[dir] {
            p[i] += 1;
            esq -= 1;
            dir += 1;
        }

        if i + p[i] > borda_dir {
            centro = i;
            borda_dir = i + p[i];
        }
    }

    // Cada P[i] no indice impar (caractere original) contribui com
    // (P[i] + 1) / 2 palindromos impares.
    // Cada P[i] no indice par (#) contribui com P[i] / 2 palindromos pares.
    let mut total = 0usize;
    for i in 0..t_len {
        if i % 2 == 1 {
            // Indice impar = caractere original
            total += (p[i] + 1) / 2; // inclui o proprio caractere (palindromo de tamanho 1)
        } else {
            // Indice par = separador '#'
            total += p[i] / 2;
        }
    }

    // Subtrai os palindromos de comprimento 1 se nao quiser conta-los
    // total -= n;  // descomente para excluir palindromos de tamanho 1

    total
}

fn main() {
    let texto = "abaab";
    let total = contar_palindromos(texto);
    println!(
        "Numero de substrings palindromicas em \"{}\": {}",
        texto, total
    );
    // Palindromos: a, b, a, a, b, aba, aa, abaab? Nao.
    // a(0), b(1), a(2), a(3), b(4), aba(0-2), aa(2-3), baab? Nao.
    // Total: 7 (incluindo caracteres individuais)
}

3. Palindromos em Posicoes Especificas

/// Para cada posicao i, retorna o comprimento do maior palindromo
/// que TERMINA na posicao i. Util para problemas de DP com palindromos.
fn palindromos_terminando_em(texto: &str) -> Vec<usize> {
    let n = texto.len();
    let (_, _) = manacher(texto);

    let bytes = texto.as_bytes();
    let mut resultado = vec![1usize; n]; // no minimo o caractere sozinho

    for i in 0..n {
        // Expande palindromo impar terminando em i
        for raio in 1..=i {
            if i + 1 > raio * 2 {
                let inicio = i - 2 * raio;
                if bytes[inicio] == bytes[i] {
                    let meio_ok = resultado[i - 1] >= 2 * raio - 1
                        || (inicio + 1 <= i - 1
                            && texto[inicio + 1..i] == texto[inicio + 1..i]);
                    // Simplificacao: verifica diretamente
                    let sub = &bytes[inicio..=i];
                    let eh_palindromo = sub.iter().zip(sub.iter().rev()).all(|(a, b)| a == b);
                    if eh_palindromo {
                        resultado[i] = resultado[i].max(2 * raio + 1);
                    }
                }
            }
            // Para eficiencia, podemos limitar as verificacoes
            if raio > 10 {
                break;
            }
        }
    }

    resultado
}

Aplicacoes Praticas

Verificador de Palindromos em Texto Natural

/// Encontra todas as palavras palindromicas em um texto.
fn encontrar_palindromos_palavras(texto: &str) -> Vec<String> {
    let mut palindromos = Vec::new();

    for palavra in texto.split_whitespace() {
        // Remove pontuacao
        let limpa: String = palavra
            .chars()
            .filter(|c| c.is_alphanumeric())
            .collect();
        let limpa_lower = limpa.to_lowercase();

        if limpa_lower.len() >= 2 {
            let bytes = limpa_lower.as_bytes();
            let eh_palindromo = bytes.iter().zip(bytes.iter().rev()).all(|(a, b)| a == b);
            if eh_palindromo {
                palindromos.push(limpa);
            }
        }
    }

    palindromos
}

/// Particiona uma string no menor numero de substrings palindromicas.
fn particao_palindromica(texto: &str) -> Vec<String> {
    let bytes = texto.as_bytes();
    let n = bytes.len();

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

    // DP: dp[i] = numero minimo de cortes para texto[0..i]
    let mut dp = vec![usize::MAX; n + 1];
    let mut corte = vec![0usize; n + 1]; // onde foi feito o melhor corte
    dp[0] = 0;

    // Pre-computa: eh_palindromo[i][j] para substrings curtas
    let eh_pal = |i: usize, j: usize| -> bool {
        let sub = &bytes[i..=j];
        sub.iter().zip(sub.iter().rev()).all(|(a, b)| a == b)
    };

    for i in 1..=n {
        for j in 0..i {
            if dp[j] != usize::MAX && eh_pal(j, i - 1) {
                if dp[j] + 1 < dp[i] {
                    dp[i] = dp[j] + 1;
                    corte[i] = j;
                }
            }
        }
    }

    // Reconstroi a particao
    let mut resultado = Vec::new();
    let mut pos = n;
    while pos > 0 {
        resultado.push(texto[corte[pos]..pos].to_string());
        pos = corte[pos];
    }
    resultado.reverse();
    resultado
}

fn main() {
    let texto = "abacdca";
    let particao = particao_palindromica(texto);
    println!("Particao palindromica de \"{}\": {:?}", texto, particao);

    let texto2 = "O ovo da aba arara e legal";
    let pals = encontrar_palindromos_palavras(texto2);
    println!("Palavras palindromicas: {:?}", pals);
}

Comparacao com Outros Algoritmos

AlgoritmoTempoEspacoEncontra
Forca BrutaO(n^3)O(1)Maior palindromo
Expandir do CentroO(n^2)O(1)Maior palindromo
DP (tabela)O(n^2)O(n^2)Todos palindromos
ManacherO(n)O(n)Todos palindromos
Eertree/Palindromic TreeO(n)O(n)Palindromos distintos
Suffix Array + LCPO(n*log n)O(n)Maior palindromo

O Manacher e a solucao otima para palindromos em uma string, com O(n) tanto em tempo quanto em espaco. Para palindromos em streams ou textos dinamicos, a Eertree (arvore palindromica) pode ser mais adequada.

Exercicios Praticos

  1. Menor insercoes para palindromo: Dado um texto, encontre o menor numero de caracteres que devem ser inseridos para torna-lo um palindromo. Use Manacher para identificar o maior prefixo/sufixo palindromico.

  2. Palindromos distintos: Conte quantos palindromos distintos existem como substrings de um texto. Combine Manacher com um HashSet.

  3. Particao palindromica otima: Dado um texto, encontre o menor numero de cortes para particiona-lo em substrings que sao todas palindromos. Combine Manacher com programacao dinamica.

  4. Palindromos em duas dimensoes: Dada uma matriz de caracteres, encontre o maior subretangulo cujas linhas E colunas sao todas palindromos. Aplique Manacher em cada dimensao.

Veja Tambem