1058-longest-increasing-subseq — Longest Increasing Subsequence
Tutorial
The Problem
The longest increasing subsequence (LIS) finds the longest subsequence of a sequence where elements are strictly increasing. It appears in scheduling (job sequencing by deadline), genomics (conserved gene segments), and card games (patience sorting, which inspired the O(n log n) algorithm).
The naive O(n²) DP can be improved to O(n log n) using patience sorting — the key insight being that you maintain a list of "piles" whose tops are sorted, enabling binary search.
🎯 Learning Outcomes
binary_search for the patience sort stepCode Example
#![allow(clippy::all)]
// 1058: Longest Increasing Subsequence — O(n log n) Patience Sorting
// Approach 1: O(n^2) DP
fn lis_dp(arr: &[i32]) -> usize {
if arr.is_empty() {
return 0;
}
let n = arr.len();
let mut dp = vec![1usize; n];
for i in 1..n {
for j in 0..i {
if arr[j] < arr[i] {
dp[i] = dp[i].max(dp[j] + 1);
}
}
}
*dp.iter().max().unwrap()
}
// Approach 2: O(n log n) patience sorting with binary search
fn lis_patience(arr: &[i32]) -> usize {
let mut tails: Vec<i32> = Vec::new();
for &x in arr {
match tails.binary_search(&x) {
Ok(pos) => tails[pos] = x,
Err(pos) => {
if pos == tails.len() {
tails.push(x);
} else {
tails[pos] = x;
}
}
}
}
tails.len()
}
// Approach 3: Iterator-based with fold
fn lis_fold(arr: &[i32]) -> usize {
arr.iter()
.fold(Vec::new(), |mut tails: Vec<i32>, &x| {
match tails.binary_search(&x) {
Ok(pos) => tails[pos] = x,
Err(pos) => {
if pos == tails.len() {
tails.push(x);
} else {
tails[pos] = x;
}
}
}
tails
})
.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lis_dp() {
assert_eq!(lis_dp(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_dp(&[0, 1, 0, 3, 2, 3]), 4);
assert_eq!(lis_dp(&[7, 7, 7, 7]), 1);
}
#[test]
fn test_lis_patience() {
assert_eq!(lis_patience(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_patience(&[0, 1, 0, 3, 2, 3]), 4);
assert_eq!(lis_patience(&[7, 7, 7, 7]), 1);
}
#[test]
fn test_lis_fold() {
assert_eq!(lis_fold(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_fold(&[0, 1, 0, 3, 2, 3]), 4);
}
}Key Differences
binary_search API**: Rust's binary_search returns Ok(pos) for exact matches and Err(pos) for insertion points; the patience algorithm needs Err(pos) which maps to a lower-bound bisection.tails grows dynamically with push; OCaml pre-allocates an array of the input length.dp array to reconstruct the actual LIS (not just its length); the tails array does not directly encode the subsequence.timsort merge decision algorithm.OCaml Approach
let lis_patience arr =
let tails = Array.make (Array.length arr) 0 in
let len = ref 0 in
Array.iter (fun x ->
(* Binary search for insertion position *)
let lo = ref 0 and hi = ref !len in
while !lo < !hi do
let mid = (!lo + !hi) / 2 in
if tails.(mid) < x then lo := mid + 1 else hi := mid
done;
tails.(!lo) <- x;
if !lo = !len then incr len
) arr;
!len
The algorithm is identical. The binary search for the lower bound is the same in both languages.
Full Source
#![allow(clippy::all)]
// 1058: Longest Increasing Subsequence — O(n log n) Patience Sorting
// Approach 1: O(n^2) DP
fn lis_dp(arr: &[i32]) -> usize {
if arr.is_empty() {
return 0;
}
let n = arr.len();
let mut dp = vec![1usize; n];
for i in 1..n {
for j in 0..i {
if arr[j] < arr[i] {
dp[i] = dp[i].max(dp[j] + 1);
}
}
}
*dp.iter().max().unwrap()
}
// Approach 2: O(n log n) patience sorting with binary search
fn lis_patience(arr: &[i32]) -> usize {
let mut tails: Vec<i32> = Vec::new();
for &x in arr {
match tails.binary_search(&x) {
Ok(pos) => tails[pos] = x,
Err(pos) => {
if pos == tails.len() {
tails.push(x);
} else {
tails[pos] = x;
}
}
}
}
tails.len()
}
// Approach 3: Iterator-based with fold
fn lis_fold(arr: &[i32]) -> usize {
arr.iter()
.fold(Vec::new(), |mut tails: Vec<i32>, &x| {
match tails.binary_search(&x) {
Ok(pos) => tails[pos] = x,
Err(pos) => {
if pos == tails.len() {
tails.push(x);
} else {
tails[pos] = x;
}
}
}
tails
})
.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lis_dp() {
assert_eq!(lis_dp(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_dp(&[0, 1, 0, 3, 2, 3]), 4);
assert_eq!(lis_dp(&[7, 7, 7, 7]), 1);
}
#[test]
fn test_lis_patience() {
assert_eq!(lis_patience(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_patience(&[0, 1, 0, 3, 2, 3]), 4);
assert_eq!(lis_patience(&[7, 7, 7, 7]), 1);
}
#[test]
fn test_lis_fold() {
assert_eq!(lis_fold(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_fold(&[0, 1, 0, 3, 2, 3]), 4);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lis_dp() {
assert_eq!(lis_dp(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_dp(&[0, 1, 0, 3, 2, 3]), 4);
assert_eq!(lis_dp(&[7, 7, 7, 7]), 1);
}
#[test]
fn test_lis_patience() {
assert_eq!(lis_patience(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_patience(&[0, 1, 0, 3, 2, 3]), 4);
assert_eq!(lis_patience(&[7, 7, 7, 7]), 1);
}
#[test]
fn test_lis_fold() {
assert_eq!(lis_fold(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
assert_eq!(lis_fold(&[0, 1, 0, 3, 2, 3]), 4);
}
}
Deep Comparison
Longest Increasing Subsequence — Comparison
Core Insight
The patience sorting algorithm maintains a sorted tails array. For each element, binary search finds its position — either extending the longest subsequence or replacing an element to keep tails minimal. This achieves O(n log n) vs the naive O(n^2) DP.
OCaml Approach
ref cells for lo/hiArray.fold_left max 0 for finding maximum in O(n^2) versionArray.iter with side effects for the patience sortRust Approach
slice::binary_search returns Ok(pos) or Err(insertion_point) — perfect for patience sortVec as dynamic tails array with push and indexingfold for a functional patience sort variantbinary_search result is elegantComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Binary search | Manual implementation | slice::binary_search() built-in |
| Search result | Returns index | Result<Ok(pos), Err(pos)> |
| Dynamic array | Fixed Array + length counter | Vec with push |
| Fold variant | Less natural (array mutation) | Clean fold over iterator |
| Max finding | Array.fold_left max 0 | iter().max().unwrap() |
Exercises
lis_patience to also reconstruct the actual LIS elements, not just the length.lis_count(arr: &[i32]) -> usize that counts the number of distinct LIS sequences of maximum length.