Skip to content

Commit 8208e71

Browse files
committed
LC 516. Longest Palindromic Subsequence (Python DP)
1 parent 7577759 commit 8208e71

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ Solutions to LeetCode problems. The first column links to the problem in LeetCod
249249
| [498. Diagonal Traverse][lc498] | 🟠 Medium | [![python](res/py.png)][lc498py] |
250250
| [502. IPO][lc502] | 🔴 Hard | [![python](res/py.png)][lc502py] [![rust](res/rs.png)][lc502rs] |
251251
| [509. Fibonacci Number][lc509] | 🟢 Easy | [![python](res/py.png)][lc509py] |
252+
| [516. Longest Palindromic Subsequence][lc516] | 🟠 Medium | [![python](res/py.png)][lc516py] |
252253
| [518. Coin Change II][lc518] | 🟠 Medium | [![python](res/py.png)][lc518py] [![rust](res/rs.png)][lc518rs] |
253254
| [520. Detect Capital][lc520] | 🟢 Easy | [![python](res/py.png)][lc520py] |
254255
| [523. Continuous Subarray Sum][lc523] | 🟠 Medium | [![python](res/py.png)][lc523py] |
@@ -982,6 +983,8 @@ Solutions to LeetCode problems. The first column links to the problem in LeetCod
982983
[lc502rs]: leetcode/ipo.rs
983984
[lc509]: https://leetcode.com/problems/fibonacci-number/
984985
[lc509py]: leetcode/fibonacci-number.py
986+
[lc516]: https://leetcode.com/problems/longest-palindromic-subsequence/
987+
[lc516py]: leetcode/longest-palindromic-subsequence.py
985988
[lc518]: https://leetcode.com/problems/coin-change-ii/
986989
[lc518py]: leetcode/coin-change-ii.py
987990
[lc518rs]: leetcode/coin-change-ii.rs
+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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

Comments
 (0)