Find the String with LCP
HardPrompt
We define the lcp matrix of any 0-indexed string word of n lowercase English letters as an n x n grid such that:
lcp[i][j]is equal to the length of the longest common prefix between the substringsword[i,n-1]andword[j,n-1].
Given an n x n matrix lcp, return the alphabetically smallest string word that corresponds to lcp. If there is no such string, return an empty string.
A string a is lexicographically smaller than a string b (of the same length) if in the first position where a and b differ, string a has a letter that appears earlier in the alphabet than the corresponding letter in b. For example, "aabd" is lexicographically smaller than "aaca" because the first position they differ is at the third letter, and 'b' comes before 'c'.
Example 1:
Input: lcp = [[4,0,2,0],[0,3,0,1],[2,0,2,0],[0,1,0,1]]
Output: "abab"
Explanation: lcp corresponds to any 4 letter string with two alternating letters. The lexicographically smallest of them is "abab".Example 2:
Input: lcp = [[4,3,2,1],[3,3,2,1],[2,2,2,1],[1,1,1,1]]
Output: "aaaa"
Explanation: lcp corresponds to any 4 letter string with a single distinct letter. The lexicographically smallest of them is "aaaa". Example 3:
Input: lcp = [[4,3,2,1],[3,3,2,1],[2,2,2,1],[1,1,1,3]]
Output: ""
Explanation: lcp[3][3] cannot be equal to 3 since word[3,...,3] consists of only a single letter; Thus, no answer exists.
Constraints:
1 <= n ==lcp.length ==lcp[i].length<= 10000 <= lcp[i][j] <= n
Approaches
2 approaches with complexity analysis and trade-offs.
This approach involves building the string character by character from left to right. At each position, we try to place a character from 'a' to 'z'. After placing a character, we check if the prefix of the string built so far is consistent with the given lcp matrix. If it's not consistent, we backtrack and try the next character. If we successfully build a complete string of length n, we have found the lexicographically smallest solution because we explore characters in alphabetical order.
Algorithm
- Define a recursive function, say
solve(index, currentWord), that attempts to build the string character by character. - The base case is
index == n. When reached, a full candidate string is formed. Validate this string against thelcpmatrix. If it matches, a solution is found. - In the recursive step for
index, iterate through characterscfrom 'a' to 'z'. - For each character
c, assigncurrentWord[index] = c. - Perform a partial validation of the prefix
currentWord[0...index]against thelcpmatrix. Check for any immediate contradictions withlcp[i][j]fori, j <= index. - If the prefix is consistent, make a recursive call
solve(index + 1, currentWord). - If the recursive call finds a solution, return it. Otherwise, backtrack and try the next character.
- Since characters are tried in alphabetical order ('a' to 'z'), the first complete valid string found will be the lexicographically smallest.
Walkthrough
The backtracking algorithm explores the search space of all possible strings of length n. It does so by constructing the string one character at a time. To ensure the result is lexicographically smallest, it tries placing 'a', then 'b', and so on, at each position.
After adding a character at index k, the algorithm checks if the prefix of length k+1 violates any conditions imposed by the lcp matrix. For example, if we have word[i] and word[j] (with i, j <= k), and word[i] != word[j], then lcp[i][j] must be 0. If the input lcp[i][j] is greater than 0, this path is invalid, and the algorithm must backtrack.
This pruning of invalid paths helps reduce the search space, but the worst-case complexity remains exponential. A full validation is required once a complete string is formed.
Due to its high time complexity, this approach is not practical for the given problem constraints but serves as a theoretical brute-force method.
Complexity
Time
O(26^n)
Space
O(n)
Trade-offs
Pros
Conceptually straightforward as it's an exhaustive search.
Guaranteed to find the lexicographically smallest string if one exists.
Cons
Extremely high time complexity, making it infeasible for the given constraints (
nup to 1000).The logic for partial validation at each step is complex to implement correctly and efficiently.
A highly efficient approach is to use a two-phase greedy algorithm. First, we construct the lexicographically smallest candidate string based on the properties of the lcp matrix. Second, we perform a full verification to check if this candidate string is indeed valid. If it is, it must be the answer. If not, no solution exists.
Algorithm
- Get the size of the matrix,
n. - Greedy Construction:
- Initialize a character array
wordof sizenand a characternextChar = 'a'. - Iterate
ifrom0ton-1:- If
word[i]has not been assigned a character yet:- If
nextChar > 'z', return""as more than 26 unique characters are needed. - Assign
word[i] = nextChar. - For all
j > i, iflcp[i][j] > 0, it impliesword[j]must be the same asword[i]. Assignword[j] = nextChar. - Increment
nextChar.
- If
- If
- Initialize a character array
- Verification:
- The constructed
wordis the only candidate for the lexicographically smallest solution. Now, we must verify it. - Create a new
n x nmatrix,computedLcp. - Fill
computedLcpusing dynamic programming, iteratingiandjfromn-1down to0:- If
word[i] == word[j],computedLcp[i][j] = 1 + (i+1 < n && j+1 < n ? computedLcp[i+1][j+1] : 0). - Otherwise,
computedLcp[i][j] = 0.
- If
- During the computation, if at any point
computedLcp[i][j]does not match the inputlcp[i][j], the candidate is invalid. Return"".
- The constructed
- If the entire verification process completes without returning, the candidate string is valid. Return it.
Walkthrough
This approach is broken down into two main parts: construction and verification. It's also good practice to perform some initial sanity checks on the input matrix.
Initial Sanity Checks (Optional but Recommended):
Before starting, we can check for basic properties that any valid lcp matrix must have:
lcp[i][i] == n - ifor alli.- The matrix must be symmetric:
lcp[i][j] == lcp[j][i]. - The LCP value cannot exceed the length of the suffixes:
lcp[i][j] <= n - max(i, j). If any of these fail, we can immediately return an empty string.
Phase 1: Greedy Construction
To find the lexicographically smallest string, we should use the smallest characters ('a', 'b', 'c', ...) as early as possible. We iterate through the string positions from left to right (i = 0 to n-1). If the character at the current position word[i] hasn't been determined yet, we assign it the next available character from the alphabet. This assignment creates a new character group. The lcp matrix tells us which other indices must belong to the same group. Specifically, if lcp[i][j] > 0, then word[j] must equal word[i]. We enforce this by assigning the same character to all such j's.
public String findString(int[][] lcp) { int n = lcp.length; char[] word = new char[n]; char nextChar = 'a'; for (int i = 0; i < n; i++) { if (word[i] == 0) { // Character not yet assigned if (nextChar > 'z') { return ""; // Need more than 26 distinct characters } word[i] = nextChar; for (int j = i + 1; j < n; j++) { if (lcp[i][j] > 0) { word[j] = nextChar; } } nextChar++; } }Phase 2: Verification
The candidate string is built by only considering if lcp[i][j] is zero or non-zero. We must now verify the exact values. We do this by computing the LCP matrix for our generated word and comparing it to the input lcp matrix. This can be done efficiently with dynamic programming.
We iterate backwards from the end of the string. The LCP of two suffixes word[i...] and word[j...] is 0 if word[i] != word[j]. If word[i] == word[j], it is 1 + the LCP of the rest of the suffixes, word[i+1...] and word[j+1...].
// Verification Phase for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { int currentLcp; if (word[i] != word[j]) { currentLcp = 0; } else { if (i == n - 1 || j == n - 1) { currentLcp = 1; } else { currentLcp = 1 + lcp[i + 1][j + 1]; } } if (lcp[i][j] != currentLcp) { return ""; } } } // A more robust way is to recompute from scratch int[][] computedLcp = new int[n][n]; for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { if (word[i] == word[j]) { computedLcp[i][j] = 1; if (i + 1 < n && j + 1 < n) { computedLcp[i][j] += computedLcp[i + 1][j + 1]; } } if (computedLcp[i][j] != lcp[i][j]) { return ""; } } } return new String(word);}This combined approach correctly and efficiently finds the solution.
Complexity
Time
O(n^2)
Space
O(n^2) or O(n)
Trade-offs
Pros
Very efficient with a polynomial time complexity.
The greedy strategy is guaranteed to find the lexicographically smallest candidate.
The verification step is robust and ensures the candidate is correct.
Cons
Requires a full O(n^2) verification phase after construction.
The logic relies on two separate phases (construction and verification), which might seem less direct than a single-pass algorithm.
Solutions
Solution
class Solution {public String findTheString(int[][] lcp) { int n = lcp.length; char[] s = new char[n]; int i = 0; for (char c = 'a'; c <= 'z'; ++c) { while (i < n && s[i] != '\0') { ++i; } if (i == n) { break; } for (int j = i; j < n; ++j) { if (lcp[i][j] > 0) { s[j] = c; } } } for (i = 0; i < n; ++i) { if (s[i] == '\0') { return ""; } } for (i = n - 1; i >= 0; --i) { for (int j = n - 1; j >= 0; --j) { if (s[i] == s[j]) { if (i == n - 1 || j == n - 1) { if (lcp[i][j] != 1) { return ""; } } else if (lcp[i][j] != lcp[i + 1][j + 1] + 1) { return ""; } } else if (lcp[i][j] > 0) { return ""; } } } return String.valueOf(s); }}class Solution {public: string findTheString(vector<vector<int>> &lcp) { int i = 0, n = lcp.size(); string s(n, '\0'); for (char c = 'a'; c <= 'z'; ++c) { while (i < n && s[i]) { ++i; } if (i == n) { break; } for (int j = i; j < n; ++j) { if (lcp[i][j]) { s[j] = c; } } } if (s.find('\0') != -1) { return ""; } for (i = n - 1; ~i; --i) { for (int j = n - 1; ~j; --j) { if (s[i] == s[j]) { if (i == n - 1 || j == n - 1) { if (lcp[i][j] != 1) { return ""; } } else if (lcp[i][j] != lcp[i + 1][j + 1] + 1) { return ""; } } else if (lcp[i][j]) { return ""; } } } return s; }};class Solution: def findTheString(self, lcp: List[List[int]]) -> str: n = len(lcp) 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 if "" in s: return "" 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 "" return "" . join(s)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.