Skip to content

Commit af1d9d1

Browse files
authored
feat(strings): add Kasai's algorithm for LCP array construction (#7324)
* feat(strings): add Kasai's algorithm for LCP array Implement Kasai's algorithm to compute the Longest Common Prefix (LCP) array in O(N) time given a string and its suffix array. Add KasaiAlgorithm.java and KasaiAlgorithmTest.java. * style(strings): fix KasaiAlgorithmTest array initialization format for clang-format
1 parent 7d57c57 commit af1d9d1

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.thealgorithms.strings;
2+
3+
/**
4+
* Kasai's Algorithm for constructing the Longest Common Prefix (LCP) array.
5+
*
6+
* <p>
7+
* The LCP array stores the lengths of the longest common prefixes between
8+
* lexicographically adjacent suffixes of a string. Kasai's algorithm computes
9+
* this array in O(N) time given the string and its suffix array.
10+
* </p>
11+
*
12+
* @see <a href="https://en.wikipedia.org/wiki/LCP_array">LCP array - Wikipedia</a>
13+
*/
14+
public final class KasaiAlgorithm {
15+
16+
private KasaiAlgorithm() {
17+
}
18+
19+
/**
20+
* Computes the LCP array using Kasai's algorithm.
21+
*
22+
* @param text the original string
23+
* @param suffixArr the suffix array of the string
24+
* @return the LCP array of length N, where LCP[i] is the length of the longest
25+
* common prefix of the suffixes indexed by suffixArr[i] and suffixArr[i+1].
26+
* The last element LCP[N-1] is always 0.
27+
* @throws IllegalArgumentException if text or suffixArr is null, or their lengths differ
28+
*/
29+
public static int[] kasai(String text, int[] suffixArr) {
30+
if (text == null || suffixArr == null) {
31+
throw new IllegalArgumentException("Text and suffix array must not be null.");
32+
}
33+
int n = text.length();
34+
if (suffixArr.length != n) {
35+
throw new IllegalArgumentException("Suffix array length must match text length.");
36+
}
37+
if (n == 0) {
38+
return new int[0];
39+
}
40+
41+
// Compute the inverse suffix array
42+
// invSuff[i] stores the index of the suffix text.substring(i) in the suffix array
43+
int[] invSuff = new int[n];
44+
for (int i = 0; i < n; i++) {
45+
if (suffixArr[i] < 0 || suffixArr[i] >= n) {
46+
throw new IllegalArgumentException("Suffix array contains out-of-bounds index.");
47+
}
48+
invSuff[suffixArr[i]] = i;
49+
}
50+
51+
int[] lcp = new int[n];
52+
int k = 0; // Length of the longest common prefix
53+
54+
for (int i = 0; i < n; i++) {
55+
// Suffix at index i has not a next suffix in suffix array
56+
int rank = invSuff[i];
57+
if (rank == n - 1) {
58+
k = 0;
59+
continue;
60+
}
61+
62+
int nextSuffixIndex = suffixArr[rank + 1];
63+
64+
// Directly match characters to find LCP
65+
while (i + k < n && nextSuffixIndex + k < n && text.charAt(i + k) == text.charAt(nextSuffixIndex + k)) {
66+
k++;
67+
}
68+
69+
lcp[rank] = k;
70+
71+
// Delete the starting character from the string
72+
if (k > 0) {
73+
k--;
74+
}
75+
}
76+
77+
return lcp;
78+
}
79+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.thealgorithms.strings;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
public class KasaiAlgorithmTest {
9+
10+
@Test
11+
public void testKasaiBanana() {
12+
String text = "banana";
13+
// Suffixes:
14+
// 0: banana
15+
// 1: anana
16+
// 2: nana
17+
// 3: ana
18+
// 4: na
19+
// 5: a
20+
//
21+
// Sorted Suffixes:
22+
// 5: a
23+
// 3: ana
24+
// 1: anana
25+
// 0: banana
26+
// 4: na
27+
// 2: nana
28+
int[] suffixArr = {5, 3, 1, 0, 4, 2};
29+
30+
int[] expectedLcp = {1, 3, 0, 0, 2, 0};
31+
32+
assertArrayEquals(expectedLcp, KasaiAlgorithm.kasai(text, suffixArr));
33+
}
34+
35+
@Test
36+
public void testKasaiAaaa() {
37+
String text = "aaaa";
38+
// Sorted Suffixes:
39+
// 3: a
40+
// 2: aa
41+
// 1: aaa
42+
// 0: aaaa
43+
int[] suffixArr = {3, 2, 1, 0};
44+
int[] expectedLcp = {1, 2, 3, 0};
45+
46+
assertArrayEquals(expectedLcp, KasaiAlgorithm.kasai(text, suffixArr));
47+
}
48+
49+
@Test
50+
public void testKasaiEmptyString() {
51+
assertArrayEquals(new int[0], KasaiAlgorithm.kasai("", new int[0]));
52+
}
53+
54+
@Test
55+
public void testKasaiSingleChar() {
56+
assertArrayEquals(new int[] {0}, KasaiAlgorithm.kasai("A", new int[] {0}));
57+
}
58+
59+
@Test
60+
public void testKasaiNullTextOrSuffixArray() {
61+
assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai(null, new int[] {0}));
62+
assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", null));
63+
}
64+
65+
@Test
66+
public void testKasaiInvalidSuffixArrayLength() {
67+
assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {0, 1}));
68+
}
69+
70+
@Test
71+
public void testKasaiInvalidSuffixArrayIndex() {
72+
assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {1})); // Out of bounds
73+
assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {-1})); // Out of bounds
74+
}
75+
}

0 commit comments

Comments
 (0)