300. Longest Increasing Subsequence
Description
Difficulty: Medium
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
- -104 <= nums[i] <= 104
Follow up: Can you come up with an algorithm that runs in O(n log(n)) time complexity?
Solution
1. Dynamic Programming
The idea is Dynamic Programming (DP), specifically a bottom-up tabulation approach. The fundamental principle here is to break down the complex problem of finding the LIS into smaller, overlapping subproblems and then build up the solution from these subproblems.
Subproblem Definition:
dp[i]is defined as the length of the Longest Increasing Subsequence that ends withnums[i]. This is a crucial definition, as it allows us to build upon previous calculations.Base Case / Initialization: Each number
nums[i]by itself forms an increasing subsequence of length 1. Therefore,dp[i]for alliis initially set to1. This provides the base values from which longer subsequences can be constructed.Building Up (State Transition): For each
nums[i](iterating from left to right):We look at all preceding numbers
nums[j](wherej < i).If
nums[j] < nums[i], it meansnums[i]can be appended to an increasing subsequence ending atnums[j].In this case, a potential length for the LIS ending at
nums[i]would bedp[j] + 1(the length of the LIS ending atnums[j]plusnums[i]itself).We take the maximum of all such
dp[j] + 1values anddp[i]'s current value (which is at least 1) to updatedp[i]. This ensuresdp[i]always stores the maximum possible LIS ending atnums[i].
Overall Result: As we compute each
dp[i], we keep track of themaxvalue encountered in the entiredparray. Thismaxvariable ultimately holds the length of the longest increasing subsequence found anywhere in thenumsarray, because the LIS must end at somenums[i].
2. Binary Search
Binary Search's Role in O(NlogN) LIS In the O(NlogN) solution for the Longest Increasing Subsequence problem, binary search isn't used to find the LIS itself, nor is it looking for a specific number in the input array. Instead, it's used to efficiently maintain and update a special auxiliary array, often called tails (or dp).
Here's the core idea:
- The tails Array: We maintain an array tails where tails[i] stores the smallest tail (last element) among all increasing subsequences of length i+1. This tails array itself will always be sorted in increasing order.
- Iterating Through nums: For each num in the input array nums:
- Binary Search for Placement: We perform a binary search on the tails array to find the first element that is greater than or equal to num.
- Two Scenarios:
- If num is greater than all elements in tails: This means num can extend the longest increasing subsequence we've found so far. We append num to the tails array, effectively increasing the length of our LIS.
- If num is less than or equal to an element in tails (at index i): This means num can form an increasing subsequence of length i+1 with a smaller ending element than tails[i] currently holds. We replace tails[i] with num. Why is this beneficial? Because a smaller ending element for a subsequence of the same length creates more opportunities for subsequent numbers to form even longer increasing subsequences. It's like saying, "I can achieve this length, but now I'm doing it with a better (smaller) last number."
- The Result: The final length of the tails array (or the counter we use to track its size) will be the length of the Longest Increasing Subsequence.
In essence, the binary search efficiently helps us to either extend our current "best" LIS or optimize an existing one by finding a smaller tail for a given length. This allows us to process each number in O(logN) time, leading to the overall O(NlogN) complexity.
Code
1. Dynamic Programming
import java.util.Arrays;
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[2501];
Arrays.fill(dp, 1);
int max = 1;
for (int i = 0; i < nums.length; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
}
if (dp[i] > max) {
max = dp[i];
}
}
return max;
}
}Time Complexity: O(N2)
The code involves two nested loops. The outer loop iterates
Ntimes (forifrom0tonums.length - 1). The inner loop iterates up toitimes (forjfrom0toi - 1).Inside the loops, operations like comparison and
Math.maxare constant time.Therefore, the total number of operations is proportional to
N * N.
Space Complexity: O(N)
- An array
dpof sizenums.length(or2501which effectively scales with the maximumN) is used to store the intermediate results. This space requirement grows linearly with the input size.
2. Binary Search
import java.util.Arrays;
class Solution {
public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int len = 0;
for (int num : nums) {
int i = Arrays.binarySearch(tails, 0, len, num);
if (i < 0) {
i = -(i + 1);
}
tails[i] = num;
if (i == len) {
len++;
}
}
return len;
}
}Time Complexity: O(NlogN)
Space Complexity: O(N)
