diff --git a/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java b/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java index febab0566..8d7ceae8e 100644 --- a/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java +++ b/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java @@ -1,5 +1,13 @@ /** - * Implementation of finding the longest paldindrome subsequence Time complexity: O(n^2) + * Longest Palindrome Subsequence (LPS) + * + *
Given a string S, find the length of the longest subsequence in S that is also a palindrome. + * + *
Important: A subsequence is different from a substring. Subsequences do not need to be + * contiguous. For example, in the string "BBBAB", the longest palindrome subsequence is "BBBB" with + * length 4, whereas the longest palindrome substring is "BBB" with length 3. + * + *
Time Complexity: O(n^2) * * @author William Fiset, william.alexandre.fiset@gmail.com */ @@ -7,33 +15,73 @@ public class LongestPalindromeSubsequence { - public static void main(String[] args) { - System.out.println(lps("bbbab")); // Outputs 4 since "bbbb" is valid soln - System.out.println(lps("bccd")); // Outputs 2 since "cc" is valid soln - } - - // Returns the length of the longest paldindrome subsequence - public static int lps(String s) { + /** + * Recursive implementation with memoization to find the length of + * the longest palindrome subsequence. + * + * Time Complexity: O(n^2) + * Space Complexity: O(n^2) + */ + public static int lpsRecursive(String s) { if (s == null || s.length() == 0) return 0; Integer[][] dp = new Integer[s.length()][s.length()]; - return lps(s, dp, 0, s.length() - 1); + return lpsRecursive(s, dp, 0, s.length() - 1); } - // Private recursive method with memoization to count - // the longest paldindrome subsequence. - private static int lps(String s, Integer[][] dp, int i, int j) { - - // Base cases + private static int lpsRecursive(String s, Integer[][] dp, int i, int j) { if (j < i) return 0; if (i == j) return 1; if (dp[i][j] != null) return dp[i][j]; - char c1 = s.charAt(i), c2 = s.charAt(j); + if (s.charAt(i) == s.charAt(j)) { + // If characters at both ends match, they form part of the palindrome. + // We add 2 to the result and shrink the window from both sides (i+1, j-1). + return dp[i][j] = lpsRecursive(s, dp, i + 1, j - 1) + 2; + } + // If characters don't match, we take the maximum by either: + // 1. Skipping the left character (i+1) + // 2. Skipping the right character (j-1) + return dp[i][j] = Math.max(lpsRecursive(s, dp, i + 1, j), lpsRecursive(s, dp, i, j - 1)); + } + + /** + * Iterative implementation (bottom-up) to find the length of + * the longest palindrome subsequence. + * + * Time Complexity: O(n^2) + * Space Complexity: O(n^2) + */ + public static int lpsIterative(String s) { + if (s == null || s.isEmpty()) return 0; + int n = s.length(); + int[][] dp = new int[n][n]; + + // Every single character is a palindrome of length 1 + for (int i = 0; i < n; i++) dp[i][i] = 1; - // Both end characters match - if (c1 == c2) return dp[i][j] = lps(s, dp, i + 1, j - 1) + 2; + for (int len = 2; len <= n; len++) { + for (int i = 0; i <= n - len; i++) { + int j = i + len - 1; + if (s.charAt(i) == s.charAt(j)) { + // Characters match: use the result from the inner substring (i+1, j-1) and add 2. + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + // Characters don't match: take the best result from either skipping the + // left character (i+1) or the right character (j-1). + dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); + } + } + } + return dp[0][n - 1]; + } + + public static void main(String[] args) { + String s1 = "bbbab"; + System.out.println(lpsRecursive(s1)); // 4 + System.out.println(lpsIterative(s1)); // 4 - // Consider both possible substrings and take the maximum - return dp[i][j] = Math.max(lps(s, dp, i + 1, j), lps(s, dp, i, j - 1)); + String s2 = "bccd"; + System.out.println(lpsRecursive(s2)); // 2 + System.out.println(lpsIterative(s2)); // 2 } } diff --git a/src/test/java/com/williamfiset/algorithms/dp/BUILD b/src/test/java/com/williamfiset/algorithms/dp/BUILD index 882c1baad..9890160fd 100644 --- a/src/test/java/com/williamfiset/algorithms/dp/BUILD +++ b/src/test/java/com/williamfiset/algorithms/dp/BUILD @@ -94,5 +94,16 @@ java_test( deps = TEST_DEPS, ) +# bazel test //src/test/java/com/williamfiset/algorithms/dp:LongestPalindromeSubsequenceTest +java_test( + name = "LongestPalindromeSubsequenceTest", + srcs = ["LongestPalindromeSubsequenceTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.dp.LongestPalindromeSubsequenceTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + # Run all tests # bazel test //src/test/java/com/williamfiset/algorithms/dp:all diff --git a/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java b/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java new file mode 100644 index 000000000..fd888777f --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java @@ -0,0 +1,38 @@ +package com.williamfiset.algorithms.dp; + +import static com.google.common.truth.Truth.assertThat; +import org.junit.jupiter.api.Test; + +public class LongestPalindromeSubsequenceTest { + + @Test + public void testLps() { + String s1 = "bbbab"; + assertThat(LongestPalindromeSubsequence.lpsRecursive(s1)).isEqualTo(4); + assertThat(LongestPalindromeSubsequence.lpsIterative(s1)).isEqualTo(4); + + String s2 = "bccd"; + assertThat(LongestPalindromeSubsequence.lpsRecursive(s2)).isEqualTo(2); + assertThat(LongestPalindromeSubsequence.lpsIterative(s2)).isEqualTo(2); + + String s3 = "abcde"; + assertThat(LongestPalindromeSubsequence.lpsRecursive(s3)).isEqualTo(1); + assertThat(LongestPalindromeSubsequence.lpsIterative(s3)).isEqualTo(1); + + String s4 = "aaaaa"; + assertThat(LongestPalindromeSubsequence.lpsRecursive(s4)).isEqualTo(5); + assertThat(LongestPalindromeSubsequence.lpsIterative(s4)).isEqualTo(5); + } + + @Test + public void testEmptyStrings() { + assertThat(LongestPalindromeSubsequence.lpsRecursive("")).isEqualTo(0); + assertThat(LongestPalindromeSubsequence.lpsIterative("")).isEqualTo(0); + } + + @Test + public void testNullInputs() { + assertThat(LongestPalindromeSubsequence.lpsRecursive(null)).isEqualTo(0); + assertThat(LongestPalindromeSubsequence.lpsIterative(null)).isEqualTo(0); + } +}