Skip to content

Commit fb6305f

Browse files
Add 'Longest Increasing Subsequence'
1 parent bd2a007 commit fb6305f

File tree

5 files changed

+112
-78
lines changed

5 files changed

+112
-78
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ A collection of LeetCode solutions
2424

2525
[Longest Common Subsequence](./src/longest_common_subsequence.py)
2626

27+
[Longest Increasing Subsequence](./src/longest_increasing_subsequence.py)
28+
2729
[Maximum Depth of Binary Tree](./src/maximum_depth_of_binary_tree.py)
2830

2931
[Maximum Subarray](./src/maximum_subarray.py)

TODO.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
- [x] Add unit tests
66
- [ ] Solve 'Same Tree' using recursion (DFS)
77
- [ ] Review other approaches to solving 'Subtree of Another Tree'
8+
- [ ] Add alternative solution for 'Longest Increasing Subsequence'
9+
- [ ] Add binary search solution for 'Longest Increasing Subsequence'

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
# RET505: Unnecessary `elif` after `return` statement
3131
# RUF002: Docstring contains ambiguous `ν`
3232
# RUF003: Docstring contains ambiguous `ν`
33+
# SIM108 Use ternary operator
3334
# T201: `print` found
3435
# W605: invalid escape sequence '\ '
35-
ignore = ["A002", "E741", "N802", "N803", "N806", "N999", "RET505", "RUF002", "RUF003", "T201", "W605"]
36+
ignore = ["A002", "E741", "N802", "N803", "N806", "N999", "RET505", "RUF002", "RUF003", "SIM108", "T201", "W605"]

src/longest_increasing_subsequence.py

Lines changed: 91 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,42 @@
44
https://leetcode.com/problems/longest-increasing-subsequence
55
66
NOTES
7+
* Use dynamic programming (1D) or recursion.
78
8-
My initial thought is this is a 1D dynamic programming problem, since the
9-
problem can be built up from its subproblems.
9+
My initial intuition was this is a dynamic programming (1D) problem, since the
10+
problem can be built up from solutions to its subproblems. However, I was
11+
unable to formulate the correct recurrence relation. I was, however, able to
12+
derive the correct recursive solution.
1013
1114
As an initial approach, let's find the base cases:
1215
13-
1. If nums = [], return 0
14-
2. If nums has the length 1, return 1
16+
1. If nums is empty, return 0
17+
2. If nums has length 1, return 1
1518
1619
Next, we look that a simplified example:
1720
18-
3. If nums = [1, 2], (i + 1) > 1, therefore return 1 + 1
19-
4. If nums = [1, 3, 2, 4], return 1 + lis([3, 2, 4]) (which is 2)
21+
3. If nums = [1, 2],
22+
2 > 1, therefore LIS = 2
23+
24+
4. If nums = [1, 5, 2, 4],
25+
5 > 1, therefore LIS([1,5]) = 2, however, LIS([1, 5, 2, 4]) = 3
26+
27+
How should we account for the fact that the longest increasing subsequence is
28+
formed not by using 5, but instead by using 2 and 4? Programmatically, this
29+
signifies a decision branch that can be resolved recursively:
30+
31+
1. When nums[i] is included in the subsequence
32+
2. When nums[i] is not included in the subsequence
33+
34+
For the first case, we increment the LIS by 1 and update the current largest
35+
element in the subsequence. For the second case, we simply ignore nums[i],
36+
retaining the current largest element in the subsequence. We then take the
37+
maximum of each branch of the decision tree.
38+
39+
if nums[0] > m:
40+
return max(1 + lis(nums[1:], nums[0]), lis(nums[1:], m))
41+
else:
42+
return lis(nums[1:], m)
2043
2144
Realizing a Dynamic Programming Problem
2245
---------------------------------------
@@ -27,21 +50,29 @@
2750
previously made decisions, which is very typical of a problem involving
2851
subsequences.
2952
53+
As we go through the input, each "decision" we must make is simple: is it worth
54+
it to consider this number? If we use a number, it may contribute towards an
55+
increasing subsequence, but it may also eliminate larger elements that came
56+
before it. For example, let's say we have nums = [5, 6, 7, 8, 1, 2, 3]. It
57+
isn't worth using the 1, 2, or 3, since using any of them would eliminate 5, 6,
58+
7, and 8, which form the longest increasing subsequence. We can use dynamic
59+
programming to determine whether an element is worth using or not.
60+
3061
A Framework to Solve Dynamic Programming Problems
3162
-------------------------------------------------
3263
3364
Typically, dynamic programming problems can be solved with three main
3465
components. If you're new to dynamic programming, this might be hard to
35-
understand, but it is extremely valuable to learn, since most dynamic
36-
programming problems can be solved this way.
66+
understand but is extremely valuable to learn since most dynamic programming
67+
problems can be solved this way.
3768
3869
First, we need some function or array that represents the answer to the problem
39-
from a given state. For many solutions on LeetCode, you will see this
40-
function/array named "dp". For this problem, let's say that we have an array
41-
dp. As previously stated, this array needs to represent the answer to the
42-
problem for a given state, so let's say that dp[i] represents the length of the
43-
longest increasing subsequence that ends with the ith element. The "state" is
44-
one-dimensional since it can be represented with only one variablethe index i.
70+
from a given state. Typically, since array is named "dp". For this problem,
71+
let's say that we have an array dp. As just stated, this array needs to
72+
represent the answer to the problem for a given state, so let's say that dp[i]
73+
represents the length of the longest increasing subsequence that ends with the
74+
ith element. The "state" is one-dimensional since it can be represented with
75+
only one variable - the index i.
4576
4677
Second, we need a way to transition between states, such as dp[5] and dp[7].
4778
This is called a recurrence relation and can sometimes be tricky to figure out.
@@ -57,14 +88,6 @@
5788
The third component is the simplest: we need a base case. For this problem, we
5889
can initialize every element of dp to 1, since every element on its own is
5990
technically an increasing subsequence.
60-
61-
NOTE: I got close when trying to solve this with recursion and memoization,
62-
however my solutions failed the following test case:
63-
64-
[3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12]
65-
66-
ChatGippity could not solve it either, so this can be a future problem to
67-
return to...
6891
"""
6992

7093
import sys
@@ -84,62 +107,71 @@ def lengthOfLIS(self, nums: list[int]) -> int:
84107

85108
class MemoizationSolution:
86109
"""
87-
Similar to the top-down approach of the recursive solution, but uses
88-
memoization to store previous calculations.
110+
Similar to the recursive solution, but uses memoization to store previous
111+
calculations. This one is a little bit tricky, since the memoization lookup
112+
uses both the index and m, the current largest element in the subsequence.
89113
90-
NOTE: This solution fails the following test case:
91-
92-
[3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12] -> 5 == 3
114+
NOTE: Though *technically* correct, this solution still exceeds the time
115+
limit.
93116
"""
94117

95118
def lengthOfLIS(self, nums: list[int]) -> int:
96-
# Initialize the memoization table. Each index in the table is the
97-
# calculated longest increasing subsequence up to and including the
98-
# current index.
99-
memo: list[int] = [1] * len(nums)
100-
101-
def lis(nums: list[int], m: int, memo) -> int:
102-
if len(nums) == 1:
103-
return 1 if nums[0] > m else 0
119+
# Initialize the memoization table. Each key in the table is formed
120+
# using an index in nums and the current largest element in the
121+
# subsequence. The stored value is the current longest increasing
122+
# subsequence for the index.
123+
#
124+
# One effect of the using the index and current largest element for the
125+
# key is that it actually lets us handle overlapping subsequences
126+
# correctly, since we're caching results for each unique combination of
127+
# remaining numbers and minimum value.
128+
memo: dict[tuple[int, int], int] = {}
129+
130+
def lis(nums: list[int], i: int, m: int) -> int:
131+
if i >= len(nums):
132+
return 0
133+
key = (i, m)
134+
if key in memo:
135+
return memo[key]
104136
# Calculate the result of each decision:
105137
#
106138
# 1. nums[i] is included in the subsequence
107-
# 2. nums[i] is *not* included in the subsequence
108-
i = len(memo) - len(nums)
109-
if memo[i] > 1:
110-
return memo[i]
111-
if nums[0] > m:
112-
memo[i] = max(1 + lis(nums[1:], nums[0], memo), lis(nums[1:], m, memo))
139+
# 2. nums[i] is not included in the subsequence
140+
#
141+
# nums[i] becomes the current largest element in the subsequence.
142+
if nums[i] > m:
143+
return max(lis(nums, i + 1, nums[i]) + 1, lis(nums, i + 1, m))
113144
else:
114-
memo[i] = lis(nums[1:], m, memo)
115-
return memo[i]
145+
res = lis(nums, i + 1, m)
146+
memo[key] = res
147+
return res
116148

117-
# NOTE: -10^4 <= nums[i] <= 10^4
118-
return lis(nums, -sys.maxsize, memo)
149+
return lis(nums, 0, -sys.maxsize)
119150

120151

121152
class RecursiveSolution:
122153
"""
123-
NOTE: This solution exceeds the time limit.
154+
The recursive solution to Longest Increasing Subsequence uses the intuition
155+
that an element should be added to the subsequence if it is greater than
156+
the current largest element in the subsequence.
124157
125-
NOTE: This solution fails the following test case:
126-
127-
[3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12] -> 6 == 3
158+
NOTE: Though *technically* correct, this solution exceeds the time limit,
159+
since it does not account for overlapping subproblems.
128160
"""
129161

130162
def lengthOfLIS(self, nums: list[int]) -> int:
131-
def lis(nums: list[int], m: int) -> int:
132-
if len(nums) == 1:
133-
return 1 if nums[0] > m else 0
134-
163+
def lis(nums: list[int], i: int, m: int) -> int:
164+
if i >= len(nums):
165+
return 0
135166
# Calculate the result of each decision:
136167
#
137168
# 1. nums[i] is included in the subsequence
138-
# 2. nums[i] is *not* included in the subsequence
139-
if nums[0] > m:
140-
return max(1 + lis(nums[1:], nums[0]), lis(nums[1:], m))
169+
# 2. nums[i] is not included in the subsequence
170+
#
171+
# nums[i] becomes the current largest element in the subsequence.
172+
if nums[i] > m:
173+
return max(lis(nums, i + 1, nums[i]) + 1, lis(nums, i + 1, m))
141174
else:
142-
return lis(nums[1:], m)
175+
return lis(nums, i + 1, m)
143176

144-
# NOTE: -10^4 <= nums[i] <= 10^4
145-
return lis(nums, -sys.maxsize)
177+
return lis(nums, 0, -sys.maxsize)

tests/test_longest_increasing_subsequence.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,29 @@
66

77
from unittest import TestCase
88

9-
from src.longest_increasing_subsequence import MemoizationSolution, RecursiveSolution
9+
from src.longest_increasing_subsequence import MemoizationSolution, RecursiveSolution, Solution
1010

1111

1212
class TestSolution(TestCase):
1313
def test_1(self):
1414
exp = 4
15-
assert MemoizationSolution().lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]) == exp
15+
assert Solution().lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]) == exp
1616

1717
def test_2(self):
1818
exp = 4
19-
assert MemoizationSolution().lengthOfLIS([0, 1, 0, 3, 2, 3]) == exp
19+
assert Solution().lengthOfLIS([0, 1, 0, 3, 2, 3]) == exp
2020

2121
def test_3(self):
2222
exp = 1
23-
assert MemoizationSolution().lengthOfLIS([7, 7, 7, 7, 7, 7, 7]) == exp
23+
assert Solution().lengthOfLIS([7, 7, 7, 7, 7, 7, 7]) == exp
2424

2525
def test_4(self):
2626
exp = 3
27-
assert MemoizationSolution().lengthOfLIS([4, 10, 4, 3, 8, 9]) == exp
27+
assert Solution().lengthOfLIS([4, 10, 4, 3, 8, 9]) == exp
28+
29+
def test_5(self):
30+
exp = 6
31+
assert Solution().lengthOfLIS([3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12]) == exp
2832

2933

3034
class TestMemoizationSolution(TestCase):
@@ -44,13 +48,9 @@ def test_4(self):
4448
exp = 3
4549
assert MemoizationSolution().lengthOfLIS([4, 10, 4, 3, 8, 9]) == exp
4650

47-
# NOTE: Implementation fails the following testcase:
48-
# def test_5(self):
49-
# exp = 3
50-
# assert (
51-
# MemoizationSolution().lengthOfLIS([3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12])
52-
# == exp
53-
# )
51+
def test_5(self):
52+
exp = 6
53+
assert MemoizationSolution().lengthOfLIS([3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12]) == exp
5454

5555

5656
class TestRecursiveSolution(TestCase):
@@ -70,9 +70,6 @@ def test_4(self):
7070
exp = 3
7171
assert RecursiveSolution().lengthOfLIS([4, 10, 4, 3, 8, 9]) == exp
7272

73-
# NOTE: Implementation fails the following testcase:
74-
# def test_5(self):
75-
# exp = 3
76-
# assert (
77-
# RecursiveSolution().lengthOfLIS([3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12]) == exp
78-
# )
73+
def test_5(self):
74+
exp = 6
75+
assert RecursiveSolution().lengthOfLIS([3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12]) == exp

0 commit comments

Comments
 (0)