Problem overview
The function lengthOfLIS receives a slice of integers, nums, and returns the length of its longest strictly increasing subsequence. A subsequence is what remains after deleting some elements, possibly none, without reordering the rest. "Strictly increasing" means every element is larger than the one before it, so two equal values can never both be in it. Only the length is returned, not the subsequence itself.
For nums = [10, 9, 2, 5, 3, 7, 101, 18] the answer is 4. One such subsequence is [2, 3, 7, 101], another is [2, 5, 7, 18], and none has five elements. For [0, 1, 0, 3, 2, 3] the answer is 4 ([0, 1, 2, 3]), and for [7, 7, 7, 7, 7, 7, 7] it is 1, because equal values don't increase.
The lesson asks for O(n²) time and O(n) extra space, where n is len(nums).
Approach: the best subsequence ending at each index
Trying every subsequence is out of reach: a slice of 2500 elements has 2 to the power 2500 of them. Instead, solve smaller versions of the problem and reuse their answers.
Let dp[i] be the length of the longest strictly increasing subsequence that ends with nums[i]. Fixing the last element is what makes the pieces fit together:
- Every
dp[i]is at least 1, becausenums[i]alone is such a subsequence. - For every earlier index
j < iwithnums[j] < nums[i], the elementnums[i]can be placed after the best subsequence that ends atj. That gives lengthdp[j] + 1. - So
dp[i]is 1 plus the largestdp[j]among thosej, or just 1 when no earlier element is smaller.
The longest subsequence of the whole slice can end anywhere, so the answer is the largest dp[i], not the last one.
A faster O(n log n) method exists. It keeps, for each length, the smallest value an increasing subsequence of that length can end with, and places each element in that list with a binary search. This lesson practises the table version.
Step by step
- Store the length in
n. Both loops run over the indexes ofnums. - Create
dpwithmake([]int, n). A new slice of ints is full of zeros, so the starting values must be set. - Loop over
dpwithfor i := range dp. With one variable,rangegives the indexes. - Set every
dp[i]to 1. A single element is an increasing subsequence of length 1, so no entry is ever below 1. - Create
best, starting at 0. It holds the longest length found so far. - Loop over
ifrom 0 ton - 1. Eachdp[i]needs only entries to its left, which are final by the time the loop gets toi. - Loop over every earlier index
jfrom 0 up toi - 1. Fori = 0there are none, and the loop doesn't run. - Test
nums[j] < nums[i]. It must be<and not<=, because an equal value can't follow. - Update
dp[i]withmax(dp[i], dp[j]+1). Keeping the largest value over all validjgives the answer fori. - Update
bestwithmax(best, dp[i]), after the inner loop and still inside the outer one. At that pointdp[i]has seen everyj. - Return
bestonce the outer loop is done.
The solution in Go
func lengthOfLIS(nums []int) int {
n := len(nums)
dp := make([]int, n)
for i := range dp {
dp[i] = 1
}
best := 0
for i := 0; i < n; i++ {
for j := 0; j < i; j++ {
if nums[j] < nums[i] {
dp[i] = max(dp[i], dp[j]+1)
}
}
best = max(best, dp[i])
}
return best
}
The Go details worth a closer look:
- The built-in
max. Since Go 1.21,maxandminare built into the language and take two or more values of the same ordered type. Older versions needed a hand-written helper; Go 1.22 does not. for i := range dploops over the indexes, 0 tolen(dp) - 1. The value is not needed here, so onlyiis named.- Zero values.
make([]int, n)gives zeros, which is why the first loop sets everything to 1 before any comparison reads the table. - Two ways to write a counted loop. The first loop uses
rangebecause it only needs every index. The nested loops use the three-partforbecause the inner bound,j < i, depends on the outer variable.
Complexity
Time: O(n²). The two loops look at each pair j < i once, which is n · (n - 1) / 2 pairs. With n up to 2500 that is about 3 million comparisons.
Space: O(n). The table dp has one entry per element, and the other variables are single numbers.
Edge cases and common mistakes
- Using
<=. It counts equal values as increasing. On[7, 7, 7, 7, 7, 7, 7]that version returns 7 instead of 1. - Returning
dp[n-1]. That is the best subsequence ending at the last element, not the best overall. On[1, 2, 3, 0]the last entry is 1, while the answer is 3. Keepingbestavoids the problem. - Starting
dpat 0. Then an element with no smaller element before it would count as a subsequence of length 0, and a one-element slice would return 0. - Confusing a subsequence with a substring. The chosen elements don't have to be next to each other. In
[0, 1, 0, 3, 2, 3]the answer uses indexes 0, 1, 4 and 5. - Descending input. No pair satisfies
nums[j] < nums[i], so every entry stays 1 and the answer is 1.
Testing
Printing nums and then dp for [10, 9, 2, 5, 3, 7, 101, 18] gives:
[10 9 2 5 3 7 101 18] [1 1 1 2 2 3 4 4]
Each value in the second list is the best length ending at the number in the same position of the first. For example, dp[5] is 3 for nums[5] = 7: the subsequence [2, 5, 7] or [2, 3, 7] ends there.
The largest entry is 4. For [0, 1, 0, 3, 2, 3] the table is [1 2 1 3 3 4], and again the answer is the largest entry. More inputs to try:
[5]gives1.[5, 4, 3, 2, 1]gives1.[1, 2, 3, 4]gives4.[1, 2, 3, 0]gives3.[7, 7, 7, 7, 7, 7, 7]gives1.
