Longest Increasing Subsequence (LIS)
The Longest Increasing Subsequence (LIS) problem is a foundational paradigm in sequence optimization and dynamic programming. The objective is formalized as follows:
Given an integer array of length , determine the maximum length of a subsequence such that all elements are sorted in a strictly increasing order.
A subsequence is derived by deleting zero or more elements from the original array while preserving the relative spatial ordering of the remaining elements.
Formalized Example
Consider the sequence:
Valid strictly increasing subsequences include:
Optimal Evaluation:
Video Explanation

Combinatorial Brute Force Intuition
A naive approach evaluates the entire state space of subsequences. At each index , a decision boundary emerges: either include (conditioned on it being strictly greater than the previously selected element) or exclude it.
This generates a state-space tree bounded by the power set of the array, yielding a total of possible subsets. The resulting time complexity is , which is computationally intractable for inputs where . This bottleneck motivates optimization via Dynamic Programming and Greedy/Binary Search strategies.
Approach 1: The Quadratic Dynamic Programming Model
Mathematical Formulation
Let represent the length of the LIS whose terminal element resides precisely at index .
For any given index , we scan all historical states where . If , the element at can structurally extend the optimal subsequence terminating at .
Recurrence Relation
Step-by-Step Execution Matrix
Input Instance:
Initialization:
| Target Index () | Element () | Valid Lookback Constraints Checked () | Candidate Evaluations () | State Mutation () |
|---|---|---|---|---|
| 0 | 3 | None | None | 1 |
| 1 | 10 | 2 | ||
| 2 | 2 | None | 1 | |
| 3 | 1 | None | 1 | |
| 4 | 20 | | 3 |
Production Implementations
#include <iostream>
#include <vector>
#include <algorithm>
int lisQuadratic(const std::vector<int>& arr) {
const size_t n = arr.size();
if (n == 0) return 0;
std::vector<int> dp(n, 1);
for (size_t i = 1; i < n; ++i) {
for (size_t j = 0; j < i; ++j) {
if (arr[j] < arr[i]) {
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
}
return *std::max_element(dp.begin(), dp.end());
}
int main() {
std::vector<int> arr = {10, 9, 2, 5, 3, 7, 101, 18};
std::cout << "LIS Length: " << lisQuadratic(arr) << "\n";
return 0;
}
import java.util.Arrays;
public class LongestIncreasingSubsequence {
public static int lisQuadratic(int[] arr) {
if (arr == null || arr.length == 0) return 0;
int n = arr.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
int maxLIS = 1;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLIS = Math.max(maxLIS, dp[i]);
}
return maxLIS;
}
public static void main(String[] args) {
int[] arr = {10, 9, 2, 5, 3, 7, 101, 18};
System.out.println("LIS Length: " + lisQuadratic(arr));
}
}
from typing import List
def lis_quadratic(arr: List[int]) -> int:
if not arr:
return 0
n = len(arr)
dp = [1] * n
for i in range(1, n):
for j in range(i):
if arr[j] < arr[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
if __name__ == "__main__":
arr = [10, 9, 2, 5, 3, 7, 101, 18]
print(f"LIS Length: {lis_quadratic(arr)}")
function lisQuadratic(arr) {
if (!arr || arr.length === 0) return 0;
const n = arr.length;
const dp = new Array(n).fill(1);
let maxLIS = 1;
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLIS = Math.max(maxLIS, dp[i]);
}
return maxLIS;
}
const arr = [10, 9, 2, 5, 3, 7, 101, 18];
console.log(`LIS Length: ${lisQuadratic(arr)}`);
Approach 2: The Log-Linear Greedy Binary Search Paradigm
Operational Core
To scale past quadratic limits, we optimize retrieval through a dynamic array, . Let be defined as the monotonically minimal tail value found among all encountered increasing subsequences of length .
For each sequential element :
- Append Criterion: If is strictly greater than the current maximum value in (the final array position), append . This signals a structural increment to the global LIS length.
- Substitution Criterion: If is bounded within or below the sequence, find the lowest index such that using binary search. Mutate this position to .
Design Invariant: Updating an entry to a smaller value does not immediately alter the verified max length of the LIS, but optimizing the boundary values lowers the threshold for upcoming elements to build upon the sequence.
Step-by-Step State Mutation Trace
Input Sequence:
| Step () | Current Input () | Initial State Vector () | Algorithmic Routing Engine Decisions | Resulting Vector State () |
|---|---|---|---|---|
| 1 | 10 | [] | Vector empty push | [10] |
| 2 | 9 | [10] | Lower-bound index overwrite with | [9] |
| 3 | 2 | [9] | Lower-bound index overwrite with | [2] |
| 4 | 5 | [2] | append to vector terminal | [2, 5] |
| 5 | 3 | [2, 5] | Lower-bound index overwrite with | [2, 3] |
| 6 | 7 | [2, 3] | append to vector terminal | [2, 3, 7] |
| 7 | 101 | [2, 3, 7] | append to vector terminal | [2, 3, 7, 101] |
| 8 | 18 | [2, 3, 7, 101] | Lower-bound index overwrite with | [2, 3, 7, 18] |
Crucial Invariant Property: The structure acts purely as a tracking mechanism for lengths; it does not track the actual sequence order. Here, the correct length 4 is found, but the true underlying sequence paths are or .
Production Implementations
#include <iostream>
#include <vector>
#include <algorithm>
int lisLogLinear(const std::vector<int>& arr) {
std::vector<int> tails;
tails.reserve(arr.size());
for (const int x : arr) {
auto it = std::lower_bound(tails.begin(), tails.end(), x);
if (it == tails.end()) {
tails.push_back(x);
} else {
*it = x;
}
}
return static_cast<int>(tails.size());
}
int main() {
std::vector<int> arr = {10, 9, 2, 5, 3, 7, 101, 18};
std::cout << "LIS Length: " << lisLogLinear(arr) << "\n";
return 0;
}
import java.util.ArrayList;
import java.util.List;
public class LISOptimized {
private static int executeLowerBound(List<Integer> list, int target) {
int low = 0;
int high = list.size();
while (low < high) {
int mid = low + ((high - low) >>> 1);
if (list.get(mid) < target) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
public static int lisLogLinear(int[] arr) {
if (arr == null || arr.length == 0) return 0;
List<Integer> tails = new ArrayList<>(arr.length);
for (int x : arr) {
int index = executeLowerBound(tails, x);
if (index == tails.size()) {
tails.add(x);
} else {
tails.set(index, x);
}
}
return tails.size();
}
public static void main(String[] args) {
int[] arr = {10, 9, 2, 5, 3, 7, 101, 18};
System.out.println("LIS Length: " + lisLogLinear(arr));
}
}
import bisect
from typing import List
def lis_log_linear(arr: List[int]) -> int:
if not arr:
return 0
tails: List[int] = []
for x in arr:
idx = bisect.bisect_left(tails, x)
if idx == len(tails):
tails.append(x)
else:
tails[idx] = x
return len(tails)
if __name__ == "__main__":
arr = [10, 9, 2, 5, 3, 7, 101, 18]
print(f"LIS Length: {lis_log_linear(arr)}")
function executeLowerBound(arr, target) {
let low = 0;
let high = arr.length;
while (low < high) {
const mid = low + ((high - low) >> 1);
if (arr[mid] < target) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
function lisLogLinear(arr) {
if (!arr || arr.length === 0) return 0;
const tails = [];
for (const x of arr) {
const idx = executeLowerBound(tails, x);
if (idx === tails.length) {
tails.push(x);
} else {
tails[idx] = x;
}
}
return tails.length;
}
const arr = [10, 9, 2, 5, 3, 7, 101, 18];
console.log(`LIS Length: ${lisLogLinear(arr)}`);
Architectural Comparison Matrix
| Dimensional Criteria | Brute Force Pattern | Classical DP Model | Log-Linear Binary Search |
|---|---|---|---|
| Time Complexity | |||
| Space Complexity | |||
| Sequence Recovery | Trivial | Built-in via backtracking pointers | Complex (requires secondary mapping trackers) |
| Application Suitability | Non-viable | Ideal for moderate array scales () | Essential for large systems () |
Reconstructing the Structural Subsequence Path
To retrieve the exact elements that form the sequence rather than just computing its scalar length, we introduce a tracking array into the classical dynamic programming engine. This acts as a reverse pointer system.
from typing import List, Tuple
def reconstruct_lis(arr: List[int]) -> Tuple[int, List[int]]:
if not arr:
return 0, []
n = len(arr)
dp = [1] * n
parent = [-1] * n
for i in range(1, n):
for j in range(i):
if arr[j] < arr[i] and dp[j] + 1 > dp[i]:
dp[i] = dp[j] + 1
parent[i] = j
# Locate the peak of the tracking array
max_len = max(dp)
current_idx = dp.index(max_len)
# Reconstruct back to the initial node tracking points
sequence = []
while current_idx != -1:
sequence.append(arr[current_idx])
current_idx = parent[current_idx]
return max_len, sequence[::-1]
if __name__ == "__main__":
arr = [3, 10, 2, 1, 20]
length, path = reconstruct_lis(arr)
print(f"Verified Max Length: {length}") # Output: 3
print(f"Reconstructed Path: {path}") # Output: [3, 10, 20]
Boundary Edge Cases and Failure Modes
Strict vs. Non-Strict Monotonic Boundaries
- Error: Applying a non-strict inequality operator () instead of a strict less-than constraint ().
- Fix: Ensure execution matches problem specifications. For strict tracking conditions, enforce and use
lower_bound. For non-strict (non-decreasing) sequences, switch to and applyupper_bound.
Global Extrema Extraction Errors
- Error: Assuming that the final entry () contains the total global maximum length.
- Fix: The maximum length sequence can terminate at any point in the array. Always sweep the entire table () or track the running maximum inline.
Degenerate Uniform Sequences
- Error: Code hangs, overflows, or incorrectly increments counts when arrays contain duplicate entries (e.g., ).
- Fix: Enforce clear invariant rules for matching values during binary search steps.
Technical Real-World Engineering Implementations
- Patience Sorting Engines: The computation of minimum pile formations during sorting runs is structurally equivalent to solving the LIS problem.
- Computational Biology (Gene Alignment): Used to isolate matching chromosomal strands or map variations by analyzing parallel long-chain patterns.
- Multidimensional Spatial Layouts: Problems such as the Russian Doll Envelopes challenge (LeetCode #354) or Box Stacking sort data along one dimension (e.g., width) and then apply standard LIS calculations along the remaining dimensions (e.g., height) to find the optimal arrangement.
Done with this topic? Mark it as complete to track your progress.