DSAPrep
Medium1-D DP

Longest Increasing Subsequence

Given an integer array nums, return the length of the longest strictly increasing subsequence.

Example 1

Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Example 2

Input: nums = [0,1,0,3,2,3]
Output: 4

Example 3

Input: nums = [7,7,7,7,7,7,7]
Output: 1

Constraints

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 10^4
Follow-up: Can you come up with an algorithm that runs in O(n log(n)) time complexity?
View original on LeetCode ↗

Define dp[i] as the length of the longest increasing subsequence that ends exactly at index i. Any such subsequence either is just nums[i] alone (length 1), or extends some earlier increasing subsequence ending at a smaller value: for every j < i with nums[j] < nums[i], dp[i] could be dp[j] + 1. Taking the best such extension gives the recurrence, and the overall answer is the max over all dp[i].

Brute Force Recursion

Time O(2ⁿ)Space O(n)

Define lengthEndingAt(i) recursively: it is 1 plus the best lengthEndingAt(j) over all valid earlier indices j < i with nums[j] < nums[i], or just 1 if there is no such j.

class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
n = len(nums)
def lengthEndingAt(i: int) -> int:
best = 1
for j in range(i):
if nums[j] < nums[i]:
best = max(best, 1 + lengthEndingAt(j))
return best
return max(lengthEndingAt(i) for i in range(n))

Each call can recurse into up to i earlier calls, and those overlap heavily across different starting indices, giving exponential O(2ⁿ) time in the worst case, O(n) recursion depth.

Bottom-Up DP

Time O(n²)Space O(n)

Fill dp[0..n-1] left to right, each starting at 1 (the subsequence containing just nums[i]), then scanning every earlier index j for a smaller value to extend.

class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
n = len(nums)
dp = [1] * n
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)

Trace for nums = [10, 9, 2, 5, 3, 7, 101, 18] (only the winning predecessor is shown as a pointer where one exists):

1
0
·
1
·
2
·
3
·
4
·
5
·
6
·
7
1 / 8
comparingresult

dp[0] = 1: "10" alone. No earlier elements to extend from.

For each of the n indices, up to n earlier indices are checked → O(n²) time, O(n) space for the dp array.

Patience Sorting (Binary Search)

OptimalTime O(n log n)Space O(n)

Maintain an auxiliary array tails, where tails[k] is the smallest possible tail value of any increasing subsequence of length k + 1 seen so far. For each new number, binary search tails for the first entry >= num: if found, replace it (a smaller tail keeps future extensions easier); if not found, num extends the longest subsequence so far, so append it. The final length of tails is the answer – note tails itself does not necessarily hold an actual valid subsequence, only correct lengths.

from bisect import bisect_left
class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
tails = []
for num in nums:
idx = bisect_left(tails, num)
if idx == len(tails):
tails.append(num)
else:
tails[idx] = num
return len(tails)

Walking through nums = [10, 9, 2, 5, 3, 7, 101, 18]: tails evolves as [10][9] (10 replaced, since 9 is a smaller tail for length-1) → [2][2,5][2,3] (5 replaced by 3, a smaller tail for length-2) → [2,3,7][2,3,7,101][2,3,7,18] (101 replaced by 18). Final length is 4, matching the expected answer, even though [2,3,7,18] itself is not the actual LIS found earlier.

Each of the n numbers does one binary search (and possibly one write) over an array of size at most nO(n log n) time, O(n) space for tails. This meets the problem’s follow-up requirement and is the best known complexity for this problem.