Skip to content

Commit 654b600

Browse files
author
irenemartnez
committed
perf(strings): optimize LongestCommonSubstring DP memory to O(N) and add tests
1 parent 87edc62 commit 654b600

2 files changed

Lines changed: 102 additions & 7 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: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import static org.junit.jupiter.api.Assertions.assertEquals;
55

66
import org.junit.jupiter.api.Test;
7-
7+
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
8+
import java.time.Duration;
89
public class LongestCommonSubstringTest {
910

1011
@Test
@@ -33,4 +34,98 @@ public void testMultipleMatchesFirstLongest() {
3334
// Keeps the first matched longest substring when lengths are tied
3435
assertEquals("abc", LongestCommonSubstring.longestCommonSubstring("abcXdef", "abcYdef"));
3536
}
37+
// NEW
38+
39+
@Test
40+
public void testSpacesAndSpecialCharacters() {
41+
assertEquals(" Hello World! ", LongestCommonSubstring.longestCommonSubstring("123 Hello World! 456", "ABC Hello World! XYZ"));
42+
assertEquals("@#$%^", LongestCommonSubstring.longestCommonSubstring("test@#$%^123", "abc@#$%^xyz"));
43+
}
44+
45+
@Test
46+
public void testCoincidenceAtBoundaries() {
47+
// Match at the beginning
48+
assertEquals("PREFIX_", LongestCommonSubstring.longestCommonSubstring("PREFIX_12345", "PREFIX_67890"));
49+
// Match at the end
50+
assertEquals("_SUFFIX", LongestCommonSubstring.longestCommonSubstring("12345_SUFFIX", "67890_SUFFIX"));
51+
}
52+
53+
@Test
54+
public void testRepeatedPatterns() {
55+
assertEquals("anabanana", LongestCommonSubstring.longestCommonSubstring("bananabanana", "anabanana"));
56+
}
57+
58+
@Test
59+
public void testLargeInputsPerformanceAndTimeout() {
60+
// Generate two 3,000-character strings containing a common substring in the middle
61+
int size = 3000;
62+
StringBuilder sb1 = new StringBuilder(size);
63+
StringBuilder sb2 = new StringBuilder(size);
64+
65+
for (int i = 0; i < 1000; i++) {
66+
sb1.append('A');
67+
sb2.append('B');
68+
}
69+
70+
String commonPart = "COMMON_LONG_SUBSTRING_TEST_1234567890";
71+
sb1.append(commonPart);
72+
sb2.append(commonPart);
73+
74+
for (int i = 0; i < 1500; i++) {
75+
sb1.append('X');
76+
sb2.append('Y');
77+
}
78+
79+
// Verify that the algorithm completes within 2 seconds
80+
assertTimeoutPreemptively(Duration.ofSeconds(2), () -> {
81+
String result = LongestCommonSubstring.longestCommonSubstring(sb1.toString(), sb2.toString());
82+
assertEquals(commonPart, result);
83+
});
84+
}
85+
86+
@Test
87+
public void testVeryLargeInputsTimeoutFailure() {
88+
// Generate two very large strings (4,000 characters each)
89+
int size = 4000;
90+
StringBuilder sb1 = new StringBuilder(size);
91+
StringBuilder sb2 = new StringBuilder(size);
92+
93+
for (int i = 0; i < size; i++) {
94+
sb1.append('A');
95+
sb2.append('B');
96+
}
97+
98+
// Enforce a strict 50ms time limit which this O(N * M) computation will exceed
99+
assertTimeoutPreemptively(Duration.ofMillis(50), () -> {
100+
LongestCommonSubstring.longestCommonSubstring(sb1.toString(), sb2.toString());
101+
});
102+
}
103+
104+
@Test
105+
public void testCaseSensitivityAndUnicode() {
106+
// Case sensitivity test
107+
assertEquals("ABC", LongestCommonSubstring.longestCommonSubstring("ABCdef", "123ABCxyz"));
108+
assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", "ABC"));
109+
110+
// Full substring containment
111+
assertEquals("inside", LongestCommonSubstring.longestCommonSubstring("inside", "text_inside_here"));
112+
113+
// Unicode characters
114+
assertEquals("_áéíóú_", LongestCommonSubstring.longestCommonSubstring("hola_áéíóú_mundo", "test_áéíóú_abc"));
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)