|
| 1 | +# [Problem 2573: Find the String with LCP](https://leetcode.com/problems/find-the-string-with-lcp/description/?envType=daily-question) |
| 2 | + |
| 3 | +## Initial thoughts (stream-of-consciousness) |
| 4 | +We are given an n x n LCP matrix describing pairwise longest common prefixes between suffixes word[i:] and word[j:]. The matrix imposes equalities between characters: if lcp[i][j] > 0 then word[i] == word[j]; more generally if lcp[i][j] >= k then word[i+k-1] == word[j+k-1]. Also if lcp[i][j] = L and both i+L and j+L are valid indices, then word[i+L] != word[j+L]. So constraints are "must-equal" (connect indices into components) and "must-differ" (edges between components). We must find the lexicographically smallest lowercase string satisfying them or return "" if impossible. |
| 5 | + |
| 6 | +A straightforward approach is: |
| 7 | +- Validate obvious properties of lcp (diagonal and symmetry and bounds). |
| 8 | +- Use DSU to union indices that must be equal (union i and j whenever lcp[i][j] > 0 — note we don't need to union every shifted pair explicitly because those shifted pairs will appear as (i+1, j+1) etc in the matrix). |
| 9 | +- Build "must-differ" constraints between DSU components using the lcp values (for lcp[i][j] = L, if i+L<n and j+L<n then comp(i+L) != comp(j+L)). |
| 10 | +- Assign letters to components in the order of appearance (i from 0..n-1) picking the smallest available letter not used by already-assigned neighbors to keep the string lexicographically smallest. |
| 11 | +- Finally compute the LCP matrix from the constructed string using DP (dp[i][j] = 1 + dp[i+1][j+1] if s[i]==s[j] else 0) and compare to the given lcp. If match, return the string, else "". |
| 12 | + |
| 13 | +This gives an overall O(n^2) algorithm (dominated by scanning lcp and validating via DP), which is fine for n ≤ 1000. |
| 14 | + |
| 15 | +## Refining the problem, round 2 thoughts |
| 16 | +Edge cases/consistency checks to handle early: |
| 17 | +- lcp[i][i] must equal n - i for all i (suffix compared to itself). |
| 18 | +- lcp must be symmetric and values cannot exceed n - max(i, j). |
| 19 | +- When building "must-differ" edges, if two indices that must differ end up in the same DSU component, it's impossible. |
| 20 | +- When assigning letters, we have only 26 lowercase letters; if a component needs a 27th distinct letter (due to constraints), that's impossible and we return "". |
| 21 | +- After producing the candidate string, we must validate it by recomputing the lcp via DP. This both checks all constraints and catches subtle inconsistencies not caught before. |
| 22 | + |
| 23 | +This approach avoids the naive O(n^3) unioning across every shifted index by relying on the fact that lcp entries themselves cover the shifted equalities (the pair (i+1, j+1) will be present and unioned if needed). |
| 24 | + |
| 25 | +Now implement. |
| 26 | + |
| 27 | +## Attempted solution(s) |
| 28 | +```python |
| 29 | +# Python 3 solution |
| 30 | + |
| 31 | +class DSU: |
| 32 | + def __init__(self, n): |
| 33 | + self.p = list(range(n)) |
| 34 | + self.r = [0]*n |
| 35 | + def find(self, x): |
| 36 | + while self.p[x] != x: |
| 37 | + self.p[x] = self.p[self.p[x]] |
| 38 | + x = self.p[x] |
| 39 | + return x |
| 40 | + def union(self, a, b): |
| 41 | + ra = self.find(a); rb = self.find(b) |
| 42 | + if ra == rb: |
| 43 | + return False |
| 44 | + if self.r[ra] < self.r[rb]: |
| 45 | + self.p[ra] = rb |
| 46 | + elif self.r[rb] < self.r[ra]: |
| 47 | + self.p[rb] = ra |
| 48 | + else: |
| 49 | + self.p[rb] = ra |
| 50 | + self.r[ra] += 1 |
| 51 | + return True |
| 52 | + |
| 53 | +class Solution: |
| 54 | + def findTheString(self, lcp): |
| 55 | + n = len(lcp) |
| 56 | + # Basic validation: diagonal and symmetry and bounds |
| 57 | + for i in range(n): |
| 58 | + if lcp[i][i] != n - i: |
| 59 | + return "" |
| 60 | + for i in range(n): |
| 61 | + for j in range(n): |
| 62 | + if lcp[i][j] != lcp[j][i]: |
| 63 | + return "" |
| 64 | + if lcp[i][j] > n - max(i, j): |
| 65 | + return "" |
| 66 | + if lcp[i][j] < 0: |
| 67 | + return "" |
| 68 | + # Build DSU: if lcp[i][j] > 0 then s[i] == s[j] |
| 69 | + dsu = DSU(n) |
| 70 | + for i in range(n): |
| 71 | + for j in range(n): |
| 72 | + if lcp[i][j] > 0: |
| 73 | + dsu.union(i, j) |
| 74 | + # Build "must-differ" edges between components |
| 75 | + comp_index = {} |
| 76 | + for i in range(n): |
| 77 | + comp_index[dsu.find(i)] = True |
| 78 | + # Map components to 0..m-1 |
| 79 | + comp_list = list(comp_index.keys()) |
| 80 | + comp_id = {c: idx for idx, c in enumerate(comp_list)} |
| 81 | + m = len(comp_list) |
| 82 | + neigh = [set() for _ in range(m)] |
| 83 | + for i in range(n): |
| 84 | + for j in range(n): |
| 85 | + L = lcp[i][j] |
| 86 | + pi = i + L |
| 87 | + pj = j + L |
| 88 | + if pi < n and pj < n: |
| 89 | + ci = comp_id[dsu.find(pi)] |
| 90 | + cj = comp_id[dsu.find(pj)] |
| 91 | + if ci == cj: |
| 92 | + # they must differ but are same component -> impossible |
| 93 | + return "" |
| 94 | + neigh[ci].add(cj) |
| 95 | + neigh[cj].add(ci) |
| 96 | + # Assign letters to components in order of first occurrence in string to ensure lexicographic minimality |
| 97 | + comp_of_pos = [comp_id[dsu.find(i)] for i in range(n)] |
| 98 | + assigned = [-1] * m # store int 0..25 for 'a'..'z' or -1 if unassigned |
| 99 | + for i in range(n): |
| 100 | + c = comp_of_pos[i] |
| 101 | + if assigned[c] != -1: |
| 102 | + continue |
| 103 | + # gather forbidden letters from already assigned neighbors |
| 104 | + forbidden = [False]*26 |
| 105 | + for nb in neigh[c]: |
| 106 | + if assigned[nb] != -1: |
| 107 | + forbidden[assigned[nb]] = True |
| 108 | + # choose smallest available letter |
| 109 | + letter = -1 |
| 110 | + for k in range(26): |
| 111 | + if not forbidden[k]: |
| 112 | + letter = k |
| 113 | + break |
| 114 | + if letter == -1: |
| 115 | + return "" # more than 26 distinct required in conflicting neighbors |
| 116 | + assigned[c] = letter |
| 117 | + # Build string |
| 118 | + s_chars = [] |
| 119 | + for i in range(n): |
| 120 | + s_chars.append(chr(ord('a') + assigned[comp_of_pos[i]])) |
| 121 | + s = "".join(s_chars) |
| 122 | + # Validate by recomputing lcp via DP O(n^2) |
| 123 | + dp = [[0]*n for _ in range(n)] |
| 124 | + for i in range(n-1, -1, -1): |
| 125 | + for j in range(n-1, -1, -1): |
| 126 | + if s[i] == s[j]: |
| 127 | + if i+1 < n and j+1 < n: |
| 128 | + dp[i][j] = 1 + dp[i+1][j+1] |
| 129 | + else: |
| 130 | + dp[i][j] = 1 |
| 131 | + else: |
| 132 | + dp[i][j] = 0 |
| 133 | + # compare dp with given lcp |
| 134 | + for i in range(n): |
| 135 | + for j in range(n): |
| 136 | + if dp[i][j] != lcp[i][j]: |
| 137 | + return "" |
| 138 | + return s |
| 139 | + |
| 140 | +# For LeetCode usage: |
| 141 | +# sol = Solution() |
| 142 | +# print(sol.findTheString([[4,0,2,0],[0,3,0,1],[2,0,2,0],[0,1,0,1]])) # should output "abab" |
| 143 | +``` |
| 144 | + |
| 145 | +- Notes about approach: |
| 146 | + - Union indices i and j whenever lcp[i][j] > 0 to capture "first character equal" equalities. The shifted equalities are covered because the matrix contains entries for shifted pairs too (e.g., (i+1, j+1)). |
| 147 | + - Create "must-differ" edges from lcp[i][j] = L by connecting components of i+L and j+L (if those indices exist). If those two indices are in the same component, contradiction: return "". |
| 148 | + - Assign letters greedily in order of first position appearance to achieve lexicographically smallest string. For each component pick the smallest letter not used by already-assigned neighbors. |
| 149 | + - Finally compute the LCP matrix from constructed string using DP and compare; if mismatch, return "". |
| 150 | +- Complexity: |
| 151 | + - Time: O(n^2) to validate and process the lcp matrix and to compute/compare the DP LCP (DSU operations add near-constant overhead). n ≤ 1000 fits. |
| 152 | + - Space: O(n^2) only for the input and the DP validation; DSU and adjacency take O(n) and O(m^2) in worst-case but in our adjacency we store edges only implied by lcp, which is at most O(n^2) entries in practice; overall dominated by O(n^2). |
0 commit comments