Problem overview
The method lengthOfLIS(nums: Array[Int]): Int returns the length of the longest strictly increasing subsequence of nums. A subsequence is what remains after deleting some elements, possibly none, without changing the order of the rest. It is strictly increasing when 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 = Array(10, 9, 2, 5, 3, 7, 101, 18) the answer is 4. The subsequence [2, 3, 7, 101] works, and so does [2, 5, 7, 18]. No increasing subsequence has 5 elements. For Array(0, 1, 0, 3, 2, 3) the answer is also 4, from [0, 1, 2, 3]. For seven 7s the answer is 1, because equal values do not increase.
Approach: the best subsequence that ends at each index
Trying every subsequence is hopeless: 2500 elements have 2^2500 of them. The lesson instead solves a smaller question for each index. Let dp(i) be the length of the longest strictly increasing subsequence that ends with nums(i). Every dp(i) is at least 1, because the element alone is such a subsequence.
The table is filled from left to right. For every earlier index j < i with nums(j) < nums(i), the element nums(i) can be added to the end of the best subsequence that ends at j, which gives length dp(j) + 1. So dp(i) is 1 plus the largest such dp(j), or just 1 when no earlier element is smaller.
The longest subsequence can end anywhere, so the answer is the largest dp(i), not necessarily the last one.
For Array(10, 9, 2, 5, 3, 7, 101, 18) the table is:
1 1 1 2 2 3 4 4
The 5 at index 3 extends the subsequence ending in 2, so its entry is 2. The 7 at index 5 extends either the 5 or the 3, both with length 2, so its entry is 3. The 101 gives 4, and so does the 18. For Array(0, 1, 0, 3, 2, 3) the table is 1 2 1 3 3 4, and the maximum is again 4.
The statement mentions a faster O(n log n) method. It keeps, for each length, the smallest value an increasing subsequence of that length can end with, and places every element in that list with a binary search. This lesson practises the table version.
Step by step
- Store the length of
numsinn. Both loops run over indexes ofnums. - Create
dpwithArray.fill(n)(1):nones. No entry is ever below 1, so starting at 1 covers the "element alone" case. - Create
bestas avarstarting at 0. It changes as the loop goes, so it cannot be aval. - Loop over every index
iwith0 until n. Eachdp(i)needs only entries before it, and they are done by the time the loop reachesi. - Loop over every earlier index
jwith0 until i. Fori = 0there are none and the loop does not run. - Check
nums(j) < nums(i). The test is strict: an equal value cannot follow, so<=would be wrong. - Update
dp(i)tomath.max(dp(i), dp(j) + 1). Over all validj, this keeps the best extension. - Update
bestwithmath.max(best, dp(i))after the inner loop, whendp(i)is final. - Return
bestas the last expression.
The solution in Scala
class Solution {
def lengthOfLIS(nums: Array[Int]): Int = {
val n = nums.length
val dp = Array.fill(n)(1)
var best = 0
for (i <- 0 until n) {
for (j <- 0 until i) {
if (nums(j) < nums(i)) {
dp(i) = math.max(dp(i), dp(j) + 1)
}
}
best = math.max(best, dp(i))
}
best
}
}
The Scala details worth a closer look:
Array.fill(n)(1)builds an array ofnelements, each the value in the second parentheses. The array is mutable, sodp(i) = ...changes it in place. The binding itself is aval: the namedpalways refers to the same array, but its contents can change.0 until nstops beforen. It is the right range for indexes.1 to nwould run one past the end of the array.math.maxtakes twoInts and returns the larger one.valandvar.nanddpnever get a new value, whilebestdoes.- The last line is the result.
bestalone, with noreturn, is what the method gives back.
Complexity
Time: O(n²). The two loops look at each pair j < i once, which is n · (n - 1) / 2 pairs. For the largest input, 2500 elements, that is about 3.1 million comparisons.
Space: O(n). The table dp has one entry per element. Everything else is a few integers.
Edge cases and common mistakes
- Returning the last
dpentry. ForArray(10, 9, 2, 5, 3, 7, 101, 18)the last entry and the maximum happen to match, but forArray(1, 2, 3, 0)the table is1 2 3 1: the best subsequence,[1, 2, 3], ends at index 2, and the last entry is 1. That is whybesttracks the maximum. - Using
<=instead of<. Equal values would then count as increasing. For seven 7s the table would grow to 1, 2, ..., 7 and the method would return 7 instead of 1. - Starting
dpat zeros. Every subsequence would come out one too short, and an array with no increasing pair would return 0 instead of 1. - Starting
bestat the wrong value. Starting at 0 is safe because the input has at least one element and everydp(i)is at least 1. - A decreasing array.
Array(5, 4, 3, 2, 1)gives1: the table is all ones. - A single element.
Array(5)gives1. The inner loop never runs. - Confusing subsequence with substring. A subsequence may skip elements. In
Array(4, 10, 4, 3, 8, 9)the answer is 3, from[4, 8, 9]or[3, 8, 9], even though those values are not neighbours.
Testing
Array(10, 9, 2, 5, 3, 7, 101, 18)gives4.Array(0, 1, 0, 3, 2, 3)gives4.Array(7, 7, 7, 7, 7, 7, 7)gives1.Array(5)gives1.Array(5, 4, 3, 2, 1)gives1.Array(1, 2, 3, 4, 5)gives5.Array(4, 10, 4, 3, 8, 9)gives3.
