|
| 1 | +# 516. Longest Palindromic Subsequence |
| 2 | +# 🟠 Medium |
| 3 | +# |
| 4 | +# https://leetcode.com/problems/longest-palindromic-subsequence/ |
| 5 | +# |
| 6 | +# Tags: String - Dynamic Programming |
| 7 | + |
| 8 | +import timeit |
| 9 | +from bisect import bisect_left |
| 10 | +from collections import defaultdict |
| 11 | +from functools import cache |
| 12 | + |
| 13 | + |
| 14 | +# Use a 2D array to store the LPS between two indexes l and r that we |
| 15 | +# have already computed, the base case is when l == r => 1 and l > r => |
| 16 | +# 0. For each pair l, r, we check base cases, then check the cache, |
| 17 | +# otherwise compute the LPS, to do it, we check if the characters at l |
| 18 | +# and r match, if they do, the result will be equal to the LPS at l+1, |
| 19 | +# r-1 plus 2, if they don't match, the result will be the max LPS |
| 20 | +# between not using the left or the right character. |
| 21 | +# |
| 22 | +# Time complexity: O(n^2) - We compute half of all possible values of |
| 23 | +# (l, r) and the computations take amortized constant time. |
| 24 | +# Space complexity: O(n^2) - The dp array can hold one key for each |
| 25 | +# combination of (l, r). |
| 26 | +# |
| 27 | +# Runtime 836 ms Beats 97.34% |
| 28 | +# Memory 71 MB Beats 25.78% |
| 29 | +class Use2DArray: |
| 30 | + def longestPalindromeSubseq(self, s: str) -> int: |
| 31 | + n = len(s) |
| 32 | + dp = [[-1] * n for _ in range(n)] |
| 33 | + |
| 34 | + def solve(l, r) -> int: |
| 35 | + if l == r: |
| 36 | + return 1 |
| 37 | + if l > r: |
| 38 | + return 0 |
| 39 | + if dp[l][r] != -1: |
| 40 | + return dp[l][r] |
| 41 | + # Are the characters a match? |
| 42 | + if s[l] == s[r]: |
| 43 | + dp[l][r] = 2 + solve(l + 1, r - 1) |
| 44 | + else: |
| 45 | + dp[l][r] = max(solve(l, r - 1), solve(l + 1, r)) |
| 46 | + return dp[l][r] |
| 47 | + |
| 48 | + return solve(0, n - 1) |
| 49 | + |
| 50 | + |
| 51 | +# Use a dictionary to store the LPS between two indexes l and r that we |
| 52 | +# have already computed, the base case is when l == r => 1 and l > r => |
| 53 | +# 0. For each pair l, r, we check base cases, then check the cache, |
| 54 | +# otherwise compute the LPS, to do it, we check if the characters at l |
| 55 | +# and r match, if they do, the result will be equal to the LPS at l+1, |
| 56 | +# r-1 plus 2, if they don't match, the result will be the max LPS |
| 57 | +# between not using the left or the right character. |
| 58 | +# |
| 59 | +# Time complexity: O(n^2) - We compute half of all possible values of |
| 60 | +# (l, r) and the computations take amortized constant time. |
| 61 | +# Space complexity: O(n^2) - The dp dictionary can hold one key for each |
| 62 | +# combination of (l, r). |
| 63 | +# |
| 64 | +# Runtime 1308 ms Beats 76.8% |
| 65 | +# Memory 236.1 MB Beats 20.17% |
| 66 | +class UseDict: |
| 67 | + def longestPalindromeSubseq(self, s: str) -> int: |
| 68 | + n = len(s) |
| 69 | + dp = {} |
| 70 | + |
| 71 | + def solve(l, r) -> int: |
| 72 | + if l == r: |
| 73 | + return 1 |
| 74 | + if l > r: |
| 75 | + return 0 |
| 76 | + if (l, r) in dp: |
| 77 | + return dp[(l, r)] |
| 78 | + # Are the characters a match? |
| 79 | + if s[l] == s[r]: |
| 80 | + dp[(l, r)] = 2 + solve(l + 1, r - 1) |
| 81 | + else: |
| 82 | + dp[(l, r)] = max(solve(l, r - 1), solve(l + 1, r)) |
| 83 | + return dp[(l, r)] |
| 84 | + |
| 85 | + return solve(0, n - 1) |
| 86 | + |
| 87 | + |
| 88 | +# Similar to the previous solution but use the built-in @cache. |
| 89 | +# |
| 90 | +# Time complexity: O(n^2) - We compute half of all possible values of |
| 91 | +# (l, r) and the computations take amortized constant time. |
| 92 | +# Space complexity: O(n^2) - The dp array can hold one key for each |
| 93 | +# combination of (l, r). |
| 94 | +# |
| 95 | +# Runtime 921 ms Beats 96% |
| 96 | +# Memory 237.4 MB Beats 16.75% |
| 97 | +class UseCache: |
| 98 | + def longestPalindromeSubseq(self, s: str) -> int: |
| 99 | + @cache |
| 100 | + def solve(l, r) -> int: |
| 101 | + if l == r: |
| 102 | + return 1 |
| 103 | + if l > r: |
| 104 | + return 0 |
| 105 | + # Are the characters a match? |
| 106 | + if s[l] == s[r]: |
| 107 | + return 2 + solve(l + 1, r - 1) |
| 108 | + return max(solve(l, r - 1), solve(l + 1, r)) |
| 109 | + |
| 110 | + return solve(0, len(s) - 1) |
| 111 | + |
| 112 | + |
| 113 | +# Iterative bottom-up version, use an array and compute the values |
| 114 | +# starting with the smaller substrings, use previous results to |
| 115 | +# compute bigger gaps of (l, r). |
| 116 | +# |
| 117 | +# Time complexity: O(n^2) - We compute half of all possible values of |
| 118 | +# (l, r) and the computations take amortized constant time. |
| 119 | +# Space complexity: O(n) - We use 2 arrays of size n. |
| 120 | +# |
| 121 | +# Runtime 1029 ms Beats 86.51% |
| 122 | +# Memory 13.9 MB Beats 97.69% |
| 123 | +class DP: |
| 124 | + def longestPalindromeSubseq(self, s: str) -> int: |
| 125 | + n = len(s) |
| 126 | + dp, tmp = [[0] * n for _ in range(2)] |
| 127 | + |
| 128 | + for i in reversed(range(n)): |
| 129 | + tmp[i] = 1 |
| 130 | + for j in range(i + 1, n): |
| 131 | + if s[i] == s[j]: |
| 132 | + tmp[j] = dp[j - 1] + 2 |
| 133 | + else: |
| 134 | + tmp[j] = max(dp[j], tmp[j - 1]) |
| 135 | + dp = tmp[:] |
| 136 | + |
| 137 | + return dp[-1] |
| 138 | + |
| 139 | + |
| 140 | +# Iterative bottom-up version, use an array and compute the values |
| 141 | +# starting with the smaller substrings, use previous results to |
| 142 | +# compute bigger gaps of (l, r). |
| 143 | +# |
| 144 | +# Time complexity: O(n^2) - We compute half of all possible values of |
| 145 | +# (l, r) and the computations take amortized constant time. |
| 146 | +# Space complexity: O(n) - We use 2 arrays of size n. |
| 147 | +# |
| 148 | +# Runtime 1029 ms Beats 86.51% |
| 149 | +# Memory 13.9 MB Beats 97.69% |
| 150 | +class DP: |
| 151 | + def longestPalindromeSubseq(self, s: str) -> int: |
| 152 | + n = len(s) |
| 153 | + dp, tmp = [[0] * n for _ in range(2)] |
| 154 | + |
| 155 | + for i in reversed(range(n)): |
| 156 | + tmp[i] = 1 |
| 157 | + for j in range(i + 1, n): |
| 158 | + if s[i] == s[j]: |
| 159 | + tmp[j] = dp[j - 1] + 2 |
| 160 | + else: |
| 161 | + tmp[j] = max(dp[j], tmp[j - 1]) |
| 162 | + dp = tmp[:] |
| 163 | + |
| 164 | + return dp[-1] |
| 165 | + |
| 166 | + |
| 167 | +# Use the algorithm to compute the longest common subsequence between |
| 168 | +# two strings and use it with the input and its reversed version as |
| 169 | +# the parameters. |
| 170 | +# |
| 171 | +# Time complexity: O(n^2) - We iterate over all characters in s, for |
| 172 | +# each, we may iterate over all characters of the reversed s. |
| 173 | +# Space complexity: O(n) - The dictionary |
| 174 | +# |
| 175 | +# Runtime 1976 ms Beats 42.18% |
| 176 | +# Memory 14 MB Beats 93.52% |
| 177 | +class LCS: |
| 178 | + def longestPalindromeSubseq(self, s: str) -> int: |
| 179 | + # The size of the dp array is => O(LCS) max == O(min(m, n)) |
| 180 | + dp = [] |
| 181 | + # Create a dictionary of characters in text2 to the positions on |
| 182 | + # which they can be found. |
| 183 | + d = defaultdict(list) |
| 184 | + for i, c in enumerate(s[::-1]): |
| 185 | + d[c].append(i) |
| 186 | + # Iterate over the characters in text1 checking in which |
| 187 | + # position of the LCS they could be inserted. |
| 188 | + for c in s: |
| 189 | + if c in d: |
| 190 | + for i in reversed(d[c]): |
| 191 | + # Find the position at which we could use this index |
| 192 | + # in the dp array. |
| 193 | + ins = bisect_left(dp, i) |
| 194 | + # We could append this character to the current LCS. |
| 195 | + if ins == len(dp): |
| 196 | + dp.append(i) |
| 197 | + # This character could be inserted before the |
| 198 | + # current character at this position of the LCS, |
| 199 | + # which makes it more likely to be able to append |
| 200 | + # later. |
| 201 | + else: |
| 202 | + dp[ins] = i |
| 203 | + return len(dp) |
| 204 | + |
| 205 | + |
| 206 | +def test(): |
| 207 | + executors = [ |
| 208 | + Use2DArray, |
| 209 | + UseDict, |
| 210 | + UseCache, |
| 211 | + DP, |
| 212 | + LCS, |
| 213 | + ] |
| 214 | + tests = [ |
| 215 | + ["cbbd", 2], |
| 216 | + ["bbbab", 4], |
| 217 | + ] |
| 218 | + for executor in executors: |
| 219 | + start = timeit.default_timer() |
| 220 | + for _ in range(1): |
| 221 | + for col, t in enumerate(tests): |
| 222 | + sol = executor() |
| 223 | + result = sol.longestPalindromeSubseq(t[0]) |
| 224 | + exp = t[1] |
| 225 | + assert result == exp, ( |
| 226 | + f"\033[93m» {result} <> {exp}\033[91m for" |
| 227 | + + f" test {col} using \033[1m{executor.__name__}" |
| 228 | + ) |
| 229 | + stop = timeit.default_timer() |
| 230 | + used = str(round(stop - start, 5)) |
| 231 | + cols = "{0:20}{1:10}{2:10}" |
| 232 | + res = cols.format(executor.__name__, used, "seconds") |
| 233 | + print(f"\033[92m» {res}\033[0m") |
| 234 | + |
| 235 | + |
| 236 | +test() |
0 commit comments