789-longest-increasing-subsequence — Longest Increasing Subsequence
Tutorial Video
Text description (accessibility)
This video demonstrates the "789-longest-increasing-subsequence — Longest Increasing Subsequence" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The Longest Increasing Subsequence (LIS) problem finds the longest subsequence of a sequence in which all elements are in sorted increasing order. Key difference from OCaml: 1. **Binary search**: Rust's `Vec::binary_search` returns `Ok(pos)` for exact match and `Err(pos)` for insertion point — exactly what LIS needs; OCaml uses `Array.binary_search` from third
Tutorial
The Problem
The Longest Increasing Subsequence (LIS) problem finds the longest subsequence of a sequence in which all elements are in sorted increasing order. It appears in patience sorting (used in the solitaire card game and by Timsort), scheduling theory, and bioinformatics. The naive O(n²) DP is classic; the O(n log n) solution using binary search and "patience sorting" is a key algorithmic insight showing that binary search applies to DP optimization.
🎯 Learning Outcomes
dp[i] = LIS length ending at index itails arraytails invariant: tails[i] is the smallest tail element of any increasing subsequence of length i+1binary_search to maintain the tails arrayCode Example
#![allow(clippy::all)]
//! # Longest Increasing Subsequence
pub fn lis(arr: &[i32]) -> usize {
if arr.is_empty() {
return 0;
}
let mut dp = vec![1; arr.len()];
for i in 1..arr.len() {
for j in 0..i {
if arr[j] < arr[i] {
dp[i] = dp[i].max(dp[j] + 1);
}
}
}
*dp.iter().max().unwrap()
}
pub fn lis_binary_search(arr: &[i32]) -> usize {
let mut tails = Vec::new();
for &x in arr {
match tails.binary_search(&x) {
Ok(_) => {}
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() {
assert_eq!(lis(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
}
#[test]
fn test_binary() {
assert_eq!(lis_binary_search(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
}
#[test]
fn test_empty() {
assert_eq!(lis(&[]), 0);
}
}Key Differences
Vec::binary_search returns Ok(pos) for exact match and Err(pos) for insertion point — exactly what LIS needs; OCaml uses Array.binary_search from third-party libraries.tails invariant is subtle; both languages benefit from a comment explaining why this works despite tails not being the actual LIS.tails alone.timsort uses the patience sorting / LIS insight to identify already-sorted runs; Rust's std sort uses a similar technique.OCaml Approach
OCaml implements the O(n²) version functionally with Array.init n (fun i -> ...) and the O(n log n) version with a mutable tails array and binary search via Array.blit. OCaml's standard library includes Array.blit for efficient element shifts. The patience sorting metaphor maps naturally: each pile is a tails slot, and placing a card uses binary search to find the leftmost pile that accepts it.
Full Source
#![allow(clippy::all)]
//! # Longest Increasing Subsequence
pub fn lis(arr: &[i32]) -> usize {
if arr.is_empty() {
return 0;
}
let mut dp = vec![1; arr.len()];
for i in 1..arr.len() {
for j in 0..i {
if arr[j] < arr[i] {
dp[i] = dp[i].max(dp[j] + 1);
}
}
}
*dp.iter().max().unwrap()
}
pub fn lis_binary_search(arr: &[i32]) -> usize {
let mut tails = Vec::new();
for &x in arr {
match tails.binary_search(&x) {
Ok(_) => {}
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() {
assert_eq!(lis(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
}
#[test]
fn test_binary() {
assert_eq!(lis_binary_search(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
}
#[test]
fn test_empty() {
assert_eq!(lis(&[]), 0);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lis() {
assert_eq!(lis(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
}
#[test]
fn test_binary() {
assert_eq!(lis_binary_search(&[10, 9, 2, 5, 3, 7, 101, 18]), 4);
}
#[test]
fn test_empty() {
assert_eq!(lis(&[]), 0);
}
}
Deep Comparison
OCaml vs Rust: Longest Increasing Subsequence
Overview
See the example.rs and example.ml files for detailed implementations.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Type system | Hindley-Milner | Ownership + traits |
| Memory | GC | Zero-cost abstractions |
| Mutability | Explicit ref | mut keyword |
| Error handling | Option/Result | Result<T, E> |
See README.md for detailed comparison.
Exercises
lis_reconstruct(arr: &[i32]) -> Vec<i32> that returns the actual LIS (not just its length) by storing predecessor indices during the O(n²) DP.lis_binary_search to handle non-strictly-increasing (allowing equal elements) by changing < to <= in the binary search.