4
4
https://leetcode.com/problems/longest-increasing-subsequence
5
5
6
6
NOTES
7
+ * Use dynamic programming (1D) or recursion.
7
8
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.
10
13
11
14
As an initial approach, let's find the base cases:
12
15
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
15
18
16
19
Next, we look that a simplified example:
17
20
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)
20
43
21
44
Realizing a Dynamic Programming Problem
22
45
---------------------------------------
27
50
previously made decisions, which is very typical of a problem involving
28
51
subsequences.
29
52
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
+
30
61
A Framework to Solve Dynamic Programming Problems
31
62
-------------------------------------------------
32
63
33
64
Typically, dynamic programming problems can be solved with three main
34
65
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.
37
68
38
69
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 variable– the 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.
45
76
46
77
Second, we need a way to transition between states, such as dp[5] and dp[7].
47
78
This is called a recurrence relation and can sometimes be tricky to figure out.
57
88
The third component is the simplest: we need a base case. For this problem, we
58
89
can initialize every element of dp to 1, since every element on its own is
59
90
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...
68
91
"""
69
92
70
93
import sys
@@ -84,62 +107,71 @@ def lengthOfLIS(self, nums: list[int]) -> int:
84
107
85
108
class MemoizationSolution :
86
109
"""
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.
89
113
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.
93
116
"""
94
117
95
118
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 ]
104
136
# Calculate the result of each decision:
105
137
#
106
138
# 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 ))
113
144
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
116
148
117
- # NOTE: -10^4 <= nums[i] <= 10^4
118
- return lis (nums , - sys .maxsize , memo )
149
+ return lis (nums , 0 , - sys .maxsize )
119
150
120
151
121
152
class RecursiveSolution :
122
153
"""
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.
124
157
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.
128
160
"""
129
161
130
162
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
135
166
# Calculate the result of each decision:
136
167
#
137
168
# 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 ))
141
174
else :
142
- return lis (nums [ 1 :] , m )
175
+ return lis (nums , i + 1 , m )
143
176
144
- # NOTE: -10^4 <= nums[i] <= 10^4
145
- return lis (nums , - sys .maxsize )
177
+ return lis (nums , 0 , - sys .maxsize )
0 commit comments