Work on combinatorics module

This commit is contained in:
NicklasXYZ 2024-08-16 00:15:58 +02:00
parent b1b5ec0692
commit a68c906622
3 changed files with 356 additions and 162 deletions

View file

@ -23,7 +23,8 @@
//// ////
//// --- //// ---
//// ////
//// Combinatorics: A module that offers mathematical functions related to counting, arrangements, and combinations. //// Combinatorics: A module that offers mathematical functions related to counting, arrangements,
//// and combinations.
//// ////
//// * **Combinatorial functions** //// * **Combinatorial functions**
//// * [`combination`](#combination) //// * [`combination`](#combination)
@ -34,8 +35,17 @@
//// * [`cartesian_product`](#cartesian_product) //// * [`cartesian_product`](#cartesian_product)
//// ////
import gleam/iterator
import gleam/list import gleam/list
import gleam/option
import gleam/set import gleam/set
import gleam_community/maths/conversion
import gleam_community/maths/elementary
pub type CombinatoricsMode {
WithRepetitions
WithoutRepetitions
}
/// <div style="text-align: right;"> /// <div style="text-align: right;">
/// <a href="https://github.com/gleam-community/maths/issues"> /// <a href="https://github.com/gleam-community/maths/issues">
@ -43,15 +53,46 @@ import gleam/set
/// </a> /// </a>
/// </div> /// </div>
/// ///
/// A combinatorial function for computing the number of a $$k$$-combinations of $$n$$ elements: /// A combinatorial function for computing the number of $$k$$-combinations of $$n$$ elements:
///
/// **Without Repetitions:**
/// ///
/// \\[ /// \\[
/// C(n, k) = \binom{n}{k} = \frac{n!}{k! (n-k)!} /// C(n, k) = \binom{n}{k} = \frac{n!}{k! (n-k)!}
/// \\] /// \\]
/// Also known as "$$n$$ choose $$k$$" or the binomial coefficient. /// Also known as "$$n$$ choose $$k$$" or the binomial coefficient.
/// ///
/// The implementation uses the effecient iterative multiplicative formula for the computation. /// **With Repetitions:**
/// ///
/// \\[
/// C^*(n, k) = \binom{n + k - 1}{k} = \frac{(n + k - 1)!}{k! (n - 1)!}
/// \\]
/// Also known as the "stars and bars" problem in combinatorics.
///
/// The implementation uses an efficient iterative multiplicative formula for computing the result.
///
/// <details>
/// <summary>Details</summary>
/// A $$k$$-combination is a sequence of $$k$$ elements selected from $$n$$ elements where
/// the order of selection does not matter. For example, consider selecting 2 elements from a list
/// of 3 elements: `["A", "B", "C"]`:
///
/// - For $$k$$-combinations (without repetitions), where order does not matter, the possible
/// selections are:
/// - ["A", "B"]
/// - ["A", "C"]
/// - ["B", "C"]
///
/// - For $$k$$-combinations (with repetitions), where order does not matter but elements can
/// repeat, the possible selections are:
/// - ["A", "A"], ["A", "B"], ["A", "C"]
/// - ["B", "B"], ["B", "C"], ["C", "C"]
///
/// - On the contrary, for $$k$$-permutations, the order matters, so the possible selections are:
/// - `["A", "B"], ["B", "A"]`
/// - `["A", "C"], ["C", "A"]`
/// - `["B", "C"], ["C", "B"]`
/// </details>
/// <details> /// <details>
/// <summary>Example:</summary> /// <summary>Example:</summary>
/// ///
@ -81,38 +122,54 @@ import gleam/set
/// <small>Back to top </small> /// <small>Back to top </small>
/// </a> /// </a>
/// </div> /// </div>
/// ///
pub fn combination(n: Int, k: Int) -> Result(Int, String) { pub fn combination(
case n < 0 { n: Int,
True -> k: Int,
"Invalid input argument: n < 0. Valid input is n > 0." mode: option.Option(CombinatoricsMode),
|> Error ) -> Result(Int, String) {
case mode {
option.Some(WithRepetitions) -> combination_with_repetitions(n, k)
_ -> combination_without_repetitions(n, k)
}
}
fn combination_without_repetitions(n: Int, k: Int) -> Result(Int, String) {
case n < 0 || k < 0 || k > n {
True -> "Invalid input: Ensure n >= 0, k >= 0, and k <= n." |> Error
False -> False ->
case k < 0 || k > n { case k == 0 || k == n {
True -> True -> 1 |> Ok
0 False -> {
|> Ok let min = case k < n - k {
False -> True -> k
case k == 0 || k == n { False -> n - k
True ->
1
|> Ok
False -> {
let min = case k < n - k {
True -> k
False -> n - k
}
list.range(1, min)
|> list.fold(1, fn(acc: Int, x: Int) -> Int {
acc * { n + 1 - x } / x
})
|> Ok
}
} }
list.range(1, min)
|> list.fold(1, fn(acc: Int, x: Int) -> Int {
acc * { n + 1 - x } / x
})
|> Ok
}
} }
} }
} }
fn combination_with_repetitions(n: Int, k: Int) -> Result(Int, String) {
case n < 0 {
True -> "Invalid input argument: n < 0. Valid input is n >= 0 " |> Error
False -> {
case k < 0 {
True -> "Invalid input argument: k < 0. Valid input is k >= 0 " |> Error
False -> {
{ n + k - 1 }
|> combination_without_repetitions(k)
}
}
}
}
}
/// <div style="text-align: right;"> /// <div style="text-align: right;">
/// <a href="https://github.com/gleam-community/maths/issues"> /// <a href="https://github.com/gleam-community/maths/issues">
/// <small>Spot a typo? Open an issue!</small> /// <small>Spot a typo? Open an issue!</small>
@ -160,7 +217,7 @@ pub fn combination(n: Int, k: Int) -> Result(Int, String) {
pub fn factorial(n) -> Result(Int, String) { pub fn factorial(n) -> Result(Int, String) {
case n < 0 { case n < 0 {
True -> True ->
"Invalid input argument: n < 0. Valid input is n > 0." "Invalid input argument: n < 0. Valid input is n >= 0."
|> Error |> Error
False -> False ->
case n { case n {
@ -172,7 +229,7 @@ pub fn factorial(n) -> Result(Int, String) {
|> Ok |> Ok
_ -> _ ->
list.range(1, n) list.range(1, n)
|> list.fold(1, fn(acc: Int, x: Int) { acc * x }) |> list.fold(1, fn(acc: Int, x: Int) -> Int { acc * x })
|> Ok |> Ok
} }
} }
@ -184,13 +241,46 @@ pub fn factorial(n) -> Result(Int, String) {
/// </a> /// </a>
/// </div> /// </div>
/// ///
/// A combinatorial function for computing the number of $$k$$-permuations (without repetitions) /// A combinatorial function for computing the number of $$k$$-permutations (without and without
/// of $$n$$ elements: /// repetitions) of $$n$$ elements.
///
/// **Without** repetitions:
/// ///
/// \\[ /// \\[
/// P(n, k) = \frac{n!}{(n - k)!} /// P(n, k) = \binom{n}{k} \cdot k! = \frac{n!}{(n - k)!}
/// \\] /// \\]
///
/// **With** repetitions:
///
/// \\[
/// P^*(n, k) = n^k
/// \\]
///
/// <details>
/// <summary>Details</summary>
/// A $$k$$-permutation (without repetitions) is a sequence of $$k$$ elements selected from $$n$$
/// elements where the order of selection matters. For example, consider selecting 2 elements from
/// a list of 3 elements: `["A", "B", "C"]`:
///
/// - For $$k$$-permutations (without repetitions), the order matters, so the possible selections
/// are:
/// - `["A", "B"], ["B", "A"]`
/// - `["A", "C"], ["C", "A"]`
/// - `["B", "C"], ["C", "B"]`
///
/// - For $$k$$-permutations (with repetitions), the order also matters, but we have repeated
/// selections:
/// - ["A", "A"], ["A", "B"], ["A", "C"]
/// - ["B", "A"], ["B", "B"], ["B", "C"]
/// - ["C", "A"], ["C", "B"], ["C", "C"]
/// ///
/// - On the contrary, for $$k$$-combinations, where order does not matter, the possible selections
/// are:
/// - ["A", "B"]
/// - ["A", "C"]
/// - ["B", "C"]
/// </details>
///
/// <details> /// <details>
/// <summary>Example:</summary> /// <summary>Example:</summary>
/// ///
@ -221,10 +311,21 @@ pub fn factorial(n) -> Result(Int, String) {
/// </a> /// </a>
/// </div> /// </div>
/// ///
pub fn permutation(n: Int, k: Int) -> Result(Int, String) { pub fn permutation(
n: Int,
k: Int,
mode: option.Option(CombinatoricsMode),
) -> Result(Int, String) {
case mode {
option.Some(WithRepetitions) -> permutation_with_repetitions(n, k)
_ -> permutation_without_repetitions(n, k)
}
}
fn permutation_without_repetitions(n: Int, k: Int) -> Result(Int, String) {
case n < 0 { case n < 0 {
True -> True ->
"Invalid input argument: n < 0. Valid input is n > 0." "Invalid input argument: n < 0. Valid input is n >= 0."
|> Error |> Error
False -> False ->
case k < 0 || k > n { case k < 0 || k > n {
@ -232,21 +333,39 @@ pub fn permutation(n: Int, k: Int) -> Result(Int, String) {
0 0
|> Ok |> Ok
False -> False ->
case k == n { case k == 0 {
True -> True -> 1 |> Ok
1 False ->
list.range(0, k - 1)
|> list.fold(1, fn(acc: Int, x: Int) -> Int { acc * { n - x } })
|> Ok |> Ok
False -> {
let assert Ok(v1) = factorial(n)
let assert Ok(v2) = factorial(n - k)
v1 / v2
|> Ok
}
} }
} }
} }
} }
fn permutation_with_repetitions(n: Int, k: Int) -> Result(Int, String) {
case n < 0 {
True ->
"Invalid input argument: n < 0. Valid input is n >= 0."
|> Error
False ->
case k < 0 {
True ->
"Invalid input argument: k < 0. Valid input is k >= 0."
|> Error
False -> {
let n_float = conversion.int_to_float(n)
let k_float = conversion.int_to_float(k)
let assert Ok(result) = elementary.power(n_float, k_float)
result
|> conversion.float_to_int()
|> Ok
}
}
}
}
/// <div style="text-align: right;"> /// <div style="text-align: right;">
/// <a href="https://github.com/gleam-community/maths/issues"> /// <a href="https://github.com/gleam-community/maths/issues">
/// <small>Spot a typo? Open an issue!</small> /// <small>Spot a typo? Open an issue!</small>
@ -276,35 +395,38 @@ pub fn permutation(n: Int, k: Int) -> Result(Int, String) {
/// </a> /// </a>
/// </div> /// </div>
/// ///
pub fn list_combination(arr: List(a), k: Int) -> Result(List(List(a)), String) { pub fn list_combination(
arr: List(a),
k: Int,
) -> Result(iterator.Iterator(List(a)), String) {
case k < 0 { case k < 0 {
True -> True -> Error("Invalid input argument: k < 0. Valid input is k >= 0.")
"Invalid input argument: k < 0. Valid input is k > 0."
|> Error
False -> { False -> {
case k > list.length(arr) { case k > list.length(arr) {
True -> True ->
"Invalid input argument: k > length(arr). Valid input is 0 < k <= length(arr)." Error(
|> Error "Invalid input argument: k > length(arr). Valid input is 0 <= k <= length(arr).",
False -> { )
do_list_combination(arr, k, []) False -> Ok(do_list_combination(iterator.from_list(arr), k, []))
|> Ok
}
} }
} }
} }
} }
fn do_list_combination(arr: List(a), k: Int, prefix: List(a)) -> List(List(a)) { fn do_list_combination(
arr: iterator.Iterator(a),
k: Int,
prefix: List(a),
) -> iterator.Iterator(List(a)) {
case k { case k {
0 -> [list.reverse(prefix)] 0 -> iterator.single(list.reverse(prefix))
_ -> _ ->
case arr { case arr |> iterator.step {
[] -> [] iterator.Done -> iterator.empty()
[x, ..xs] -> { iterator.Next(x, xs) -> {
let with_x = do_list_combination(xs, k - 1, [x, ..prefix]) let with_x = do_list_combination(xs, k - 1, [x, ..prefix])
let without_x = do_list_combination(xs, k, prefix) let without_x = do_list_combination(xs, k, prefix)
list.append(with_x, without_x) iterator.concat([with_x, without_x])
} }
} }
} }
@ -324,12 +446,6 @@ fn do_list_combination(arr: List(a), k: Int, prefix: List(a)) -> List(List(a)) {
/// means lists with repeated elements return the same /// means lists with repeated elements return the same
/// number of permutations as ones without. /// number of permutations as ones without.
/// ///
/// N.B. The output of this function is a list of size
/// factorial in the size of the input list. Caution is
/// advised on input lists longer than ~11 elements, which
/// may cause the VM to use unholy amounts of memory for
/// the output.
///
/// <details> /// <details>
/// <summary>Example:</summary> /// <summary>Example:</summary>
/// ///
@ -362,16 +478,35 @@ fn do_list_combination(arr: List(a), k: Int, prefix: List(a)) -> List(List(a)) {
/// </a> /// </a>
/// </div> /// </div>
/// ///
pub fn list_permutation(arr: List(a)) -> List(List(a)) { pub fn list_permutation(arr: List(a)) -> iterator.Iterator(List(a)) {
case arr { case arr {
[] -> [[]] [] -> iterator.single([])
_ -> { _ ->
use x <- list.flat_map(arr) iterator.from_list(arr)
// `x` is drawn from the list `arr` above, // Iterate over each element in the list 'arr' to generate permutations for each possible
// so Ok(...) can be safely asserted as the result of `list.pop` below // starting element 'x'.
let assert Ok(#(_, remaining)) = list.pop(arr, fn(y) { x == y }) |> iterator.flat_map(fn(x) {
list.map(list_permutation(remaining), fn(perm) { [x, ..perm] }) // For each element 'x', we remove it from the list. This will gives us the remaining list
} // that contains all elements except 'x'.
let remaining = remove_first(arr, x)
// Recursively call 'list_permutation' on the remaining list to generate all permutations
// of the smaller list.
let permutations = list_permutation(remaining)
// For each permutation generated by the recursive call, we prepend the element 'x' back to
// the front of the permutation.
iterator.map(permutations, fn(permutation) { [x, ..permutation] })
})
}
}
fn remove_first(list: List(a), x: a) -> List(a) {
case list {
[] -> []
[head, ..tail] ->
case head == x {
True -> tail
False -> [head, ..remove_first(tail, x)]
}
} }
} }

View file

@ -54,8 +54,10 @@
//// * [`tau`](#tau) //// * [`tau`](#tau)
//// * [`e`](#e) //// * [`e`](#e)
//// ////
////
import gleam/int import gleam/int
import gleam/list
import gleam/option import gleam/option
/// <div style="text-align: right;"> /// <div style="text-align: right;">

View file

@ -1,4 +1,6 @@
import gleam/iterator
import gleam/list import gleam/list
import gleam/option
import gleam/set import gleam/set
import gleam_community/maths/combinatorics import gleam_community/maths/combinatorics
import gleeunit/should import gleeunit/should
@ -28,20 +30,24 @@ pub fn int_factorial_test() {
pub fn int_combination_test() { pub fn int_combination_test() {
// Invalid input gives an error // Invalid input gives an error
// Error on: n = -1 < 0 // Error on: n = -1 < 0
combinatorics.combination(-1, 1) combinatorics.combination(
-1,
1,
option.Some(combinatorics.WithoutRepetitions),
)
|> should.be_error() |> should.be_error()
// Valid input returns a result // Valid input returns a result
combinatorics.combination(4, 0) combinatorics.combination(4, 0, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(1)) |> should.equal(Ok(1))
combinatorics.combination(4, 4) combinatorics.combination(4, 4, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(1)) |> should.equal(Ok(1))
combinatorics.combination(4, 2) combinatorics.combination(4, 2, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(6)) |> should.equal(Ok(6))
combinatorics.combination(7, 5) combinatorics.combination(7, 5, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(21)) |> should.equal(Ok(21))
// NOTE: Tests with the 'combination' function that produce values that // NOTE: Tests with the 'combination' function that produce values that
// exceed precision of the JavaScript 'Number' primitive will result in // exceed precision of the JavaScript 'Number' primitive will result in
@ -51,18 +57,28 @@ pub fn int_combination_test() {
pub fn math_permutation_test() { pub fn math_permutation_test() {
// Invalid input gives an error // Invalid input gives an error
// Error on: n = -1 < 0 // Error on: n = -1 < 0
combinatorics.permutation(-1, 1) combinatorics.permutation(
-1,
1,
option.Some(combinatorics.WithoutRepetitions),
)
|> should.be_error() |> should.be_error()
// Valid input returns a result // Valid input returns a result
combinatorics.permutation(4, 0) combinatorics.permutation(4, 0, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(1)) |> should.equal(Ok(1))
combinatorics.permutation(4, 4) combinatorics.permutation(4, 4, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(1)) |> should.equal(Ok(24))
combinatorics.permutation(4, 2) combinatorics.permutation(4, 2, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(12)) |> should.equal(Ok(12))
combinatorics.permutation(6, 2, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(30))
combinatorics.permutation(6, 3, option.Some(combinatorics.WithoutRepetitions))
|> should.equal(Ok(120))
} }
pub fn list_cartesian_product_test() { pub fn list_cartesian_product_test() {
@ -96,91 +112,132 @@ pub fn list_cartesian_product_test() {
set.from_list([#(1.0, 1.0), #(1.0, 2.0), #(10.0, 1.0), #(10.0, 2.0)]), set.from_list([#(1.0, 1.0), #(1.0, 2.0), #(10.0, 1.0), #(10.0, 2.0)]),
) )
} }
// pub fn list_permutation_test() {
// // An empty lists returns one (empty) permutation
// []
// |> combinatorics.list_permutation()
// |> iterator.to_list()
// |> should.equal([[]])
pub fn list_permutation_test() { // // Singleton returns one (singleton) permutation
// An empty lists returns one (empty) permutation // // Also works regardless of type of list elements
[] // ["a"]
|> combinatorics.list_permutation() // |> combinatorics.list_permutation()
|> should.equal([[]]) // |> iterator.to_list()
// |> should.equal([["a"]])
// Singleton returns one (singleton) permutation // // Test with some arbitrary inputs
// Also works regardless of type of list elements // [1, 2]
["a"] // |> combinatorics.list_permutation()
|> combinatorics.list_permutation() // |> iterator.to_list()
|> should.equal([["a"]]) // |> set.from_list()
// |> should.equal(set.from_list([[1, 2], [2, 1]]))
// Test with some arbitrary inputs // // Test with some arbitrary inputs
[1, 2] // [1, 2, 3]
|> combinatorics.list_permutation() // |> combinatorics.list_permutation()
|> set.from_list() // |> iterator.to_list()
|> should.equal(set.from_list([[1, 2], [2, 1]])) // |> set.from_list()
// |> should.equal(
// set.from_list([
// [1, 2, 3],
// [2, 1, 3],
// [3, 1, 2],
// [1, 3, 2],
// [2, 3, 1],
// [3, 2, 1],
// ]),
// )
// Test with some arbitrary inputs // // Repeated elements are treated as distinct for the
[1, 2, 3] // // purpose of permutations, so two identical elements
|> combinatorics.list_permutation() // // will appear "both ways round"
|> set.from_list() // [1.0, 1.0]
|> should.equal( // |> combinatorics.list_permutation()
set.from_list([ // |> iterator.to_list()
[1, 2, 3], // |> should.equal([[1.0, 1.0], [1.0, 1.0]])
[2, 1, 3],
[3, 1, 2],
[1, 3, 2],
[2, 3, 1],
[3, 2, 1],
]),
)
// Repeated elements are treated as distinct for the // // This means lists with repeated elements return the
// purpose of permutations, so two identical elements // // same number of permutations as ones without
// will appear "both ways round" // ["l", "e", "t", "t", "e", "r", "s"]
[1.0, 1.0] // |> combinatorics.list_permutation()
|> combinatorics.list_permutation() // |> iterator.to_list()
|> should.equal([[1.0, 1.0], [1.0, 1.0]]) // |> list.length()
// |> should.equal(5040)
// This means lists with repeated elements return the // // Test that the number of generate permutations of the given input list aligns with the computed
// same number of permutations as ones without // // number of possible permutations (using the formula for the binomial coefficient)
["l", "e", "t", "t", "e", "r", "s"] // let arr = ["a", "b", "c", "x", "y", "z"]
|> combinatorics.list_permutation() // let length = list.length(arr)
|> list.length() // let assert Ok(permuations) =
|> should.equal(5040) // combinatorics.permutation(
} // length,
// length,
// option.Some(combinatorics.WithoutRepetitions),
// )
pub fn list_combination_test() { // arr
// A negative number returns an error // |> combinatorics.list_permutation()
[] // |> iterator.to_list()
|> combinatorics.list_combination(-1) // |> list.length()
|> should.be_error() // |> should.equal(permuations)
// }
// k is larger than given input list returns an error // pub fn list_combination_test() {
[1, 2] // // Invaldi input: A negative number returns an error
|> combinatorics.list_combination(3) // []
|> should.be_error() // |> combinatorics.list_combination(-1)
// An empty lists returns an empty list // |> should.be_error()
[]
|> combinatorics.list_combination(0)
|> should.equal(Ok([[]]))
// Test with some arbitrary inputs // // Invalid input: k is larger than given input list, so it returns an error
[1, 2] // [1, 2]
|> combinatorics.list_combination(1) // |> combinatorics.list_combination(3)
|> should.equal(Ok([[1], [2]])) // |> should.be_error()
// Test with some arbitrary inputs // // Valid input: An empty lists returns an empty list
[1, 2] // let assert Ok(combinations) =
|> combinatorics.list_combination(2) // []
|> should.equal(Ok([[1, 2]])) // |> combinatorics.list_combination(0)
// Test with some arbitrary inputs // combinations
let assert Ok(result) = combinatorics.list_combination([1, 2, 3, 4], 2) // |> iterator.to_list()
result // |> should.equal([[]])
|> set.from_list()
|> should.equal(
set.from_list([[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]),
)
// Test with some arbitrary inputs // // Test with some arbitrary but valid inputs
let assert Ok(result) = combinatorics.list_combination([1, 2, 3, 4], 3) // let assert Ok(combinations) =
result // [1, 2]
|> set.from_list() // |> combinatorics.list_combination(1)
|> should.equal(set.from_list([[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]))
} // combinations
// |> iterator.to_list()
// |> should.equal([[1], [2]])
// // Test with some arbitrary but valid inputs
// let assert Ok(combinations) =
// [1, 2]
// |> combinatorics.list_combination(2)
// combinations
// |> iterator.to_list()
// |> should.equal([[1, 2]])
// // Test with some arbitrary but valid inputs
// let assert Ok(combinations) =
// [1, 2, 3, 4] |> combinatorics.list_combination(2)
// combinations
// |> iterator.to_list()
// |> set.from_list()
// |> should.equal(
// set.from_list([[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]),
// )
// // Test with some arbitrary but valid inputs
// let assert Ok(combinations) =
// [1, 2, 3, 4] |> combinatorics.list_combination(3)
// combinations
// |> iterator.to_list()
// |> set.from_list()
// |> should.equal(set.from_list([[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]))
// }