Skip to content

Commit 7bf3377

Browse files
authored
Refactor LongestPalindromeSubsequence: add iterative solver, docs, an… (williamfiset#1284)
* Refactor LongestPalindromeSubsequence: add iterative solver, docs, and tests * Refactor LongestPalindromeSubsequence: remove redundant lps() method, add comments, and update tests.
1 parent 339f951 commit 7bf3377

3 files changed

Lines changed: 116 additions & 19 deletions

File tree

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,87 @@
11
/**
2-
* Implementation of finding the longest paldindrome subsequence Time complexity: O(n^2)
2+
* Longest Palindrome Subsequence (LPS)
3+
*
4+
* <p>Given a string S, find the length of the longest subsequence in S that is also a palindrome.
5+
*
6+
* <p>Important: A subsequence is different from a substring. Subsequences do not need to be
7+
* contiguous. For example, in the string "BBBAB", the longest palindrome subsequence is "BBBB" with
8+
* length 4, whereas the longest palindrome substring is "BBB" with length 3.
9+
*
10+
* <p>Time Complexity: O(n^2)
311
*
412
* @author William Fiset, [email protected]
513
*/
614
package com.williamfiset.algorithms.dp;
715

816
public class LongestPalindromeSubsequence {
917

10-
public static void main(String[] args) {
11-
System.out.println(lps("bbbab")); // Outputs 4 since "bbbb" is valid soln
12-
System.out.println(lps("bccd")); // Outputs 2 since "cc" is valid soln
13-
}
14-
15-
// Returns the length of the longest paldindrome subsequence
16-
public static int lps(String s) {
18+
/**
19+
* Recursive implementation with memoization to find the length of
20+
* the longest palindrome subsequence.
21+
*
22+
* Time Complexity: O(n^2)
23+
* Space Complexity: O(n^2)
24+
*/
25+
public static int lpsRecursive(String s) {
1726
if (s == null || s.length() == 0) return 0;
1827
Integer[][] dp = new Integer[s.length()][s.length()];
19-
return lps(s, dp, 0, s.length() - 1);
28+
return lpsRecursive(s, dp, 0, s.length() - 1);
2029
}
2130

22-
// Private recursive method with memoization to count
23-
// the longest paldindrome subsequence.
24-
private static int lps(String s, Integer[][] dp, int i, int j) {
25-
26-
// Base cases
31+
private static int lpsRecursive(String s, Integer[][] dp, int i, int j) {
2732
if (j < i) return 0;
2833
if (i == j) return 1;
2934
if (dp[i][j] != null) return dp[i][j];
3035

31-
char c1 = s.charAt(i), c2 = s.charAt(j);
36+
if (s.charAt(i) == s.charAt(j)) {
37+
// If characters at both ends match, they form part of the palindrome.
38+
// We add 2 to the result and shrink the window from both sides (i+1, j-1).
39+
return dp[i][j] = lpsRecursive(s, dp, i + 1, j - 1) + 2;
40+
}
41+
// If characters don't match, we take the maximum by either:
42+
// 1. Skipping the left character (i+1)
43+
// 2. Skipping the right character (j-1)
44+
return dp[i][j] = Math.max(lpsRecursive(s, dp, i + 1, j), lpsRecursive(s, dp, i, j - 1));
45+
}
46+
47+
/**
48+
* Iterative implementation (bottom-up) to find the length of
49+
* the longest palindrome subsequence.
50+
*
51+
* Time Complexity: O(n^2)
52+
* Space Complexity: O(n^2)
53+
*/
54+
public static int lpsIterative(String s) {
55+
if (s == null || s.isEmpty()) return 0;
56+
int n = s.length();
57+
int[][] dp = new int[n][n];
58+
59+
// Every single character is a palindrome of length 1
60+
for (int i = 0; i < n; i++) dp[i][i] = 1;
3261

33-
// Both end characters match
34-
if (c1 == c2) return dp[i][j] = lps(s, dp, i + 1, j - 1) + 2;
62+
for (int len = 2; len <= n; len++) {
63+
for (int i = 0; i <= n - len; i++) {
64+
int j = i + len - 1;
65+
if (s.charAt(i) == s.charAt(j)) {
66+
// Characters match: use the result from the inner substring (i+1, j-1) and add 2.
67+
dp[i][j] = dp[i + 1][j - 1] + 2;
68+
} else {
69+
// Characters don't match: take the best result from either skipping the
70+
// left character (i+1) or the right character (j-1).
71+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
72+
}
73+
}
74+
}
75+
return dp[0][n - 1];
76+
}
77+
78+
public static void main(String[] args) {
79+
String s1 = "bbbab";
80+
System.out.println(lpsRecursive(s1)); // 4
81+
System.out.println(lpsIterative(s1)); // 4
3582

36-
// Consider both possible substrings and take the maximum
37-
return dp[i][j] = Math.max(lps(s, dp, i + 1, j), lps(s, dp, i, j - 1));
83+
String s2 = "bccd";
84+
System.out.println(lpsRecursive(s2)); // 2
85+
System.out.println(lpsIterative(s2)); // 2
3886
}
3987
}

src/test/java/com/williamfiset/algorithms/dp/BUILD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,16 @@ java_test(
9494
deps = TEST_DEPS,
9595
)
9696

97+
# bazel test //src/test/java/com/williamfiset/algorithms/dp:LongestPalindromeSubsequenceTest
98+
java_test(
99+
name = "LongestPalindromeSubsequenceTest",
100+
srcs = ["LongestPalindromeSubsequenceTest.java"],
101+
main_class = "org.junit.platform.console.ConsoleLauncher",
102+
use_testrunner = False,
103+
args = ["--select-class=com.williamfiset.algorithms.dp.LongestPalindromeSubsequenceTest"],
104+
runtime_deps = JUNIT5_RUNTIME_DEPS,
105+
deps = TEST_DEPS,
106+
)
107+
97108
# Run all tests
98109
# bazel test //src/test/java/com/williamfiset/algorithms/dp:all
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.williamfiset.algorithms.dp;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
import org.junit.jupiter.api.Test;
5+
6+
public class LongestPalindromeSubsequenceTest {
7+
8+
@Test
9+
public void testLps() {
10+
String s1 = "bbbab";
11+
assertThat(LongestPalindromeSubsequence.lpsRecursive(s1)).isEqualTo(4);
12+
assertThat(LongestPalindromeSubsequence.lpsIterative(s1)).isEqualTo(4);
13+
14+
String s2 = "bccd";
15+
assertThat(LongestPalindromeSubsequence.lpsRecursive(s2)).isEqualTo(2);
16+
assertThat(LongestPalindromeSubsequence.lpsIterative(s2)).isEqualTo(2);
17+
18+
String s3 = "abcde";
19+
assertThat(LongestPalindromeSubsequence.lpsRecursive(s3)).isEqualTo(1);
20+
assertThat(LongestPalindromeSubsequence.lpsIterative(s3)).isEqualTo(1);
21+
22+
String s4 = "aaaaa";
23+
assertThat(LongestPalindromeSubsequence.lpsRecursive(s4)).isEqualTo(5);
24+
assertThat(LongestPalindromeSubsequence.lpsIterative(s4)).isEqualTo(5);
25+
}
26+
27+
@Test
28+
public void testEmptyStrings() {
29+
assertThat(LongestPalindromeSubsequence.lpsRecursive("")).isEqualTo(0);
30+
assertThat(LongestPalindromeSubsequence.lpsIterative("")).isEqualTo(0);
31+
}
32+
33+
@Test
34+
public void testNullInputs() {
35+
assertThat(LongestPalindromeSubsequence.lpsRecursive(null)).isEqualTo(0);
36+
assertThat(LongestPalindromeSubsequence.lpsIterative(null)).isEqualTo(0);
37+
}
38+
}

0 commit comments

Comments
 (0)