Skip to content

Commit b2b9d3a

Browse files
irenemrtinezirenemartnez
andauthored
perf(strings): optimize LongestCommonSubstring DP memory to O(N) and … (#7613)
* perf(strings): optimize LongestCommonSubstring DP memory to O(N) and add tests * fix(strings): fix formatting, unicode characters and update performance test * style(strings): add trailing newline to fix Checkstyle and Clang-format * style(strings): inline lambda in testVeryLargeInputsPerformance for clang-format --------- Co-authored-by: irenemartnez <117649832+irenemartnez@users.noreply.github.com>
1 parent 87edc62 commit b2b9d3a

2 files changed

Lines changed: 101 additions & 6 deletions

File tree

src/main/java/com/thealgorithms/strings/LongestCommonSubstring.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,20 @@ public static String longestCommonSubstring(final String a, final String b) {
2929
return "";
3030
}
3131

32-
int[][] dp = new int[a.length() + 1][b.length() + 1];
32+
int[] dp = new int[b.length() + 1];
3333
int maxLength = 0;
3434
int endIndex = 0;
3535

3636
for (int i = 1; i <= a.length(); i++) {
37-
for (int j = 1; j <= b.length(); j++) {
37+
for (int j = b.length(); j >= 1; j--) {
3838
if (a.charAt(i - 1) == b.charAt(j - 1)) {
39-
dp[i][j] = dp[i - 1][j - 1] + 1;
40-
if (dp[i][j] > maxLength) {
41-
maxLength = dp[i][j];
39+
dp[j] = dp[j - 1] + 1;
40+
if (dp[j] > maxLength) {
41+
maxLength = dp[j];
4242
endIndex = i;
4343
}
4444
} else {
45-
dp[i][j] = 0;
45+
dp[j] = 0;
4646
}
4747
}
4848
}

src/test/java/com/thealgorithms/strings/LongestCommonSubstringTest.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// author: Vraj Prajapati @Rosander0
33

44
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
56

7+
import java.time.Duration;
68
import org.junit.jupiter.api.Test;
79

810
public class LongestCommonSubstringTest {
@@ -33,4 +35,97 @@ public void testMultipleMatchesFirstLongest() {
3335
// Keeps the first matched longest substring when lengths are tied
3436
assertEquals("abc", LongestCommonSubstring.longestCommonSubstring("abcXdef", "abcYdef"));
3537
}
38+
39+
// NEW
40+
41+
@Test
42+
public void testSpacesAndSpecialCharacters() {
43+
assertEquals(" Hello World! ", LongestCommonSubstring.longestCommonSubstring("123 Hello World! 456", "ABC Hello World! XYZ"));
44+
assertEquals("@#$%^", LongestCommonSubstring.longestCommonSubstring("test@#$%^123", "abc@#$%^xyz"));
45+
}
46+
47+
@Test
48+
public void testCoincidenceAtBoundaries() {
49+
// Match at the beginning
50+
assertEquals("PREFIX_", LongestCommonSubstring.longestCommonSubstring("PREFIX_12345", "PREFIX_67890"));
51+
// Match at the end
52+
assertEquals("_SUFFIX", LongestCommonSubstring.longestCommonSubstring("12345_SUFFIX", "67890_SUFFIX"));
53+
}
54+
55+
@Test
56+
public void testRepeatedPatterns() {
57+
assertEquals("anabanana", LongestCommonSubstring.longestCommonSubstring("bananabanana", "anabanana"));
58+
}
59+
60+
@Test
61+
public void testLargeInputsPerformanceAndTimeout() {
62+
// Generate two 3,000-character strings containing a common substring in the middle
63+
int size = 3000;
64+
StringBuilder sb1 = new StringBuilder(size);
65+
StringBuilder sb2 = new StringBuilder(size);
66+
67+
for (int i = 0; i < 1000; i++) {
68+
sb1.append('A');
69+
sb2.append('B');
70+
}
71+
72+
String commonPart = "COMMON_LONG_SUBSTRING_TEST_1234567890";
73+
sb1.append(commonPart);
74+
sb2.append(commonPart);
75+
76+
for (int i = 0; i < 1500; i++) {
77+
sb1.append('X');
78+
sb2.append('Y');
79+
}
80+
81+
// Verify that the algorithm completes within 2 seconds
82+
assertTimeoutPreemptively(Duration.ofSeconds(2), () -> {
83+
String result = LongestCommonSubstring.longestCommonSubstring(sb1.toString(), sb2.toString());
84+
assertEquals(commonPart, result);
85+
});
86+
}
87+
88+
@Test
89+
public void testVeryLargeInputsPerformance() {
90+
// Generate two very large strings (4,000 characters each)
91+
int size = 4000;
92+
StringBuilder sb1 = new StringBuilder(size);
93+
StringBuilder sb2 = new StringBuilder(size);
94+
95+
for (int i = 0; i < size; i++) {
96+
sb1.append('A');
97+
sb2.append('B');
98+
}
99+
100+
assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { assertEquals("", LongestCommonSubstring.longestCommonSubstring(sb1.toString(), sb2.toString())); });
101+
}
102+
103+
@Test
104+
public void testCaseSensitivityAndUnicode() {
105+
// Case sensitivity test
106+
assertEquals("ABC", LongestCommonSubstring.longestCommonSubstring("ABCdef", "123ABCxyz"));
107+
assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", "ABC"));
108+
109+
// Full substring containment
110+
assertEquals("inside", LongestCommonSubstring.longestCommonSubstring("inside", "text_inside_here"));
111+
112+
// Unicode characters
113+
assertEquals("_áéíóú_", LongestCommonSubstring.longestCommonSubstring("hola_áéíóú_mundo", "test_áéíóú_abc"));
114+
}
115+
116+
@Test
117+
public void testWhitespaceAndControlCharacters() {
118+
// Test with newlines and tabs
119+
assertEquals("\t\n", LongestCommonSubstring.longestCommonSubstring("start\t\nend", "begin\t\nfinish"));
120+
121+
// Test with multiple consecutive spaces
122+
assertEquals(" ", LongestCommonSubstring.longestCommonSubstring("a b", "x y"));
123+
}
124+
125+
@Test
126+
public void testOverlappingSubstrings() {
127+
// Test overlapping matches like "AAAA" in "AAAAA" vs "AAAA"
128+
assertEquals("AAAA", LongestCommonSubstring.longestCommonSubstring("AAAAA", "AAAA"));
129+
assertEquals("ABAB", LongestCommonSubstring.longestCommonSubstring("ABABAB", "CABAB"));
130+
}
36131
}

0 commit comments

Comments
 (0)