Problem overview
LengthOfLIS receives an int[] named nums and returns the length of its longest strictly increasing subsequence. A subsequence keeps some elements, possibly all or none removed, in their original order. Strictly increasing means every element is larger than the one before it, so equal values cannot both be used.
For {10, 9, 2, 5, 3, 7, 101, 18} the answer is 4. One such subsequence is 2, 3, 7, 101, and 2, 5, 7, 18 is another. For {7, 7, 7, 7, 7, 7, 7} the answer is 1, because equal values do not increase.
Approach: best subsequence ending at each index
Checking every subsequence is hopeless: 2500 elements have an astronomical number of them (2 to the power 2500) of them. Define a smaller problem instead: dp[i] is the length of the longest strictly increasing subsequence that ends with nums[i]. Every dp[i] is at least 1, since the element alone is such a subsequence.
To compute dp[i], look at every earlier index j. If nums[j] < nums[i], then nums[i] can be appended to the best subsequence that ends at j, giving length dp[j] + 1. Keep the largest such value. If no earlier element is smaller, dp[i] stays 1.
The answer is the largest dp[i], not the last one: the best subsequence can end anywhere. For {10, 9, 2, 5, 3, 7, 101, 18} the table is:
dp = [1,1,1,2,2,3,4,4]
The 3 at index 4 ends a subsequence of length 2 (2, 3), the 7 ends one of length 3 (2, 3, 7), and both 101 and 18 end ones of length 4. The maximum is 4.
This lesson uses this simple table; a faster method is described after the complexity section.
Step by step
- Store
nums.Lengthinn. Both loops run over the indexes ofnums. - Create
dpasnew int[n]and fill it with 1 usingArray.Fill(dp, 1). A newintarray holds zeros, but no entry should be below 1. - Create
best, starting at 0. It tracks the largestdp[i]seen. - Loop
ifrom 0 ton - 1. Eachdp[i]needs only entries before it, and those are final by then. - Loop
jfrom 0 toi - 1. Only elements beforenums[i]can precede it. Fori = 0the loop does not run. - Check
nums[j] < nums[i]. It is<, not<=, because the subsequence must be strictly increasing. - Update
dp[i]withMath.Max(dp[i], dp[j] + 1). - After the inner loop, update
bestwithMath.Max(best, dp[i]). By thendp[i]has seen everyj. - Return
bestafter the outer loop.
The solution in C#
class Solution {
public int LengthOfLIS(int[] nums) {
int n = nums.Length;
int[] dp = new int[n];
Array.Fill(dp, 1);
int best = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.Max(dp[i], dp[j] + 1);
}
}
best = Math.Max(best, dp[i]);
}
return best;
}
}
The C# details worth knowing:
Array.Fill(dp, 1)sets every element of an array to one value. It lives in theSystemnamespace, likeMath.Max.- An
int[]is a fixed-length array whose elements start at 0. Without the fill, everydp[i]would be one too small. Math.Max(a, b)returns the larger of two numbers; there is no need for anif.
Complexity
Time: O(n^2). 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 three million comparisons.
Space: O(n) for the dp array.
A faster way: O(n log n)
Keep a list tails in which entry k is the smallest value that an increasing subsequence of length k + 1 can end with. The list is always sorted, so each new element finds its place by binary search: if it is larger than everything, it extends the list by one (a longer subsequence exists); otherwise it replaces the first entry that is at least as large, which keeps that length's ending value as small as possible. The answer is the length of the list.
public int LengthOfLIS(int[] nums) {
List<int> tails = new List<int>();
foreach (int x in nums) {
int pos = tails.BinarySearch(x);
if (pos < 0) pos = ~pos;
if (pos == tails.Count) tails.Add(x); else tails[pos] = x;
}
return tails.Count;
}
List<T>.BinarySearch returns the index of an equal element if there is one, and otherwise the bitwise complement (~) of the index where the value would be inserted. The line if (pos < 0) pos = ~pos; turns both cases into a position. Because an equal element is replaced by itself, equal values never lengthen the list, which keeps the subsequence strictly increasing. On {10, 9, 2, 5, 3, 7, 101, 18} the list evolves like this:
10 -> [10]
9 -> [9]
2 -> [2]
5 -> [2,5]
3 -> [2,3]
7 -> [2,3,7]
101 -> [2,3,7,101]
18 -> [2,3,7,18]
Only the length of the list is the answer: in general its entries can come from different subsequences, so the list itself need not be one. Here its length, 4, is right. For the other test inputs it returns 4, 1, 1 and 3, matching the table method.
Edge cases and common mistakes
- One element.
{5}returns 1. The inner loop never runs, sodp[0]stays 1, andbestbecomes 1. - Decreasing input.
{4, 3, 2, 1}returns 1: no element is smaller than a later one. - Already increasing.
{1, 2, 3, 4}returns 4. - Equal values.
{7, 7, 7, 7, 7, 7, 7}returns 1. Using<=would return 7. - Returning
dp[n - 1]. For{10, 9, 2, 5, 3, 7, 101, 18}that happens to be 4, but for{1, 2, 3, 0}the last entry is 1 while the answer is 3. Take the maximum. - Subsequence versus substring. The chosen elements need not be next to each other.
Testing
{10, 9, 2, 5, 3, 7, 101, 18}gives4.{0, 1, 0, 3, 2, 3}gives4.{7, 7, 7, 7, 7, 7, 7}gives1.{5}gives1.{4, 3, 2, 1}gives1.{1, 2, 3, 4}gives4.
