SubsequĂȘncia comum mais longa: Python, C++ Exemplo
⥠Resumo Inteligente
A SubsequĂȘncia Comum Mais Longa identifica o padrĂŁo de elementos ordenados mais longo compartilhado por duas strings sem exigir caracteres contĂguos. Este clĂĄssico da programação dinĂąmica Ă© a base de utilitĂĄrios de comparação de arquivos (diff), alinhamento de DNA e controle de versĂŁo, comparando sequĂȘncias de forma eficiente em tempo polinomial.
Qual Ă© a subsequĂȘncia comum mais longa?
A Maior SubsequĂȘncia Comum (LCS, na sigla em inglĂȘs) consiste em encontrar a maior subsequĂȘncia de elementos presentes em ambas as strings, padrĂ”es ou sequĂȘncias de objetos, na mesma ordem em que aparecem em cada uma delas.
Exemplo
Por exemplo, sĂŁo fornecidas duas strings. Suponhamos que:
PadrĂŁo_1 = âRGBGARGAâ
PadrĂŁo_2 = âBGRARGâ
- A partir do padrĂŁo 1, podem ser produzidas sequĂȘncias como âRGBâ, âRGGAâ, âRGARâ. Para criar uma sequĂȘncia, Ă© necessĂĄrio manter a posição relativa de cada caractere na string.
- A partir do padrĂŁo 2, podemos produzir sequĂȘncias como âBGRâ, âBRAGâ, âRARGâ. SequĂȘncias podem ser produzidas desde que mantenham a posição relativa da string original.
O termo posição relativa significa ordem.
Por exemplo, âBRGâ Ă© uma sequĂȘncia vĂĄlida porque âBâ aparece primeiro, depois âRâ e, por fim, âGâ na string original pattern_2. No entanto, se a sequĂȘncia for âRBRGâ, ela nĂŁo Ă© vĂĄlida, porque na string original (pattern_2), âBâ vem primeiro.
Temos duas opçÔes para encontrar a maior subsequĂȘncia comum das duas sequĂȘncias ou matrizes fornecidas.
- MĂ©todo ingĂȘnuo
- Solução de programação dinĂąmica: a maior subsequĂȘncia comum tambĂ©m Ă© conhecida como LCS.
Uma solução ingĂȘnua apresenta maior complexidade temporal e nĂŁo Ă© a solução Ăłtima. Utilizando a Programação DinĂąmica (PD), superamos o problema da complexidade.
MĂ©todo IngĂȘnuo
O mĂ©todo ingĂȘnuo Ă© uma abordagem simples para o problema, independentemente da complexidade temporal e de outros fatores de otimização. Consiste em "força bruta", mĂșltiplos loops e chamadas recursivas na maioria dos casos. O termo força bruta significa percorrer todos os padrĂ”es possĂveis para um determinado problema.
Exemplo
A partir do exemplo acima de padrĂŁo1 e padrĂŁo2, vamos supor que o padrĂŁo1 tenha um comprimento m e o padrĂŁo2 tenha um comprimento n. Para verificar todos os casos possĂveis, precisamos avaliar todas as subsequĂȘncias possĂveis do padrĂŁo1 com o padrĂŁo2.
Aqui estĂĄ uma sequĂȘncia simples de 4 letras: âABCDâ. Por exemplo, precisamos criar uma sequĂȘncia a partir de âABCDâ. Podemos usar um caractere ou nĂŁo. Isso significa que, para cada caractere, temos duas opçÔes:
- O personagem serĂĄ adicionado Ă subsequĂȘncia.
- O caractere nĂŁo serĂĄ adicionado Ă subsequĂȘncia.
Aqui, as imagens mostram todas as sequĂȘncias que podemos fazer a partir da string âABCDâ.
SequĂȘncia com 1 caractere:
SequĂȘncias com 2 caracteres:
SequĂȘncias com 3 caracteres:
A partir do diagrama acima, existem 14 sequĂȘncias. Se nĂŁo considerarmos nenhuma letra, ou seja, uma sequĂȘncia vazia, o total de sequĂȘncias serĂĄ 15. AlĂ©m disso, a prĂłpria sequĂȘncia âABCDâ Ă© uma sequĂȘncia. Portanto, o total de sequĂȘncias Ă© 16.
Portanto, Ă© possĂvel gerar 2^4 ou 16 subsequĂȘncias a partir de âABCDâ. Em seguida, uma string com comprimento de m terĂĄ uma subsequĂȘncia total de 2^m.
Para cada subsequĂȘncia, precisamos verificar todo o padrĂŁo2. Isso levarĂĄ um tempo O(n). O(n) significa a função de complexidade que calcula o tempo necessĂĄrio para a execução.
EntĂŁo, a complexidade total do tempo torna-se O(n*2^m). No exemplo que vimos acima, o valor de m=8 e n=5.
Aqui estĂŁo as etapas do MĂ©todo IngĂȘnuo:
Passo 1) Pegue uma sequĂȘncia do padrĂŁo 1.
Passo 2) Combine a sequĂȘncia da etapa 1 com o padrĂŁo 2.
Passo 3) Se corresponder, salve a subsequĂȘncia.
Passo 4) Se ainda houver sequĂȘncias restantes no padrĂŁo 1, volte ao passo 1.
Passo 5) Imprima a subsequĂȘncia mais longa.
Subestrutura Ătima
O termo subestrutura ótima significa que uma solução ótima pode ser encontrada resolvendo os subproblemas. Por exemplo, no exemplo acima, temos o padrão 1 e o padrão 2.
Passo 1) Selecione os dois primeiros caracteres de cada padrĂŁo.
Passo 2) Pegue o terceiro ao quinto caracteres de cada padrĂŁo.
Passo 3) Continue da mesma forma com os caracteres restantes.
Estrutura recursiva do problema LCS
Encontramos a LCS (menor cadeia de caracteres) na subcadeia (uma cadeia gerada a partir de uma cadeia original). Em seguida, armazenamos o comprimento da LCS das subcadeias.
Agora, aqui estĂĄ outra propriedade interessante que Ă© sobreposiçãoping subproblemasDiz-se que um problema tem sobreposição.ping subproblemas se o enunciado do problema puder ser dividido em pequenos subproblemas e usados ââvĂĄrias vezes no programa.
O diagrama abaixo mostra que o algoritmo recursivo chamou a função com o mesmo parùmetro vårias vezes.
Por exemplo, observe a ĂĄrvore de recursĂŁo. Na caixa escura, vocĂȘ pode notar sobreposiçÔes.ping subproblemas. (âRGâ, âRAâ), (âRGâ, âRâ), e outros sĂŁo chamados vĂĄrias vezes.
Para otimizar isso, adotamos a seguinte abordagem: Programaçao dinamica (DP).
MĂ©todo recursivo da maior subsequĂȘncia comum
O gråfico acima representa o método recursivo. Cada função recursiva possui um caso base para interromper a recursão ou iniciar o retorno a partir de sua pilha.
Para esta implementação, usaremos um caso base. Portanto, o algoritmo é como o seguinte:
- Se todos os elementos anteriores ao Ășltimo elemento tiverem uma correspondĂȘncia, aumente o comprimento em um e retorne.
- Passe dois padrÔes para a função e pegue o valor måximo do retorno.
- Se um padrĂŁo tiver comprimento zero, nĂŁo teremos subsequĂȘncia para comparar. Retorne 0 neste caso. Este Ă© o caso base da recursĂŁo.
Apelido Code:
def lcs: input: pattern_1, pattern_2, len_1, len_2 if len_1 or len_2 is zero: return 0 if pattern_1[len_1 - 1] equals pattern_2[len_2 - 1]: return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1) else: return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2), lcs(pattern_1, pattern_2, len_1, len_2 - 1))
Implementação em C++
#include<iostream> #include<bits/stdc++.h> using namespace std; int lcs(string pattern_1, string pattern_2, int len_1, int len_2) { if (len_1 == 0 || len_2 == 0) return 0; if (pattern_1[len_1 - 1] == pattern_2[len_2 - 1]) { return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1); } else { return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2), lcs(pattern_1, pattern_2, len_1, len_2 - 1)); } } int main() { string pattern_1, pattern_2; pattern_1 = "RGBGARGA"; pattern_2 = "BGRARG"; cout<<"Length of LCS is: "<<lcs(pattern_1, pattern_2, pattern_1.size(), pattern_2.size())<<endl; }
SaĂda:
Length of LCS is: 5
Implementação em Python
def lcs(pattern_1, pattern_2, len_1, len_2): if len_1 == 0 or len_2 == 0: return 0 if pattern_1[len_1 - 1] == pattern_2[len_2 - 1]: return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1) else: return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2), lcs(pattern_1, pattern_2, len_1, len_2 - 1)) pattern_1 = "RGBGARGA" pattern_2 = "BGRARG" print("Length of LCS is: ", lcs(pattern_1, pattern_2, len(pattern_1), len(pattern_2)))
SaĂda:
Length of LCS is: 5
MĂ©todo de Programação DinĂąmica da Maior SubsequĂȘncia Comum (LCS)
Programação dinĂąmica significa otimizar o mĂ©todo recursivo simples. Por exemplo, se observarmos o grĂĄfico da abordagem recursiva ou ingĂȘnua, podemos ver que existem vĂĄrias chamadas de função idĂȘnticas. O mĂ©todo de Programação DinĂąmica registra todos os cĂĄlculos em um vetor e os reutiliza quando necessĂĄrio.
Usaremos uma matriz 2D com dimensÔes m x n, onde m e n são os comprimentos de pattern1 e pattern2. Para um Matriz 2D, podemos usar estruturas de dados de lista em Python ou estruturas de dados vetoriais/de matriz em C++.
Apelido Code Para LCS usando DP:
LCS(pattern_1, pattern_2): m = length of pattern_1 + 1 n = length of pattern_2 + 1 dp[n][m] for i in range 0 to n + 1: for j in range 0 to m + 1: if i or j equals to 0: dp[i][j] = 0 else if pattern_1[i] == pattern_2[j]: dp[i][j] = dp[i - 1][j - 1] + 1 else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) return dp[n][m]
Aqui estå a tabela LCS, que é usada como uma estrutura de dados de matriz 2D para a abordagem de programação dinùmica.
Vamos discutir a lĂłgica que usamos aqui. Os passos sĂŁo:
Passo 1) Se i ou j for zero, estamos selecionando uma string vazia dentre as duas strings dadas e tentando encontrar as subsequĂȘncias comuns. No entanto, como a substring que estamos selecionando estĂĄ vazia, o comprimento da subsequĂȘncia Ă© 0.
Passo 2) Se dois caracteres coincidirem, atribuiremos o valor ao Ăndice (i,j) incrementando o LCS calculado anteriormente, que estĂĄ presente no Ăndice (i-1,j-1) (da linha anterior).
Passo 3) Caso nĂŁo haja correspondĂȘncia, utilizaremos o menor mĂșltiplo comum (MMC) dos dois Ăndices adjacentes. Dessa forma, precisamos preencher todos os valores na matriz bidimensional.
Passo 4) Por fim, retornaremos o valor da Ășltima cĂ©lula do array 2D.
Basicamente, todos os valores na matriz 2D contĂȘm o comprimento de subsequĂȘncias comuns. Dentre elas, a Ășltima cĂ©lula contĂ©m o comprimento da subsequĂȘncia comum mais longa.
Implementação em C++
#include<iostream> using namespace std; int lcs(string pattern_1, string pattern_2) { int m = pattern_1.size(); int n = pattern_2.size(); // dp will store solutions as the iteration goes on int dp[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) { for (int j = 0; j < m + 1; j++) { if (i == 0 || j == 0) { dp[i][j] = 0; } else if (pattern_2[i - 1] == pattern_1[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); } } } return dp[n][m]; } int main() { string pattern_1 = "RGBGARGA"; string pattern_2 = "BGRARG"; cout<<"Length of LCS: "<<lcs(pattern_1, pattern_2)<<endl; }
SaĂda:
Length of LCS: 5
Implementação em Python
def lcs(pattern_1, pattern_2): m = len(pattern_1) n = len(pattern_2) # dp will store solutions as the iteration goes on dp = [[None] * (n + 1) for item in range(m + 1)] for i in range(m + 1): for j in range(n + 1): if i == 0 or j == 0: dp[i][j] = 0 elif pattern_1[i - 1] == pattern_2[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) return dp[m][n] pattern_1 = "RGBGARGA" pattern_2 = "BGRARG" print("Length of LCS: ", lcs(pattern_1, pattern_2))
SaĂda:
Length of LCS: 5
Portanto, ambas as strings tĂȘm a subsequĂȘncia comum mais longa de comprimento 5.
Em resumo, no mĂ©todo de programação dinĂąmica, calculamos cada tarefa apenas uma vez. No mĂ©todo recursivo, poderĂamos ter sobreposição de tarefas.ping subproblemas.
Neste algoritmo de programação dinùmica, estamos usando uma matriz 2D. Serão fornecidas duas strings (suponha que ambas tenham comprimento n). Então o espaço necessårio no array é nx n. Se as strings forem grandes o suficiente, precisaremos de uma versão da solução DP com otimização de memória.
A lĂłgica simplificada que foi usada no cĂłdigo Ă©:
- Declare uma matriz 2D DP[m][n].
- Preencha a primeira linha e a primeira coluna da matriz DP com 0.
- Pegue i e j para a iteração.
- Se pattern1[i] for igual a pattern2[j], entĂŁo atualize DP[i][j] = DP[i-1][j-1] + 1.
- Se pattern1[i] nĂŁo for igual a pattern2[j], entĂŁo DP[i][j] serĂĄ o valor mĂĄximo entre DP[i-1][j] e DP[i][j-1].
- Continue até que i e j alcancem m e n.
- O Ășltimo elemento, DP[m-1][n-1], conterĂĄ o comprimento.
Aqui, Ă© referido como DP[m-1][n-1] porque o Ăndice da matriz começa em 0.









