|
| 1 | +package com.williamfiset.algorithms.dp; |
| 2 | + |
1 | 3 | /** |
2 | | - * This file contains an implementation of finding the Longest Common Subsequence (LCS) between two |
3 | | - * strings using dynamic programming. |
| 4 | + * Longest Common Subsequence (LCS) |
| 5 | + * |
| 6 | + * Given two strings A and B, find the longest subsequence present in both. |
| 7 | + * A subsequence is a sequence that appears in the same relative order but |
| 8 | + * not necessarily contiguously (unlike a substring). |
| 9 | + * |
| 10 | + * Builds an (n+1) x (m+1) DP table where dp[i][j] = length of the LCS of |
| 11 | + * A[0..i-1] and B[0..j-1], then backtracks to recover one LCS string. |
| 12 | + * |
| 13 | + * Tested against: https://leetcode.com/problems/longest-common-subsequence |
4 | 14 | * |
5 | | - * <p>Time Complexity: O(nm) |
| 15 | + * Time: O(n*m) |
| 16 | + * Space: O(n*m) |
6 | 17 | * |
7 | 18 | * @author William Fiset, william.alexandre.fiset@gmail.com |
8 | 19 | */ |
9 | | -package com.williamfiset.algorithms.dp; |
10 | | - |
11 | 20 | public class LongestCommonSubsequence { |
12 | 21 |
|
13 | | - // Returns a non unique Longest Common Subsequence |
14 | | - // between the strings str1 and str2 in O(nm) |
15 | | - public static String lcs(char[] A, char[] B) { |
| 22 | + /** |
| 23 | + * Finds one Longest Common Subsequence between A and B. |
| 24 | + * |
| 25 | + * @param A - first string |
| 26 | + * @param B - second string |
| 27 | + * @return one LCS string, or null if either input is null |
| 28 | + */ |
| 29 | + public static String lcs(String A, String B) { |
| 30 | + if (A == null || B == null) return null; |
| 31 | + return lcs(A.toCharArray(), B.toCharArray()); |
| 32 | + } |
16 | 33 |
|
| 34 | + /** |
| 35 | + * Finds one Longest Common Subsequence between A and B using bottom-up DP. |
| 36 | + * |
| 37 | + * Builds a table dp[i][j] = length of LCS of A[0..i-1] and B[0..j-1], |
| 38 | + * then backtracks through the table to reconstruct the actual subsequence. |
| 39 | + * |
| 40 | + * @param A - first character array |
| 41 | + * @param B - second character array |
| 42 | + * @return one LCS string, or null if either input is null |
| 43 | + * |
| 44 | + * Time: O(n*m) |
| 45 | + * Space: O(n*m) |
| 46 | + */ |
| 47 | + public static String lcs(char[] A, char[] B) { |
17 | 48 | if (A == null || B == null) return null; |
18 | 49 |
|
19 | 50 | final int n = A.length; |
20 | 51 | final int m = B.length; |
21 | 52 |
|
22 | | - if (n == 0 || m == 0) return null; |
| 53 | + if (n == 0 || m == 0) return ""; |
23 | 54 |
|
24 | 55 | int[][] dp = new int[n + 1][m + 1]; |
25 | 56 |
|
26 | | - // Suppose A = a1a2..an-1an and B = b1b2..bn-1bn |
| 57 | + // Fill the DP table |
27 | 58 | for (int i = 1; i <= n; i++) { |
28 | 59 | for (int j = 1; j <= m; j++) { |
29 | | - |
30 | | - // If ends match the LCS(a1a2..an-1an, b1b2..bn-1bn) = LCS(a1a2..an-1, b1b2..bn-1) + 1 |
31 | | - if (A[i - 1] == B[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; |
32 | | - |
33 | | - // If the ends do not match the LCS of a1a2..an-1an and b1b2..bn-1bn is |
34 | | - // max( LCS(a1a2..an-1, b1b2..bn-1bn), LCS(a1a2..an-1an, b1b2..bn-1) ) |
35 | | - else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); |
| 60 | + // If characters match, extend the LCS from the diagonal |
| 61 | + if (A[i - 1] == B[j - 1]) |
| 62 | + dp[i][j] = dp[i - 1][j - 1] + 1; |
| 63 | + // Otherwise take the best LCS excluding one character from either string |
| 64 | + else |
| 65 | + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); |
36 | 66 | } |
37 | 67 | } |
38 | 68 |
|
39 | | - int lcsLen = dp[n][m]; |
40 | | - char[] lcs = new char[lcsLen]; |
41 | | - int index = 0; |
42 | | - |
43 | | - // Backtrack to find a LCS. We search for the cells |
44 | | - // where we included an element which are those with |
45 | | - // dp[i][j] != dp[i-1][j] and dp[i][j] != dp[i][j-1]) |
| 69 | + // Backtrack from dp[n][m] to reconstruct the LCS string. |
| 70 | + // At each cell, if the characters match, that character is part of |
| 71 | + // the LCS — take it and move diagonally. Otherwise, move toward |
| 72 | + // the neighbor with the larger value (up or left) to stay on the |
| 73 | + // path that produced the optimal length. |
| 74 | + StringBuilder sb = new StringBuilder(); |
46 | 75 | int i = n, j = m; |
47 | | - while (i >= 1 && j >= 1) { |
48 | | - |
49 | | - int v = dp[i][j]; |
50 | 76 |
|
51 | | - // The order of these may output different LCSs |
52 | | - while (i > 1 && dp[i - 1][j] == v) i--; |
53 | | - while (j > 1 && dp[i][j - 1] == v) j--; |
54 | | - |
55 | | - // Make sure there is a match before adding |
56 | | - if (v > 0) lcs[lcsLen - index++ - 1] = A[i - 1]; // or B[j-1]; |
57 | | - |
58 | | - i--; |
59 | | - j--; |
| 77 | + while (i > 0 && j > 0) { |
| 78 | + if (A[i - 1] == B[j - 1]) { |
| 79 | + sb.append(A[i - 1]); |
| 80 | + i--; |
| 81 | + j--; |
| 82 | + } else if (dp[i - 1][j] >= dp[i][j - 1]) { |
| 83 | + i--; |
| 84 | + } else { |
| 85 | + j--; |
| 86 | + } |
60 | 87 | } |
61 | 88 |
|
62 | | - return new String(lcs, 0, lcsLen); |
| 89 | + return sb.reverse().toString(); |
63 | 90 | } |
64 | 91 |
|
65 | | - public static void main(String[] args) { |
| 92 | + // ==================== Main ==================== |
66 | 93 |
|
67 | | - char[] A = {'A', 'X', 'B', 'C', 'Y'}; |
68 | | - char[] B = {'Z', 'A', 'Y', 'W', 'B', 'C'}; |
69 | | - System.out.println(lcs(A, B)); // ABC |
| 94 | + public static void main(String[] args) { |
| 95 | + // LCS: ABC |
| 96 | + System.out.println("LCS: " + lcs("AXBCY", "ZAYWBC")); |
70 | 97 |
|
71 | | - A = new char[] {'3', '9', '8', '3', '9', '7', '9', '7', '0'}; |
72 | | - B = new char[] {'3', '3', '9', '9', '9', '1', '7', '2', '0', '6'}; |
73 | | - System.out.println(lcs(A, B)); // 339970 |
| 98 | + // LCS: 339970 |
| 99 | + System.out.println("LCS: " + lcs("398397970", "3399917206")); |
74 | 100 | } |
75 | 101 | } |
0 commit comments