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.

  • 📘 Conceito Central: A função `Longest Common Subsequence` retorna o conjunto ordenado mais longo de caracteres que aparece em ambas as strings de entrada, preservando sua ordem relativa original.
  • ???? Abordagem ingĂȘnua: A força bruta enumera todas as subsequĂȘncias da primeira string e as verifica em relação Ă  segunda, executando em tempo exponencial O(n·2^m).
  • 🔁 MĂ©todo recursivo: Uma regra recursiva corresponde aos Ășltimos caracteres ou recorre a substrings menores, mas recalcula a sobreposição.ping subproblemas repetidamente.
  • 🧼 Programaçao dinamica: Uma tabela dp bidimensional armazena em cache os resultados dos subproblemas, produzindo uma solução limpa de O(m·n) com espaço auxiliar de O(m·n).
  • 🐍 Cobertura de idiomas: Automação Python e C++ As implementaçÔes demonstram tanto a linha de base recursiva quanto a tabela dp memorizada para uso prĂĄtico.
  • 🌐 AplicaçÔes reais: A ferramenta Longest Common Subsequence (LSS) oferece suporte a ferramentas de comparação de arquivos (diff), verificadores de plĂĄgio, corretores ortogrĂĄficos e alinhamento de sequĂȘncias bioinformĂĄticas em DNA e proteĂ­nas.

SubsequĂȘncia Comum Mais Longa

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.

Exemplos de strings da SubsequĂȘncia Comum Mais Longa

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ĂȘncias do mĂ©todo ingĂȘnuo de ABCD

SequĂȘncia com 1 caractere:

SequĂȘncias de caracteres Ășnicos do MĂ©todo IngĂȘnuo

SequĂȘncias com 2 caracteres:

MĂ©todo IngĂȘnuo: duas sequĂȘncias de caracteres

SequĂȘncias com 3 caracteres:

MĂ©todo IngĂȘnuo: sequĂȘncias de trĂȘs 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

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.

Sobreposição ideal de subestruturasping subproblemas

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.

Método de Programação Dinùmica da tabela LCS 2D

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.

Perguntas Frequentes

Os fluxos de trabalho de aprendizado de mĂĄquina usam LCS como um recurso de similaridade na classificação de texto, avaliação de sequĂȘncia para sequĂȘncia e detectores de plĂĄgio de cĂłdigo. Ele tambĂ©m estĂĄ na base de mĂ©tricas do tipo BLEU e ROUGE, que avaliam o texto gerado em comparação com saĂ­das de referĂȘncia.

Sim. Assistentes de codificação de IA como o GitHub Copilot e o GPT podem produzir as versĂ”es de programação recursiva e dinĂąmica do LCS em Python, C++, ou JavaEles tambĂ©m podem adicionar memoização, imprimir a subsequĂȘncia real ou converter o cĂłdigo para um formato iterativo sob demanda.

Uma subcadeia de caracteres deve ser contĂ­gua, enquanto uma subsequĂȘncia precisa apenas preservar a ordem. Para “ABCDE”, “ACD” Ă© uma subsequĂȘncia vĂĄlida, mas nĂŁo uma subcadeia de caracteres, enquanto “BCD” Ă© tanto uma subcadeia de caracteres quanto uma subsequĂȘncia.

A versĂŁo de programação dinĂąmica tem complexidade de tempo e espaço O(m·n), onde m e n sĂŁo os comprimentos das duas sequĂȘncias de entrada. A versĂŁo recursiva simples tem complexidade exponencial O(2^(m+n)) no pior caso.

O LCS oferece suporte a utilitĂĄrios de comparação de arquivos, fusĂ”es do Git, alinhamento de sequĂȘncias de DNA e proteĂ­nas em bioinformĂĄtica, detecção de plĂĄgio, corretores ortogrĂĄficos e ferramentas de sincronização de dados que devem preservar a ordem compartilhada dos registros.

A tabela padrĂŁo requer espaço O(m·n). Uma otimização de duas linhas contĂ­nuas reduz o espaço para O(min(m, n)) quando vocĂȘ precisa apenas do comprimento, embora a reconstrução da subsequĂȘncia real ainda exija a tabela completa.

Sim, a recursão pura funciona para strings curtas, mas recalcula os mesmos subproblemas muitas vezes e torna-se impraticåvel após 20 a 25 caracteres. Adicionar memoização ou a tabela de programação dinùmica resolve o problema. tracdesempenho da mesa.

Sim. A ideia de programação dinĂąmica (DP) se estende a k sequĂȘncias usando uma tabela k-dimensional com complexidade de tempo e espaço O(n^k). Essa variante aparece em ferramentas de comparação de mĂșltiplos arquivos e alinhamento mĂșltiplo de sequĂȘncias em bioinformĂĄtica.

Resuma esta postagem com: