Programação Dinâmica Subsequência comum mais longa Em inglês, Longest Common Subsequence (LCS) Fernando Lobo Algoritmos e Estrutura de Dados II 1 / 23 Longest Common Subsequence (LCS) Dadas duas sequências, X = x 1 x 2... x m e Y = y 1 y 2... y n, encontrar uma subsequência comum a X e Y que seja o mais longa possível. Exemplo: X = n o c t u r n o Y = m o s q u i t e i r o LCS(X, Y ) = o t r o (também podia ser o u r o) 2 / 23
Algoritmo de força bruta Gerar todas as subsequências de X e verificar se também é subsequência de Y, e ir guardando a subsequência mais longa vista até ao momento. Complexidade? Θ(2 m ) para gerar todas as subsequências de X. Θ(n) para verificar se uma subsequência de X é subsequência de Y. Total: Θ(n 2 m ) É exponencial. Muito mau! 3 / 23 Será que podemos aplicar Programação Dinâmica? Se sim teremos de conseguir definir o problema recursivamente em termos de subproblemas. O n o de subproblemas tem de ser relativamente pequeno (polinomial em n e m) para que a Programação Dinâmica seja útil. Depois de definir o problema em termos de subproblemas, podemos resolver o problema de baixo para cima, começando pelos casos base e armazenando as soluções dos subproblemas. 4 / 23
Subestrutura óptima Vamos olhar para prefixos de X e Y. Seja X i o prefixo dos i primeiros elementos de X. Exemplo: X = n o c t u r n o X 4 = n o c t X0 = X3 = n o c X 8 = n o c t u r n o 5 / 23 Subestrutura óptima Seja X = x 1 x 2... x m e Y = y 1 y 2... y n. Seja Z = z 1 z 2... z k uma LCS entre X e Y. Três casos: 1. Se x m = y n, então z k = x m = y n e Z k 1 é uma LCS entre X m 1 e Y n 1. 2. Se x m y n e z k x m, então Z é uma LCS entre X m 1 e Y n. 3. Se x m y n e z k y n, então Z é uma LCS entre X m e Y n 1. 6 / 23
Demonstração do caso 1 Caso 1: Se x m = y n, então z k = x m = y n e Z k 1 é uma LCS entre X m 1 e Y n 1. Teremos de provar que z k = x m = y n. Suponhamos que tal não é verdade. Então a subsequência Z = z 1 z 2... z k x m é uma subsequência comum a X e Y e tem comprimento k + 1. Contradiz o facto de Z ser uma LCS entre X e Y. 7 / 23 Demonstração do caso 1 (cont.) Agora temos de provar que Z k 1 é uma LCS entre X m 1 e Y n 1. Suponhamos que existe uma subsequência W comum a X m 1 e Y n 1 que é mais longa que Z k 1. comprimento de W k. A subsequência W = W x m é comum a X e Y e tem comprimento k + 1. Contradiz o facto de Z ser uma LCS entre X e Y. 8 / 23
Demonstração dos casos 2 e 3 Caso 2: Se x m y n e z k x m, então Z é uma LCS entre X m 1 e Y n. Suponhamos que existe uma subsequência W comum a X m 1 e Y n com comprimento > k. Então W é uma subsequência comum entre X e Y. = Contradiz o facto de Z ser uma LCS entre X e Y. Caso 3: Se x m y n e z k y n, então Z é uma LCS entre X m e Y n 1. A demonstração do caso 3 é análoga à do caso 2. 9 / 23 Resumindo Podemos definir LCS(X m, Y n ) em termos de subproblemas., se m = 0 ou n = 0 LCS(X m, Y n ) = LCS(X m 1, Y n 1 ) x m, se x m = y n LCS(X m 1, Y n ) ou LCS(X m, Y n 1 ), se x m y n 10 / 23
Comprimento de LCS(X, Y ) Vamos tentar primeiro resolver um problema mais simples: Obter LCS(X, Y ) o comprimento de LCS(X, Y ) Seja c[i, j] = LCS(X i, Y j ) Queremos obter c[m, n] 11 / 23 Definição recursiva de c[i, j] c[i, j] = 0, se i = 0 ou j = 0 c[i 1, j 1] + 1, se i, j > 0 e x i = y j max(c[i 1, j], c[i, j 1]), se i, j > 0 e x i y j 12 / 23
Algortimo recursivo LCS-Length-Rec(X, Y, i, j) if i == 0 or j == 0 return 0 elseif X [i] == Y [j] return LCS-Length-Rec(X, Y, i 1, j 1) + 1 else a = LCS-Length-Rec(X, Y, i 1, j) b = LCS-Length-Rec(X, Y, i, j 1) return max(a, b) Chamada inicial: LCS-Length-Rec(X, Y, m, n) Tal como em Fib-Rec e Comb-Rec, a árvore dá origem a muitos subproblemas repetidos. O algoritmo é exponencial. Mas o número de subproblemas distintos = m n. 13 / 23 Podemos usar Programação Dinâmica LCS-Length-DP(X, Y ) m = X.length n = Y.length for i = 1 to m c[i, 0] = 0 for j = 0 to n c[0, j] = 0 for i = 1 to m for j = 1 to n if X [i] == Y [j] c[i, j] = c[i 1, j 1] + 1 elseif c[i 1, j] c[i, j 1] c[i, j] = c[i 1, j] else c[i, j] = c[i, j 1] return c[m, n] 14 / 23
Demo c[i, j] é preenchida linha a linha, da esquerda para a direita. 15 / 23 Como obter a LCS própriamente dita? O nosso algoritmo apenas obteve o comprimento da LCS. A ideia é alterar o código de LCS-Length-DP e, de cada vez que obtemos um c[i, j], registamos como é que ele foi obtido. Isso permite-nos reconstruir a solução. 16 / 23
Aqui vai o código alterado LCS-Length-DP-v2(X, Y ). for i = 1 to m for j = 1 to n if X [i] == Y [j] c[i, j] = c[i 1, j 1] + 1 b[i, j] = elseif c[i 1, j] c[i, j 1] c[i, j] = c[i 1, j] b[i, j] = else c[i, j] = c[i, j 1] b[i, j] = return c[m, n], b 17 / 23 Demo As setas, e são armazenadas em b[i, j]. b[i, j] indica o subproblema escolhido para obter c[i, j]. 18 / 23
Uma vez tendo a informação em b, podemos obter uma LCS entre X e Y. A chamada inicial é Print-LCS(b, X, m, n) Print-LCS(b, X, i, j) if i == 0 or j == 0 return // Não faz nada if b[i, j] == Print-LCS(b, X, i 1, j 1) print X [i] elseif b[i, j] == Print-LCS(b, X, i 1, j) else Print-LCS(b, X, i, j 1) 19 / 23 Complexidade A complexidade é Θ(m n) A Programação Dinâmica reduziu a complexidade de exponencial para polinomial. No livro têm mais exemplos de problemas resolvidos com Programação Dinâmica. 20 / 23
Versão memoized de LCS-Length LCS-Length-Memoized(X, Y ) m = X.length n = Y.length for i = 0 to m for j = 0 to n c[i, j] = unknown return M-LCS-Length(X, Y, m, n) 21 / 23 M-LCS-Length(X, Y, i, j) if c[i, j] == unknown if i == 0 or j == 0 c[i, j] = 0 elseif X [i] == Y [j] c[i, j] = M-LCS-Length(X, Y, i 1, j 1) + 1 else a = M-LCS-Length(X, Y, i 1, j) b = M-LCS-Length(X, Y, i, j 1) c[i, j] = max(a, b) return c[i, j] 22 / 23
Como aplicar a Programação Dinâmica? Para aplicarmos Programação Dinâmica ou Memoization para resolver um problema, temos de fazer 4 coisas: 1. Caracterizar a estrutura de uma solução óptima. 2. Definir o valor da solução óptima recursivamente em termos de subsoluções óptimas. 3. Calcular o valor de uma solução óptima de baixo para cima (no caso de P.D.) ou de cima para baixo (no caso de Memoization). 4. Obter a solução óptima através da informação calculada e armazenada no passo 3. 23 / 23