027 — Group the Elements of a Set into Disjoint Subsets
Tutorial Video
Text description (accessibility)
This video demonstrates the "027 — Group the Elements of a Set into Disjoint Subsets" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Partitioning a set into groups of specified sizes (OCaml 99 Problems #27) — for example, dividing 9 people into groups of 2, 3, and 4 — generalizes the combinations problem. Key difference from OCaml: 1. **List.mem vs HashSet**: OCaml's `List.filter ... not (List.mem x combo)` is O(n·k) per combination. Rust should use a `HashSet` for O(1) membership testing when removing selected elements.
Tutorial
The Problem
Partitioning a set into groups of specified sizes (OCaml 99 Problems #27) — for example, dividing 9 people into groups of 2, 3, and 4 — generalizes the combinations problem. This is multinomial selection: choose the first group, then choose the second group from the remainder, and so on. The number of ways to partition n elements into groups of sizes k1, k2, ..., km is the multinomial coefficient n! / (k1! · k2! · ... · km!).
This problem appears in sports scheduling (dividing teams into pools), committee selection, machine learning data splitting (train/validation/test), and parallel task distribution. The recursive structure — select one group, recurse on the rest — is a generalization of backtracking.
🎯 Learning Outcomes
combinations to select each group from the remaining elementsCode Example
#![allow(clippy::all)]
// Partition a list into disjoint groups of the given sizes (OCaml 99 Problems #27),
// composing combinations() to select each group in turn.
fn combinations<T: Clone + PartialEq>(k: usize, list: &[T]) -> Vec<Vec<T>> {
if k == 0 {
return vec![vec![]];
}
match list.split_first() {
None => vec![],
Some((head, tail)) => {
let mut with_head: Vec<Vec<T>> = combinations(k - 1, tail)
.into_iter()
.map(|mut c| {
c.insert(0, head.clone());
c
})
.collect();
with_head.extend(combinations(k, tail));
with_head
}
}
}
pub fn group<T: Clone + PartialEq>(list: &[T], sizes: &[usize]) -> Vec<Vec<Vec<T>>> {
match sizes.split_first() {
None => vec![vec![]],
Some((&k, rest)) => {
let mut result = Vec::new();
for combo in combinations(k, list) {
let remaining: Vec<T> = list.iter().filter(|x| !combo.contains(x)).cloned().collect();
for sub in group(&remaining, rest) {
let mut g = vec![combo.clone()];
g.extend(sub);
result.push(g);
}
}
result
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_group_counts_match_multinomial_coefficient() {
// C(3,1) * C(2,2) = 3 * 1 = 3
assert_eq!(group(&[1, 2, 3], &[1, 2]).len(), 3);
}
#[test]
fn test_each_grouping_partitions_the_original_set() {
let groupings = group(&[1, 2, 3, 4], &[2, 2]);
for grouping in &groupings {
let flattened: HashSet<_> = grouping.iter().flatten().collect();
assert_eq!(flattened, [1, 2, 3, 4].iter().collect());
assert_eq!(grouping.len(), 2);
for g in grouping {
assert_eq!(g.len(), 2);
}
}
}
#[test]
fn test_larger_grouping_count() {
// C(9,2) * C(7,3) * C(4,4) = 36 * 35 * 1 = 1260
let list: Vec<i32> = (1..=9).collect();
assert_eq!(group(&list, &[2, 3, 4]).len(), 1260);
}
#[test]
fn test_empty_sizes_yields_one_empty_grouping() {
assert_eq!(group(&[1, 2, 3], &[]), vec![Vec::<Vec<i32>>::new()]);
}
}Key Differences
List.filter ... not (List.mem x combo) is O(n·k) per combination. Rust should use a HashSet for O(1) membership testing when removing selected elements.Eq types.combinations, then recurse on the remainder. The structure is identical — composition of previous solutions.List.filter (fun x -> not (List.mem x c)) (O(k·n)); Rust uses index-based exclusion or HashSet.Vec<Vec<Vec<T>>> in Rust (list of groupings, each grouping is a list of groups). Deeply nested types benefit from type aliases for readability.OCaml Approach
OCaml's version: let group lst sizes = let rec aux lst sizes = match sizes with | [] -> [[]] | k :: rest -> List.concat_map (fun combo -> let remaining = List.filter (fun x -> not (List.mem x combo)) lst in List.map (fun groups -> combo :: groups) (aux remaining rest)) (combinations k lst) in aux lst sizes. This selects a combination of size k, removes those elements from the pool, then recursively groups the remainder.
OCaml's group operation: let group list sizes = match sizes with | [] -> [[]] | k :: ks -> List.concat_map (fun c -> List.map (fun g -> c :: g) (group (List.filter (fun x -> not (List.mem x c)) list) ks)) (combinations k list). For each combination of size k, recursively group the remaining elements. The multinomial coefficient n! / (k1! · k2! · ... · km!) counts the results.
Full Source
#![allow(clippy::all)]
// Partition a list into disjoint groups of the given sizes (OCaml 99 Problems #27),
// composing combinations() to select each group in turn.
fn combinations<T: Clone + PartialEq>(k: usize, list: &[T]) -> Vec<Vec<T>> {
if k == 0 {
return vec![vec![]];
}
match list.split_first() {
None => vec![],
Some((head, tail)) => {
let mut with_head: Vec<Vec<T>> = combinations(k - 1, tail)
.into_iter()
.map(|mut c| {
c.insert(0, head.clone());
c
})
.collect();
with_head.extend(combinations(k, tail));
with_head
}
}
}
pub fn group<T: Clone + PartialEq>(list: &[T], sizes: &[usize]) -> Vec<Vec<Vec<T>>> {
match sizes.split_first() {
None => vec![vec![]],
Some((&k, rest)) => {
let mut result = Vec::new();
for combo in combinations(k, list) {
let remaining: Vec<T> = list.iter().filter(|x| !combo.contains(x)).cloned().collect();
for sub in group(&remaining, rest) {
let mut g = vec![combo.clone()];
g.extend(sub);
result.push(g);
}
}
result
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_group_counts_match_multinomial_coefficient() {
// C(3,1) * C(2,2) = 3 * 1 = 3
assert_eq!(group(&[1, 2, 3], &[1, 2]).len(), 3);
}
#[test]
fn test_each_grouping_partitions_the_original_set() {
let groupings = group(&[1, 2, 3, 4], &[2, 2]);
for grouping in &groupings {
let flattened: HashSet<_> = grouping.iter().flatten().collect();
assert_eq!(flattened, [1, 2, 3, 4].iter().collect());
assert_eq!(grouping.len(), 2);
for g in grouping {
assert_eq!(g.len(), 2);
}
}
}
#[test]
fn test_larger_grouping_count() {
// C(9,2) * C(7,3) * C(4,4) = 36 * 35 * 1 = 1260
let list: Vec<i32> = (1..=9).collect();
assert_eq!(group(&list, &[2, 3, 4]).len(), 1260);
}
#[test]
fn test_empty_sizes_yields_one_empty_grouping() {
assert_eq!(group(&[1, 2, 3], &[]), vec![Vec::<Vec<i32>>::new()]);
}
}#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_group_counts_match_multinomial_coefficient() {
// C(3,1) * C(2,2) = 3 * 1 = 3
assert_eq!(group(&[1, 2, 3], &[1, 2]).len(), 3);
}
#[test]
fn test_each_grouping_partitions_the_original_set() {
let groupings = group(&[1, 2, 3, 4], &[2, 2]);
for grouping in &groupings {
let flattened: HashSet<_> = grouping.iter().flatten().collect();
assert_eq!(flattened, [1, 2, 3, 4].iter().collect());
assert_eq!(grouping.len(), 2);
for g in grouping {
assert_eq!(g.len(), 2);
}
}
}
#[test]
fn test_larger_grouping_count() {
// C(9,2) * C(7,3) * C(4,4) = 36 * 35 * 1 = 1260
let list: Vec<i32> = (1..=9).collect();
assert_eq!(group(&list, &[2, 3, 4]).len(), 1260);
}
#[test]
fn test_empty_sizes_yields_one_empty_grouping() {
assert_eq!(group(&[1, 2, 3], &[]), vec![Vec::<Vec<i32>>::new()]);
}
}
Deep Comparison
OCaml vs Rust: Group By Size
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
equal_groups(list: &[i32], k: usize) -> Vec<Vec<Vec<i32>>> that divides the list into groups all of size k. Handle the case where list.len() % k != 0.count_groups(n: u64, sizes: &[u64]) -> u64 that computes the number of partitions using the multinomial formula without generating them.balanced_split<T: Clone>(list: &[T]) -> Vec<(Vec<T>, Vec<T>)> that generates all ways to split a list into two roughly equal halves.