Processamento de Strings
A resolução de problemas envolvendo cadeia de caracteres (ou simplesmente strings) se tornou um importante domínio no estudo de algoritmos. Estes problemas ocorrem, principalmente, no processamento de texto, como verificação ortográfica e busca de substrings específicos ou de padrões mais gerais (pattern matching). Por exemplo, o padrão ABC
ocorre duas vezes na string ABABCBABC
.
Nesta seção, diferente das outras, por simplicidade alguns algoritmos serão implementados (também) em Python.
Leituras fortemente recomendadas:
- Strings
- Capítulo 14 do livro "Principles of Algorithmic Problem Solving"
- Capítulo 6 do livro "Competitive Programming"
- Capítulo 26 do livro "Competitive Programmer’s Handbook"
Terminologia
-
Uma substring é uma sequência de caracteres consecutivos em uma string. Usa-se a notação
s[a...b]
para se referir a uma substring des
que começa na posiçãoa
e termina na posiçãob
. Uma string de tamanho \(n\) tem \(n ( n + 1)/2\) substrings. Por exemplo, as substrings deABCD
sãoA
,B
,C
,D
,AB
,BC
,CD
,ABC
,BCD
eABCD
. -
Uma subsequência é uma sequência de caracteres (não necessariamente consecutivos) em uma string em sua ordem original. Uma string de tamanho \(n\) tem \(2^n − 1\) subsequências. Por exemplo, as subsequências de
ABCD
sãoA
,B
,C
,D
,AB
,AC
,AD
,BC
,BD
,CD
,ABC
,ABD
,ACD
,BCD
eABCD
. -
Um prefixo é uma substring que começa no início de uma string e um sufixo é uma substring que termina no final de uma string. Por exemplo, os prefixos de
ABCD
sãoA
,AB
,ABC
eABCD
, e os sufixos deABCD
sãoD
,CD
,BCD
eABCD
. -
Uma string \(w\) é um anagrama de uma string \(v\) se existir uma permutação das caracteres que transformam \(w\) em \(v\). Por exemplo, os anagramas de
alegria
sãoalergia
,alegrai
,regalia
,galeria
, etc. O código abaixo ilustra uma forma de gerar todos os anagramas de uma lista de palavras.
Expressão Regular
Alguns problemas de processamento de string são facilmente resolvidos usando expressão regular (Regex). Tanto C++ quanto Python possuem bibliotecas para lidar com expressões regulares.
Material complementar:
- Regular Expression Basics in C++
- Regular Expressions: Regexes in Python (Part 1)
- Regular Expressions: Regexes in Python (Part 2)
- Expressões Regulares
Processamento de String com Programação Dinâmica
Dois problemas clássicos de processamento de string que usam programação dinâmica são alinhamento de string (distância de edição) e maior subsequência comum.
Distância de Edição
O problema Distância de Edição (também conhecido como Alinhamento de String ou Levenshtein Distance) é definido da seguinte forma: Dadas duas strings \(x\) e \(y\), deseja-se saber o mínimo de operações (inserção, exclusão ou substituição) que são necessárias para transformar \(x\) em \(y\). Por exemplo, a distância de edição entre as palavras inglesas kitten
e sitting
é 3, já que com apenas 3 edições conseguimos transformar uma palavra na outra e não há maneira de o fazer com menos de três edições:
- kitten
- sitten (substituição de 'k' por 's')
- sittin (substituição de 'e' por 'i')
- sitting (inserção de 'g' no final)
Uma primeira estratégia (ingênua) é tentar, recursivamente, todas as possibilidades (inserir, remover, substituir):
Um código que implementa esta relação de recorrência terá complexidade \(O(3^m)\). O pior caso acontece quando nenhum dos caracteres de duas strings são iguais. Neste caso, tem-se varias sobreposições de subproblemas. Assim, usar programação dinâmica é o melhor caminho. O código abaixo (extraida daqui) mostra uma implementação do algoritmo de distância de edição usando programação dinâmica. A complexidade do código é \(O(nm)\).
Os links abaixo são fundamentais para entender melhor como o algoritmo funciona.
Maior Subsequência Comum
O problema da Maior Subsequência Comum (Longest Common Subsequence - LCS) é definido da seguinte forma: Dadas duas strings \(A\) e \(B\), qual é a subsequência comum mais longa entre elas. Lembrando que uma subsequência é uma sequência que aparece na mesma ordem relativa, mas não necessariamente consecutiva. Por exemplo, A = "ACAATCC"
e B = "AGCATGC"
têm LCS de comprimento 5, ou seja, "ACATC"
.
O código abaixo (extraido daqui) mostra uma implementação do algoritmo para encontrar o tamanho da maior subsequência comum usando programação dinâmica. A complexidade do código é \(O(nm)\).
Os links abaixo são fundamentais para entender melhor como o algoritmo funciona.
Busca em String (String Matching)
Busca em string (também conhecido como string matching) é um problema de encontrar o indice (ou indices) de uma dada (sub)string (chamada de padrão P
) de uma string (chamada de texto T
). Por exemplo, considerando que T = "STEVEN EVENT"
. Se P = "EVE"
, então as respostas são os indices 2 e 7. Se P = "EVENT"
, então a resposta é apenas o índice 7. Se P = "EVENING"
, então o algoritmo deve retornar que não encontrou o padrão P
no texto T
.
Um algoritmo ingênuo (veja abaixo) consegue resolver esse problema com complexidade \(O(nm)\), onde \(n\) é o tamanho do texto T
e \(m\) o tamanho de padrão P
.
Com o intuito de melhorar a complexidade do algoritmo anterior, Knuth, Morris e Pratt propuseram um algoritmo mais eficiente (chamado de KMP Algorithm) que faz uso das informações obtidas nas comparações dos caracteres anteriores, especialmente aqueles que são iguais. Este link mostra uma animação do funcionamento do Algoritmo KMP. Você pode encontrar a implementação e a explicação detalhada do algoritmo KMP aqui, aqui e aqui.
Os links abaixo detalham mais sobre busca em string e explicam outros algoritmos não descritos aqui.
- ⭐️ KMP Algorithm for Pattern Searching (GeeksforGeeks)
- ⭐️ Strings
- ⭐️ Applications of String Matching Algorithms (GeeksforGeeks)
- ⭐️ String Matching
- ⭐️ Algoritmo KMP para busca de substring
- Aho-Corasick Algorithm for Pattern Searching (GeeksforGeeks)
- Rabin-Karp Algorithm for Pattern Searching (GeeksforGeeks)
- Prefix function. Knuth–Morris–Pratt algorithm
- String-searching algorithm
- Trie (GeeksforGeeks)
String Hashing
Em construção...
Array de Sufixo
Basicamente, array de sufixo (Suffix Array) é um array que contém inteiros indicando os índices iniciais dos sufixos de uma determinada string, após os sufixos serem ordenados. Lembrando que o \(i\)-ésimo sufixo de uma string \(s\) de tamanho \(n\) é a substring \(s[i…n−1]\). Por exemplo, os sufixos de S = "banana"
são:
Após de ordenar essas strings, tem-se:
Portanto, o array de sufixos de \(S\) será \([5, 3, 1, 0, 4, 2]\).
Um método simples para construir um array de sufixos é fazer um array de todos os sufixos e então ordenar o array. O código abaixo ilustra este procedimento.
A ordenação possui complexidade \(O(n \log n)\) e como comparar duas strings é \(O(n)\), a complexidade final do algoritmo será \(O(n^2 \log n)\). Pode-se melhor esta complexidade alterando o método de ordenação (usando Radix Sort, por exemplo). Essa versão otimizada terá complexidade \(O(n \log n)\). Você pode encontrar a explicação e implementação aqui, aqui e aqui.
Os links abaixo exemplificam várias aplicações do array de sufixo:
- Applications of Suffix Array
- Suffix Array - Applications
- Suffix arrays – Applications in contest problems
Material Complementar:
- ⭐️ Suffix Array 🤯
- ⭐️ Array de Sufixos
- ⭐️ Suffix Array
- Suffix Array
- Suffix Array (TopCoder)
- Suffix arrays - a programming contest approach
Palindromo
Palíndromos são palavras ou frases que podem ser lidas da esquerda para a direita ou da direita para a esquerda. Por exemplo, ovo
, radar
, 123321
, a base do teto desaba
, Socorram-me, subi no ônibus em Marrocos
, anotaram a data da maratona
.
Uma variante comum de problemas de palíndromos envolve contar o número de substrings de uma string \(s\) que são palíndromos. Uma solução ingênua (mostrada abaixo) faz uma busca completa com complexidade \(O(n^3)\).
Perceba que muitos subproblemas (substrings) serão sobrepostos. Assim, pode-se criar uma matriz para armazenar o substring \(s[l..r]\) de forma que este substring só seja computado uma vez. Desta forma, teremos uma solução que usa programação dinâmica com complexidade \(O(n^2)\). Os códigos abaixo (extraido daqui) ilustram esta estratégia.
Outro problema comum é encontrar o maior palindromo que pode ser gerado deletando zero ou mais caracteres de uma string. Por exemplo: "ADAM" -> "ADA"
(tamanho 3, deletando "M"
), "RACEF1CARFAST" -> "RACECAR"
(tamanho 7, deletando "F1"
e "FAST"
). Novamente, a solução desse problema pode ser obtida usando programação dinâmica:
- Seja \(len(l, r)\) o tamanho do maior palindromo da string \(S[l..r]\).
- Casos bases:
- Se \(l = r\), então \(len(l, r) = 1\);
- Se \(l + 1 = r\), então \(len(l, r) = 2\) se \(S[l] = S[r]\) ou 1, caso contrário.
- Recorrencia:
- Se \(S[l] = S[r]\), então \(len(l, r) = 2 + len(l + 1, r - 1)\);
- Senão, \(len(l, r) = \max(len(l, r - 1), len(l + 1, r))\)
Material Complementar: