951 Series Sliding Window
Tutorial
The Problem
Extract all contiguous substrings of length n from a string using a sliding window. Apply this to find the window with the largest digit product. Implement two variants: the byte-slice .windows(n) approach and an explicit index range version. Also implement a recursive variant that mirrors OCaml's list-based sliding window style.
🎯 Learning Outcomes
.as_bytes().windows(n) to produce zero-copy overlapping byte windows over a string slicestd::str::from_utf8(0..=len - n).map(|i| &s[i..i+n]).windows() → .map(digit_product) → .max()Result: span too large, invalid (non-digit) charactersCode Example
pub fn series(n: usize, s: &str) -> Vec<String> {
if n == 0 { return vec![String::new(); s.len() + 1]; }
s.as_bytes()
.windows(n)
.map(|w| std::str::from_utf8(w).unwrap().to_owned())
.collect()
}
pub fn largest_product(n: usize, s: &str) -> Result<u64, String> {
if n == 0 { return Ok(1); }
if n > s.len() { return Err("span too large".to_string()); }
let max = series(n, s)
.into_iter()
.map(|sub| sub.chars().map(|c| c as u64 - '0' as u64).product::<u64>())
.max()
.unwrap_or(0);
Ok(max)
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Window source | .as_bytes().windows(n) — zero-copy | String.sub — allocates each substring |
| Index iteration | (0..=len-n).map(\|i\| ...) | List.init (len-n+1) (fun i -> ...) |
| Error handling | Result<u64, String> with ? | result type |
| Product | .product::<u64>() | List.fold_left ( * ) 1 |
.windows(n) is a zero-copy operation — it hands out references into the original byte slice. Converting to String allocates, but the window itself does not. For very large inputs, processing windows without collecting saves memory.
OCaml Approach
let series n s =
let len = String.length s in
if n = 0 then List.init (len + 1) (fun _ -> "")
else List.init (len - n + 1) (fun i -> String.sub s i n)
let digit_product sub =
String.fold_left (fun acc c -> acc * (Char.code c - Char.code '0')) 1 sub
let largest_product n s =
if n = 0 then Ok 1
else if n > String.length s then Error "span too large"
else
series n s
|> List.map digit_product
|> List.fold_left max 0
|> Result.ok
OCaml's List.init k f generates a list of k elements where element i is f(i). This creates substrings without mutation. String.fold_left computes the digit product purely functionally.
Full Source
#![allow(clippy::all)]
pub fn series(n: usize, s: &str) -> Vec<String> {
if n == 0 {
return vec![String::new(); s.len() + 1];
}
s.as_bytes()
.windows(n)
.map(|w| std::str::from_utf8(w).unwrap().to_owned())
.collect()
}
pub fn series_functional(n: usize, s: &str) -> Vec<String> {
let len = s.len();
if n == 0 || n > len {
if n == 0 {
return vec![String::new(); len + 1];
}
return vec![];
}
(0..=len - n).map(|i| s[i..i + n].to_owned()).collect()
}
pub fn largest_product(n: usize, s: &str) -> Result<u64, String> {
if n == 0 {
return Ok(1);
}
if n > s.len() {
return Err("span too large".to_string());
}
if !s.chars().all(|c| c.is_ascii_digit()) {
return Err("invalid character".to_string());
}
let max = series(n, s)
.into_iter()
.map(|sub| sub.chars().map(|c| c as u64 - '0' as u64).product::<u64>())
.max()
.unwrap_or(0);
Ok(max)
}
pub fn largest_product_recursive(n: usize, s: &str) -> Result<u64, String> {
if n == 0 {
return Ok(1);
}
if n > s.len() {
return Err("span too large".to_string());
}
fn digit_product(s: &str) -> u64 {
s.chars().map(|c| c as u64 - '0' as u64).product()
}
fn go(n: usize, s: &str, best: u64) -> u64 {
if s.len() < n {
best
} else {
let p = digit_product(&s[..n]);
go(n, &s[1..], best.max(p))
}
}
Ok(go(n, s, 0))
}
/* Output:
series(3, "49142") = ["491", "914", "142"]
series_functional(3, "49142") = ["491", "914", "142"]
largest_product(2, "0123456789") = Ok(72)
largest_product(6, "49142") = Err("span too large")
largest_product_recursive(2, "0123456789") = Ok(72)
*/Deep Comparison
OCaml vs Rust: Series — Sliding Window
Side-by-Side Code
OCaml
let series n s =
if n > String.length s then []
else
List.init (String.length s - n + 1) (fun i ->
String.sub s i n
)
let largest_product n s =
if n = 0 then Ok 1
else if n > String.length s then Error "span too large"
else
series n s
|> List.map (fun sub ->
String.fold_left (fun acc c ->
acc * (Char.code c - Char.code '0')
) 1 sub
)
|> List.fold_left max 0
|> fun m -> Ok m
Rust (idiomatic)
pub fn series(n: usize, s: &str) -> Vec<String> {
if n == 0 { return vec![String::new(); s.len() + 1]; }
s.as_bytes()
.windows(n)
.map(|w| std::str::from_utf8(w).unwrap().to_owned())
.collect()
}
pub fn largest_product(n: usize, s: &str) -> Result<u64, String> {
if n == 0 { return Ok(1); }
if n > s.len() { return Err("span too large".to_string()); }
let max = series(n, s)
.into_iter()
.map(|sub| sub.chars().map(|c| c as u64 - '0' as u64).product::<u64>())
.max()
.unwrap_or(0);
Ok(max)
}
Rust (functional/recursive)
pub fn largest_product_recursive(n: usize, s: &str) -> Result<u64, String> {
if n == 0 { return Ok(1); }
if n > s.len() { return Err("span too large".to_string()); }
fn digit_product(s: &str) -> u64 {
s.chars().map(|c| c as u64 - '0' as u64).product()
}
fn go(n: usize, s: &str, best: u64) -> u64 {
if s.len() < n { best }
else {
let p = digit_product(&s[..n]);
go(n, &s[1..], best.max(p))
}
}
Ok(go(n, s, 0))
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Sliding windows | val series : int -> string -> string list | fn series(n: usize, s: &str) -> Vec<String> |
| Product with error | val largest_product : int -> string -> (int, string) result | fn largest_product(n: usize, s: &str) -> Result<u64, String> |
| Fold over chars | String.fold_left : ('a -> char -> 'a) -> 'a -> string -> 'a | .chars().fold(init, f) or .product() |
| Max of list | List.fold_left max 0 | .max().unwrap_or(0) |
| Optional integer | 'a option | Option<T> |
Key Insights
slice::windows() is the idiomatic Rust primitive.** OCaml has no built-in sliding-window for strings; you reconstruct it with List.init + String.sub. Rust's slice::windows(n) is provided by the standard library and produces borrowed sub-slices with zero additional allocation, making it both ergonomic and efficient.&str has no O(1) indexing. Working via as_bytes() is safe here because ASCII digit strings are single-byte, and from_utf8 re-borrows the window as &str without copying.product() replaces fold_left (*) 1.** OCaml's String.fold_left (fun acc c -> acc * digit_of c) 1 sub maps directly to .chars().map(digit_of).product::<u64>() in Rust. The product iterator method is the exact Rust idiom for multiplicative folds.u64 safely holds the product of up to 19 single-digit values before overflow, which covers all practical inputs for this problem.best accumulator through go(n, &s[1..], best.max(p)), consuming one character per recursive call — exactly the OCaml List.fold_left pattern translated to explicit recursion on string slices.When to Use Each Style
Use idiomatic Rust when: you want readable, allocation-light code that composes naturally with the rest of the iterator pipeline. windows() + map() + max() reads as a single declarative query.
Use recursive Rust when: teaching the OCaml parallel explicitly, or when the algorithm is naturally expressed as "process head, recurse on tail" and you want to avoid collecting an intermediate Vec.
Exercises
max_window_sum(n, nums: &[i64]) — the maximum sum of any contiguous subarray of length n.distinct_windows(n, s) — count the number of distinct substrings of length n.series and return an empty Vec rather than the current n+1 empty strings.Vec<String>.largest_product to return the starting index of the maximum window in addition to the product value.