From df7c61708357dfc31a2bb652bf5474b5cbd93ee6 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Tue, 10 Mar 2026 22:00:38 -0700 Subject: [PATCH 1/2] Refactor LongestPalindromeSubsequence: add iterative solver, docs, and tests --- .../dp/LongestPalindromeSubsequence.java | 79 +++++++++++++++---- .../java/com/williamfiset/algorithms/dp/BUILD | 11 +++ .../dp/LongestPalindromeSubsequenceTest.java | 40 ++++++++++ 3 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java diff --git a/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java b/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java index febab0566..7c367fd75 100644 --- a/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java +++ b/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java @@ -1,5 +1,10 @@ /** - * 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. + * + * Time Complexity: O(n^2) * * @author William Fiset, william.alexandre.fiset@gmail.com */ @@ -7,33 +12,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 palindrome subsequence. + * Defaults to the recursive implementation with memoization. + */ + public static int lps(String s) { + return lpsRecursive(s); } - // 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)) { + return dp[i][j] = lpsRecursive(s, dp, i + 1, j - 1) + 2; + } + 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)) { + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + 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(lps(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(lps(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 e19e69782..9dbcafc18 100644 --- a/src/test/java/com/williamfiset/algorithms/dp/BUILD +++ b/src/test/java/com/williamfiset/algorithms/dp/BUILD @@ -61,5 +61,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..38ec94ba4 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java @@ -0,0 +1,40 @@ +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.lps(s1)).isEqualTo(4); + assertThat(LongestPalindromeSubsequence.lpsRecursive(s1)).isEqualTo(4); + assertThat(LongestPalindromeSubsequence.lpsIterative(s1)).isEqualTo(4); + + String s2 = "bccd"; + assertThat(LongestPalindromeSubsequence.lps(s2)).isEqualTo(2); + assertThat(LongestPalindromeSubsequence.lpsRecursive(s2)).isEqualTo(2); + assertThat(LongestPalindromeSubsequence.lpsIterative(s2)).isEqualTo(2); + + String s3 = "abcde"; + assertThat(LongestPalindromeSubsequence.lps(s3)).isEqualTo(1); + + String s4 = "aaaaa"; + assertThat(LongestPalindromeSubsequence.lps(s4)).isEqualTo(5); + } + + @Test + public void testEmptyStrings() { + assertThat(LongestPalindromeSubsequence.lps("")).isEqualTo(0); + assertThat(LongestPalindromeSubsequence.lpsRecursive("")).isEqualTo(0); + assertThat(LongestPalindromeSubsequence.lpsIterative("")).isEqualTo(0); + } + + @Test + public void testNullInputs() { + assertThat(LongestPalindromeSubsequence.lps(null)).isEqualTo(0); + assertThat(LongestPalindromeSubsequence.lpsRecursive(null)).isEqualTo(0); + assertThat(LongestPalindromeSubsequence.lpsIterative(null)).isEqualTo(0); + } +} From 03334d3eff1fd3864af82e06c5c756ae603fad72 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Wed, 11 Mar 2026 10:06:27 -0700 Subject: [PATCH 2/2] Refactor LongestPalindromeSubsequence: remove redundant lps() method, add comments, and update tests. --- .../dp/LongestPalindromeSubsequence.java | 29 ++++++++++--------- .../dp/LongestPalindromeSubsequenceTest.java | 10 +++---- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java b/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java index 7c367fd75..8d7ceae8e 100644 --- a/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java +++ b/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java @@ -1,10 +1,13 @@ /** * Longest Palindrome Subsequence (LPS) * - * Given a string S, find the length of the longest subsequence in S - * that is also a palindrome. + *

Given a string S, find the length of the longest subsequence in S that is also a palindrome. * - * Time Complexity: O(n^2) + *

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 */ @@ -12,14 +15,6 @@ public class LongestPalindromeSubsequence { - /** - * Returns the length of the longest palindrome subsequence. - * Defaults to the recursive implementation with memoization. - */ - public static int lps(String s) { - return lpsRecursive(s); - } - /** * Recursive implementation with memoization to find the length of * the longest palindrome subsequence. @@ -39,8 +34,13 @@ private static int lpsRecursive(String s, Integer[][] dp, int i, int j) { if (dp[i][j] != null) return dp[i][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)); } @@ -63,8 +63,11 @@ public static int lpsIterative(String s) { 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]); } } @@ -74,11 +77,11 @@ public static int lpsIterative(String s) { public static void main(String[] args) { String s1 = "bbbab"; - System.out.println(lps(s1)); // 4 + System.out.println(lpsRecursive(s1)); // 4 System.out.println(lpsIterative(s1)); // 4 String s2 = "bccd"; - System.out.println(lps(s2)); // 2 + System.out.println(lpsRecursive(s2)); // 2 System.out.println(lpsIterative(s2)); // 2 } } diff --git a/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java b/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java index 38ec94ba4..fd888777f 100644 --- a/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java +++ b/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java @@ -8,32 +8,30 @@ public class LongestPalindromeSubsequenceTest { @Test public void testLps() { String s1 = "bbbab"; - assertThat(LongestPalindromeSubsequence.lps(s1)).isEqualTo(4); assertThat(LongestPalindromeSubsequence.lpsRecursive(s1)).isEqualTo(4); assertThat(LongestPalindromeSubsequence.lpsIterative(s1)).isEqualTo(4); String s2 = "bccd"; - assertThat(LongestPalindromeSubsequence.lps(s2)).isEqualTo(2); assertThat(LongestPalindromeSubsequence.lpsRecursive(s2)).isEqualTo(2); assertThat(LongestPalindromeSubsequence.lpsIterative(s2)).isEqualTo(2); String s3 = "abcde"; - assertThat(LongestPalindromeSubsequence.lps(s3)).isEqualTo(1); + assertThat(LongestPalindromeSubsequence.lpsRecursive(s3)).isEqualTo(1); + assertThat(LongestPalindromeSubsequence.lpsIterative(s3)).isEqualTo(1); String s4 = "aaaaa"; - assertThat(LongestPalindromeSubsequence.lps(s4)).isEqualTo(5); + assertThat(LongestPalindromeSubsequence.lpsRecursive(s4)).isEqualTo(5); + assertThat(LongestPalindromeSubsequence.lpsIterative(s4)).isEqualTo(5); } @Test public void testEmptyStrings() { - assertThat(LongestPalindromeSubsequence.lps("")).isEqualTo(0); assertThat(LongestPalindromeSubsequence.lpsRecursive("")).isEqualTo(0); assertThat(LongestPalindromeSubsequence.lpsIterative("")).isEqualTo(0); } @Test public void testNullInputs() { - assertThat(LongestPalindromeSubsequence.lps(null)).isEqualTo(0); assertThat(LongestPalindromeSubsequence.lpsRecursive(null)).isEqualTo(0); assertThat(LongestPalindromeSubsequence.lpsIterative(null)).isEqualTo(0); }