2573. Find the String with LCP
Problem Description
Given an n x n
matrix called lcp
, you need to find the alphabetically smallest string word
of length n
that would produce this exact lcp
matrix. If no such string exists, return an empty string.
The lcp
matrix is defined as follows:
lcp[i][j]
represents the length of the longest common prefix between two substrings ofword
:- The first substring is
word[i,n-1]
(from indexi
to the end) - The second substring is
word[j,n-1]
(from indexj
to the end)
- The first substring is
For example, if word = "abab"
:
lcp[0][2] = 2
because substrings"abab"
and"ab"
have a longest common prefix of"ab"
(length 2)lcp[1][3] = 1
because substrings"bab"
and"b"
have a longest common prefix of"b"
(length 1)
The solution requires you to:
- Construct a string that would generate the given
lcp
matrix - Ensure the string uses only lowercase English letters
- Return the lexicographically smallest valid string (earliest in dictionary order)
- Return an empty string if no valid string can produce the given
lcp
matrix
The approach involves:
- Greedily assigning characters starting from
'a'
to positions that must share the same character based on thelcp
values - Validating that the constructed string actually produces the given
lcp
matrix by checking if characters match and verifying the longest common prefix lengths
Intuition
The key insight is understanding what the lcp
matrix tells us about the structure of the string. If lcp[i][j] > 0
, it means the substrings starting at positions i
and j
share at least one common character at their beginning. More specifically, if lcp[i][j] = k
, then word[i] = word[j]
, word[i+1] = word[j+1]
, ..., up to word[i+k-1] = word[j+k-1]
.
Since we want the lexicographically smallest string, we should use characters in alphabetical order starting from 'a'
. The greedy approach works here: whenever we encounter a position that hasn't been assigned a character yet, we assign it the smallest available character.
The construction process follows this logic:
- Start with the first unfilled position and assign it
'a'
- Look at all positions
j
wherelcp[i][j] > 0
- these positions must also start with'a'
because they share a common prefix with positioni
- Move to the next unfilled position and assign it
'b'
, repeating the process - Continue until all positions are filled or we run out of letters
The validation step is crucial because not all lcp
matrices correspond to valid strings. The relationship between lcp
values must be consistent:
- If
s[i] = s[j]
, thenlcp[i][j]
should equal1 + lcp[i+1][j+1]
(except at the string's end where it should be1
) - If
s[i] ≠ s[j]
, thenlcp[i][j]
must be0
This recursive relationship makes sense: if two substrings start with the same character, their longest common prefix is 1 plus the longest common prefix of the substrings starting from their next positions. By checking these conditions backwards (from the end of the string), we can verify if our constructed string produces the exact lcp
matrix given.
Learn more about Greedy, Union Find and Dynamic Programming patterns.
Solution Approach
The implementation consists of two main phases: construction and validation.
Phase 1: Construction
We initialize an empty string array s
of length n
and attempt to fill it with characters:
s = [""] * n
i = 0
for c in ascii_lowercase:
while i < n and s[i]:
i += 1
if i == n:
break
for j in range(i, n):
if lcp[i][j]:
s[j] = c
- We iterate through lowercase letters starting from
'a'
- For each letter, we find the first unfilled position
i
- If
lcp[i][j] > 0
for any positionj ≥ i
, we know positionsi
andj
must share the same starting character, so we assign the current letter to positionj
- This greedy assignment ensures we use the smallest possible letters in lexicographical order
After construction, if any position remains unfilled ("" in s
), it means the lcp
matrix is invalid, and we return an empty string.
Phase 2: Validation
We verify that our constructed string actually produces the given lcp
matrix:
for i in range(n - 1, -1, -1):
for j in range(n - 1, -1, -1):
if s[i] == s[j]:
if i == n - 1 or j == n - 1:
if lcp[i][j] != 1:
return ""
elif lcp[i][j] != lcp[i + 1][j + 1] + 1:
return ""
elif lcp[i][j]:
return ""
We iterate backwards from the end of the string to check:
-
When
s[i] = s[j]
:- If either
i
orj
is at the last position (n-1
), thenlcp[i][j]
should equal1
(only one character can match) - Otherwise,
lcp[i][j]
should equallcp[i+1][j+1] + 1
(the recursive relationship: matching character plus the LCP of remaining substrings)
- If either
-
When
s[i] ≠ s[j]
:lcp[i][j]
must be0
(no common prefix possible)
The backward iteration is crucial because it allows us to build up the validation from known base cases (at the string's end) to earlier positions.
If all checks pass, we join the character array into a string and return it: return "".join(s)
.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with lcp = [[3,2,1],[2,2,1],[1,1,1]]
.
Step 1: Construction Phase
We start with an empty array s = ["", "", ""]
and iterate through lowercase letters.
Using letter 'a' (i=0):
- First unfilled position is
i=0
- Check
lcp[0][j]
for allj
:lcp[0][0] = 3
→ assigns[0] = 'a'
lcp[0][1] = 2
→ assigns[1] = 'a'
lcp[0][2] = 1
→ assigns[2] = 'a'
- Result:
s = ['a', 'a', 'a']
Using letter 'b' (i=1):
- First unfilled position is
i=1
, buts[1]
is already filled - Move to
i=2
, buts[2]
is already filled - Move to
i=3
, which exceeds array bounds - All positions are filled, so we exit
Our constructed string is "aaa"
.
Step 2: Validation Phase
Now we validate backwards that "aaa"
produces the given lcp
matrix.
Check position (2,2):
s[2] = s[2]
('a' = 'a')- Both indices are at position
n-1 = 2
- Expected:
lcp[2][2] = 1
✓
Check position (2,1):
s[2] = s[1]
('a' = 'a')- Index 2 is at position
n-1
- Expected:
lcp[2][1] = 1
✓
Check position (2,0):
s[2] = s[0]
('a' = 'a')- Index 2 is at position
n-1
- Expected:
lcp[2][0] = 1
✓
Check position (1,2):
s[1] = s[2]
('a' = 'a')- Index 2 is at position
n-1
- Expected:
lcp[1][2] = 1
✓
Check position (1,1):
s[1] = s[1]
('a' = 'a')- Neither index at
n-1
, so check:lcp[1][1] = lcp[2][2] + 1 = 1 + 1 = 2
✓
Check position (1,0):
s[1] = s[0]
('a' = 'a')- Neither index at
n-1
, so check:lcp[1][0] = lcp[2][1] + 1 = 1 + 1 = 2
✓
Check position (0,2):
s[0] = s[2]
('a' = 'a')- Index 2 is at position
n-1
- Expected:
lcp[0][2] = 1
✓
Check position (0,1):
s[0] = s[1]
('a' = 'a')- Neither index at
n-1
, so check:lcp[0][1] = lcp[1][2] + 1 = 1 + 1 = 2
✓
Check position (0,0):
s[0] = s[0]
('a' = 'a')- Neither index at
n-1
, so check:lcp[0][0] = lcp[1][1] + 1 = 2 + 1 = 3
✓
All validations pass! The final answer is "aaa"
.
To verify our understanding, let's confirm the LCP values:
lcp[0][0] = 3
: substrings "aaa" and "aaa" have LCP "aaa" (length 3) ✓lcp[0][1] = 2
: substrings "aaa" and "aa" have LCP "aa" (length 2) ✓lcp[0][2] = 1
: substrings "aaa" and "a" have LCP "a" (length 1) ✓lcp[1][1] = 2
: substrings "aa" and "aa" have LCP "aa" (length 2) ✓lcp[1][2] = 1
: substrings "aa" and "a" have LCP "a" (length 1) ✓lcp[2][2] = 1
: substrings "a" and "a" have LCP "a" (length 1) ✓
Solution Implementation
1from typing import List
2from string import ascii_lowercase
3
4class Solution:
5 def findTheString(self, lcp: List[List[int]]) -> str:
6 """
7 Constructs a string from its LCP (Longest Common Prefix) matrix.
8
9 Args:
10 lcp: A 2D matrix where lcp[i][j] represents the longest common prefix
11 length starting from positions i and j in the target string.
12
13 Returns:
14 The lexicographically smallest string that matches the LCP matrix,
15 or empty string if no valid string exists.
16 """
17 n = len(lcp)
18
19 # Initialize result string array with empty strings
20 result_string = [""] * n
21
22 # Greedy assignment: assign smallest available character to unassigned positions
23 current_position = 0
24 for character in ascii_lowercase:
25 # Find next unassigned position
26 while current_position < n and result_string[current_position]:
27 current_position += 1
28
29 # All positions have been assigned
30 if current_position == n:
31 break
32
33 # Assign current character to all positions that must have same character
34 # based on LCP matrix (if lcp[i][j] > 0, positions i and j must share prefix)
35 for j in range(current_position, n):
36 if lcp[current_position][j] > 0:
37 result_string[j] = character
38
39 # Check if all positions have been assigned
40 if "" in result_string:
41 return ""
42
43 # Validate the constructed string against the LCP matrix
44 for i in range(n - 1, -1, -1):
45 for j in range(n - 1, -1, -1):
46 if result_string[i] == result_string[j]:
47 # Characters match, so LCP should continue
48 if i == n - 1 or j == n - 1:
49 # At boundary: LCP should be exactly 1
50 if lcp[i][j] != 1:
51 return ""
52 else:
53 # Not at boundary: LCP should be 1 + LCP of next positions
54 if lcp[i][j] != lcp[i + 1][j + 1] + 1:
55 return ""
56 else:
57 # Characters don't match, so LCP should be 0
58 if lcp[i][j] != 0:
59 return ""
60
61 # Join the character array into final string
62 return "".join(result_string)
63
1class Solution {
2 public String findTheString(int[][] lcp) {
3 int n = lcp.length;
4 char[] result = new char[n];
5 int currentIndex = 0;
6
7 // Try to assign characters from 'a' to 'z' to build the string
8 for (char currentChar = 'a'; currentChar <= 'z'; ++currentChar) {
9 // Find the next unassigned position
10 while (currentIndex < n && result[currentIndex] != '\0') {
11 ++currentIndex;
12 }
13
14 // If all positions are filled, we're done
15 if (currentIndex == n) {
16 break;
17 }
18
19 // Assign current character to all positions that should have it
20 // based on LCP values (if lcp[currentIndex][j] > 0, they share a prefix)
21 for (int j = currentIndex; j < n; ++j) {
22 if (lcp[currentIndex][j] > 0) {
23 result[j] = currentChar;
24 }
25 }
26 }
27
28 // Check if all positions have been assigned a character
29 for (int i = 0; i < n; ++i) {
30 if (result[i] == '\0') {
31 return ""; // Not enough characters to satisfy LCP constraints
32 }
33 }
34
35 // Validate the constructed string against LCP matrix
36 for (int i = n - 1; i >= 0; --i) {
37 for (int j = n - 1; j >= 0; --j) {
38 if (result[i] == result[j]) {
39 // Characters match, verify LCP value
40 if (i == n - 1 || j == n - 1) {
41 // At boundary, LCP should be exactly 1
42 if (lcp[i][j] != 1) {
43 return "";
44 }
45 } else {
46 // LCP should be 1 + LCP of next positions
47 if (lcp[i][j] != lcp[i + 1][j + 1] + 1) {
48 return "";
49 }
50 }
51 } else {
52 // Characters don't match, LCP should be 0
53 if (lcp[i][j] > 0) {
54 return "";
55 }
56 }
57 }
58 }
59
60 return String.valueOf(result);
61 }
62}
63
1class Solution {
2public:
3 string findTheString(vector<vector<int>>& lcp) {
4 int n = lcp.size();
5 string result(n, '\0'); // Initialize string with null characters
6 int currentIndex = 0;
7
8 // Step 1: Construct the string based on LCP constraints
9 // Try to assign characters 'a' to 'z' to positions in the string
10 for (char ch = 'a'; ch <= 'z'; ++ch) {
11 // Find the next unassigned position
12 while (currentIndex < n && result[currentIndex] != '\0') {
13 ++currentIndex;
14 }
15
16 // If all positions are assigned, we're done
17 if (currentIndex == n) {
18 break;
19 }
20
21 // Assign current character to all positions that must have it
22 // based on LCP values with currentIndex
23 for (int j = currentIndex; j < n; ++j) {
24 if (lcp[currentIndex][j] > 0) {
25 result[j] = ch;
26 }
27 }
28 }
29
30 // Step 2: Check if all positions have been assigned
31 if (result.find('\0') != string::npos) {
32 return ""; // Not enough characters to satisfy constraints
33 }
34
35 // Step 3: Validate the constructed string against LCP matrix
36 for (int i = n - 1; i >= 0; --i) {
37 for (int j = n - 1; j >= 0; --j) {
38 if (result[i] == result[j]) {
39 // Characters match, so LCP should be positive
40 if (i == n - 1 || j == n - 1) {
41 // At boundary: LCP should be exactly 1
42 if (lcp[i][j] != 1) {
43 return "";
44 }
45 } else {
46 // Not at boundary: LCP should follow recurrence relation
47 // lcp[i][j] = lcp[i+1][j+1] + 1
48 if (lcp[i][j] != lcp[i + 1][j + 1] + 1) {
49 return "";
50 }
51 }
52 } else {
53 // Characters don't match, so LCP should be 0
54 if (lcp[i][j] != 0) {
55 return "";
56 }
57 }
58 }
59 }
60
61 return result;
62 }
63};
64
1/**
2 * Finds the lexicographically smallest string that satisfies the given LCP matrix
3 * @param lcp - 2D array where lcp[i][j] represents the longest common prefix length
4 * between suffixes starting at positions i and j
5 * @returns The lexicographically smallest string if it exists, empty string otherwise
6 */
7function findTheString(lcp: number[][]): string {
8 const n: number = lcp.length;
9
10 // Initialize string with null characters (using space as placeholder)
11 let resultArray: string[] = new Array(n).fill(' ');
12 let currentPosition: number = 0;
13
14 // Try to assign characters from 'a' to 'z' to build the string
15 for (let asciiCode: number = 97; asciiCode < 123; asciiCode++) {
16 const currentChar: string = String.fromCharCode(asciiCode);
17
18 // Find the next unassigned position
19 while (currentPosition < n && resultArray[currentPosition] !== ' ') {
20 currentPosition++;
21 }
22
23 // If all positions are assigned, we're done building the string
24 if (currentPosition === n) {
25 break;
26 }
27
28 // Assign the current character to all positions that should have it
29 // based on the LCP matrix constraints
30 for (let j: number = currentPosition; j < n; j++) {
31 if (lcp[currentPosition][j] > 0) {
32 resultArray[j] = currentChar;
33 }
34 }
35 }
36
37 // Convert array to string for easier manipulation
38 let result: string = resultArray.join('');
39
40 // Check if all positions have been assigned a character
41 if (result.includes(' ')) {
42 return '';
43 }
44
45 // Validate that the constructed string satisfies the LCP matrix
46 for (let i: number = n - 1; i >= 0; i--) {
47 for (let j: number = n - 1; j >= 0; j--) {
48 if (result[i] === result[j]) {
49 // Characters match, so LCP should be calculated correctly
50 if (i === n - 1 || j === n - 1) {
51 // At the boundary, LCP should be 1 for matching characters
52 if (lcp[i][j] !== 1) {
53 return '';
54 }
55 } else {
56 // LCP should be 1 + LCP of the next positions
57 if (lcp[i][j] !== lcp[i + 1][j + 1] + 1) {
58 return '';
59 }
60 }
61 } else {
62 // Characters don't match, so LCP should be 0
63 if (lcp[i][j] !== 0) {
64 return '';
65 }
66 }
67 }
68 }
69
70 return result;
71}
72
Time and Space Complexity
Time Complexity: O(n²)
The algorithm consists of three main parts:
-
String construction phase: The outer loop iterates through at most 26 characters (lowercase letters). For each character, we scan through the array to find an empty position (
O(n)
), and then iterate through positions to assign the character based onlcp[i][j]
values (O(n)
). Overall, this phase isO(26 × n) = O(n)
. -
Validation phase: This uses two nested loops that iterate from
n-1
to0
, checking each pair of positions(i, j)
. For each pair, we perform constant-time operations to validate the LCP constraints. This results inO(n²)
operations.
The dominant term is O(n²)
from the validation phase, giving us an overall time complexity of O(n²)
.
Space Complexity: O(n)
The algorithm uses:
- Array
s
of sizen
to store the constructed string:O(n)
- A few scalar variables (
i
,c
):O(1)
- The final string created by
"".join(s)
:O(n)
The total space complexity is O(n)
, where n
is the length of the string to be constructed.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Validation Order - Forward vs Backward Iteration
One of the most common mistakes is validating the LCP matrix in forward order (from index 0 to n-1) instead of backward order. This leads to referencing lcp[i+1][j+1]
values that haven't been validated yet.
Incorrect approach:
# Wrong: Forward iteration
for i in range(n):
for j in range(n):
if s[i] == s[j]:
if i == n - 1 or j == n - 1:
if lcp[i][j] != 1:
return ""
elif lcp[i][j] != lcp[i + 1][j + 1] + 1:
# Problem: lcp[i+1][j+1] hasn't been validated yet!
return ""
Correct approach:
# Correct: Backward iteration
for i in range(n - 1, -1, -1):
for j in range(n - 1, -1, -1):
if s[i] == s[j]:
if i == n - 1 or j == n - 1:
if lcp[i][j] != 1:
return ""
elif lcp[i][j] != lcp[i + 1][j + 1] + 1:
# Now lcp[i+1][j+1] has already been validated
return ""
2. Not Checking Matrix Symmetry and Diagonal Values
The LCP matrix must be symmetric (lcp[i][j] == lcp[j][i]
) and diagonal values must equal n - i
(since lcp[i][i]
represents the full suffix starting at position i). Failing to validate these properties can lead to accepting invalid matrices.
Solution - Add preliminary validation:
# Check matrix properties before construction
for i in range(n):
for j in range(n):
# Check symmetry
if lcp[i][j] != lcp[j][i]:
return ""
# Check diagonal values
if i == j and lcp[i][j] != n - i:
return ""
# Check bounds
if lcp[i][j] > n - max(i, j):
return ""
3. Forgetting to Handle Empty Positions After Character Assignment
After the greedy character assignment phase, some positions might remain unassigned if we run out of lowercase letters (26 characters). Not checking for this case can cause errors when joining the string.
Incomplete check:
# Missing validation after assignment result_string = [""] * n # ... character assignment logic ... # Directly joining without checking return "".join(result_string) # Error if any position is still ""
Complete solution:
# Proper validation if "" in result_string: return "" # Invalid LCP matrix - can't construct valid string
4. Misunderstanding the LCP Relationship
A subtle but critical error is misunderstanding how LCP values relate to character positions. When lcp[i][j] > 0
, it means positions i
and j
must have the same character, but this doesn't mean only these two positions share the character - all positions with non-zero LCP values with position i
must share the same character.
Incorrect assignment:
# Wrong: Only assigning to position j when lcp[i][j] > 0 if lcp[i][j] > 0: result_string[j] = character break # Wrong! Should check all positions
Correct assignment:
# Correct: Check all positions from current_position to n
for j in range(current_position, n):
if lcp[current_position][j] > 0:
result_string[j] = character
5. Off-by-One Errors in Boundary Conditions
When checking if we're at the string's boundary, ensure you're comparing with n - 1
(the last valid index) rather than n
.
Wrong boundary check:
if i == n or j == n: # Wrong: n is out of bounds if lcp[i][j] != 1: return ""
Correct boundary check:
if i == n - 1 or j == n - 1: # Correct: n-1 is the last position if lcp[i][j] != 1: return ""
Which data structure is used to implement priority queue?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!