DSA Longest Increasing Subsequence
The longest increasing subsequence is the longest set of values that rises while keeping the original list order. A subsequence keeps the original order but may skip values.
The problem
The longest increasing subsequence is the longest set of values that rises while keeping the original list order. A subsequence keeps the original order but may skip values.
Each value in the subsequence must be strictly larger than the one before it. Equal values cannot extend a run. The answer is the length of the longest such run.
The cases used in this article
cases = [
([10, 9, 2, 5, 3, 7], 3),
([1, 3, 6, 7, 9, 4, 10, 5, 6], 6),
([7, 7, 7], 1),
([5, 4, 3, 2], 1),
([], 0),
]
for nums, expected in cases:
print("values", nums, "longest length:", expected)The first list hides its best run late. The values 2, 5 and 7 rise in order, skipping everything between them. No four values in that list rise in order.
Intuition: grabbing the next larger value fails
The first idea starts at the first value and grabs each later value that is larger. It never looks back and never restarts. A large early value blocks everything after it.
The greedy grab on the first case
nums = [10, 9, 2, 5, 3, 7]
kept = [nums[0]]
for value in nums[1:]:
if value > kept[-1]:
kept.append(value)
print("greedy run:", kept)
print("greedy length:", len(kept))
print("true longest length:", 3)The greedy run keeps only 10. Starting later at 2 would keep 2, 5 and 7. The first idea cannot know which start is right without trying them all.
Brute force: test every subsequence
The brute force tests every subset of positions. It keeps the list order inside each subset, because a subset of positions is read left to right. It counts a subset only when its values strictly rise.
Enumerating all subsets of six values
nums = [10, 9, 2, 5, 3, 7]
n = len(nums)
checked = 0
best_len = 0
best_run = []
for mask in range(2 ** n):
checked += 1
run = [nums[i] for i in range(n) if mask & (1 << i)]
rising = all(run[k] < run[k + 1] for k in range(len(run) - 1))
if rising and len(run) > best_len:
best_len = len(run)
best_run = run
print("subsets checked:", checked)
print("longest length:", best_len)
print("one longest run:", best_run)Six values give 64 subsets. Each added value doubles the subset count. Ten values give 1,024 subsets, and twenty values give over a million.
The optimal solution: the best run ending at each index
The state is best[i], the length of the longest rising run that ends exactly at index i. That one sentence is the whole method.
Each index looks back at every earlier index. An earlier smaller value offers its best length plus one. The index keeps the largest offer, or 1 when no earlier value is smaller.
The ending-here table on every case
def lis_length(nums):
if not nums:
return 0
best = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i]:
best[i] = max(best[i], best[j] + 1)
return max(best)
cases = [
([10, 9, 2, 5, 3, 7], 3),
([1, 3, 6, 7, 9, 4, 10, 5, 6], 6),
([7, 7, 7], 1),
([5, 4, 3, 2], 1),
([], 0),
]
for nums, expected in cases:
result = lis_length(nums)
print("values", nums, "->", result, "expected:", expected, "match:", result == expected)All five cases match, including the empty list at length 0. Pair comparisons replace the subset explosion. The answer is the largest table entry, because the best run may end at any index.
Walkthrough: the table for [10, 9, 2, 5, 3, 7]
The walk fills the table one index at a time. Each line prints the table up to the index just processed. A parent table records which earlier index gave each best length.
The best table after each index
nums = [10, 9, 2, 5, 3, 7]
best = [1] * len(nums)
parent = [-1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i] and best[j] + 1 > best[i]:
best[i] = best[j] + 1
parent[i] = j
print("after index", i, "value", nums[i], "table:", best[:i + 1])
end = best.index(max(best))
run = []
while end != -1:
run.append(nums[end])
end = parent[end]
run.reverse()
print("longest length:", max(best))
print("recovered run:", run)The largest entry is 3, at the last index. The parent links walk back from 7 through 5 to 2. The recovered run is read straight off those links.
Complexity
Time is O(n squared). Each index compares against every earlier index, and the pair count is n times n minus 1, divided by 2.
Space is O(n). The best table and the parent table each hold one entry per value.
Counting the pair comparisons
def count_comparisons(n):
nums = list(range(n))
best = [1] * n
comparisons = 0
for i in range(n):
for j in range(i):
comparisons += 1
if nums[j] < nums[i]:
best[i] = max(best[i], best[j] + 1)
return comparisons
for n in [5, 10, 20]:
print("values", n, "- comparisons:", count_comparisons(n))Doubling the list roughly quadruples the comparisons. That growth is the price of checking every pair. It is still far below the doubling subset count of the brute force.
Edge cases and pitfalls
An empty list has no run at all, so its length is 0. A falling list and a list of equal values both answer 1, from any single value.
Edge cases, run
def lis_length(nums):
if not nums:
return 0
best = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i]:
best[i] = max(best[i], best[j] + 1)
return max(best)
print("empty list:", lis_length([]))
print("one value:", lis_length([4]))
print("falling list:", lis_length([5, 4, 3, 2]))
print("already rising:", lis_length([1, 2, 3, 4]))
print("equal values:", lis_length([7, 7, 7]))The classic mistake lets an equal value extend a run. Swapping the strict comparison for a loose one invents runs that the problem forbids.
The loose comparison, run
def lis_loose(nums):
best = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] <= nums[i]:
best[i] = max(best[i], best[j] + 1)
return max(best)
print("loose count for [7, 7, 7]:", lis_loose([7, 7, 7]))
print("strict count for [7, 7, 7]:", 1)Strictly larger is the whole rule, and the comparison sign carries it. A faster patience-based method for this problem exists, and it belongs to a later course.
Exercise
A best-ending-here table reads [1, 2, 1, 3]. What is the length of the longest increasing subsequence?