From a68c906622829ab30cd6b5c5088fe2ffc9c8c354 Mon Sep 17 00:00:00 2001 From: NicklasXYZ <18580183+NicklasXYZ@users.noreply.github.com> Date: Fri, 16 Aug 2024 00:15:58 +0200 Subject: [PATCH] Work on combinatorics module --- src/gleam_community/maths/combinatorics.gleam | 287 +++++++++++++----- src/gleam_community/maths/elementary.gleam | 2 + .../maths/combinatorics_test.gleam | 229 ++++++++------ 3 files changed, 356 insertions(+), 162 deletions(-) diff --git a/src/gleam_community/maths/combinatorics.gleam b/src/gleam_community/maths/combinatorics.gleam index f459ef6..e60555f 100644 --- a/src/gleam_community/maths/combinatorics.gleam +++ b/src/gleam_community/maths/combinatorics.gleam @@ -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** //// * [`combination`](#combination) @@ -34,8 +35,17 @@ //// * [`cartesian_product`](#cartesian_product) //// +import gleam/iterator import gleam/list +import gleam/option import gleam/set +import gleam_community/maths/conversion +import gleam_community/maths/elementary + +pub type CombinatoricsMode { + WithRepetitions + WithoutRepetitions +} ///
/// @@ -43,15 +53,46 @@ import gleam/set /// ///
/// -/// 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)!} /// \\] /// 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 +/// 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"]` +///
///
/// Example: /// @@ -81,38 +122,54 @@ import gleam/set /// Back to top ↑ /// /// -/// -pub fn combination(n: Int, k: Int) -> Result(Int, String) { - case n < 0 { - True -> - "Invalid input argument: n < 0. Valid input is n > 0." - |> Error +/// +pub fn combination( + n: Int, + k: Int, + mode: option.Option(CombinatoricsMode), +) -> 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 -> - case k < 0 || k > n { - True -> - 0 - |> Ok - False -> - case k == 0 || k == n { - 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 - } + case k == 0 || k == n { + 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 + } } } } +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) + } + } + } + } +} + ///
/// /// Spot a typo? Open an issue! @@ -160,7 +217,7 @@ pub fn combination(n: Int, k: Int) -> Result(Int, String) { pub fn factorial(n) -> Result(Int, String) { case n < 0 { True -> - "Invalid input argument: n < 0. Valid input is n > 0." + "Invalid input argument: n < 0. Valid input is n >= 0." |> Error False -> case n { @@ -172,7 +229,7 @@ pub fn factorial(n) -> Result(Int, String) { |> Ok _ -> 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 } } @@ -184,13 +241,46 @@ pub fn factorial(n) -> Result(Int, String) { /// ///
/// -/// A combinatorial function for computing the number of $$k$$-permuations (without repetitions) -/// of $$n$$ elements: +/// A combinatorial function for computing the number of $$k$$-permutations (without and without +/// 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 +/// 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"] +///
+/// ///
/// Example: /// @@ -221,10 +311,21 @@ pub fn factorial(n) -> Result(Int, String) { /// /// /// -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 { True -> - "Invalid input argument: n < 0. Valid input is n > 0." + "Invalid input argument: n < 0. Valid input is n >= 0." |> Error False -> case k < 0 || k > n { @@ -232,21 +333,39 @@ pub fn permutation(n: Int, k: Int) -> Result(Int, String) { 0 |> Ok False -> - case k == n { - True -> - 1 + case k == 0 { + True -> 1 |> Ok + False -> + list.range(0, k - 1) + |> list.fold(1, fn(acc: Int, x: Int) -> Int { acc * { n - x } }) |> 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 + } + } + } +} + ///
/// /// Spot a typo? Open an issue! @@ -276,35 +395,38 @@ pub fn permutation(n: Int, k: Int) -> Result(Int, String) { /// ///
/// -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 { - True -> - "Invalid input argument: k < 0. Valid input is k > 0." - |> Error + True -> Error("Invalid input argument: k < 0. Valid input is k >= 0.") False -> { case k > list.length(arr) { True -> - "Invalid input argument: k > length(arr). Valid input is 0 < k <= length(arr)." - |> Error - False -> { - do_list_combination(arr, k, []) - |> Ok - } + Error( + "Invalid input argument: k > length(arr). Valid input is 0 <= k <= length(arr).", + ) + False -> Ok(do_list_combination(iterator.from_list(arr), k, [])) } } } } -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 { - 0 -> [list.reverse(prefix)] + 0 -> iterator.single(list.reverse(prefix)) _ -> - case arr { - [] -> [] - [x, ..xs] -> { + case arr |> iterator.step { + iterator.Done -> iterator.empty() + iterator.Next(x, xs) -> { let with_x = do_list_combination(xs, k - 1, [x, ..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 /// 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. -/// ///
/// Example: /// @@ -362,16 +478,35 @@ fn do_list_combination(arr: List(a), k: Int, prefix: List(a)) -> List(List(a)) { /// /// /// -pub fn list_permutation(arr: List(a)) -> List(List(a)) { +pub fn list_permutation(arr: List(a)) -> iterator.Iterator(List(a)) { case arr { - [] -> [[]] - _ -> { - use x <- list.flat_map(arr) - // `x` is drawn from the list `arr` above, - // so Ok(...) can be safely asserted as the result of `list.pop` below - let assert Ok(#(_, remaining)) = list.pop(arr, fn(y) { x == y }) - list.map(list_permutation(remaining), fn(perm) { [x, ..perm] }) - } + [] -> iterator.single([]) + _ -> + iterator.from_list(arr) + // Iterate over each element in the list 'arr' to generate permutations for each possible + // starting element 'x'. + |> iterator.flat_map(fn(x) { + // 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)] + } } } diff --git a/src/gleam_community/maths/elementary.gleam b/src/gleam_community/maths/elementary.gleam index e130e07..0c1a3ba 100644 --- a/src/gleam_community/maths/elementary.gleam +++ b/src/gleam_community/maths/elementary.gleam @@ -54,8 +54,10 @@ //// * [`tau`](#tau) //// * [`e`](#e) //// +//// import gleam/int +import gleam/list import gleam/option ///
diff --git a/test/gleam_community/maths/combinatorics_test.gleam b/test/gleam_community/maths/combinatorics_test.gleam index 0b82534..cef437b 100644 --- a/test/gleam_community/maths/combinatorics_test.gleam +++ b/test/gleam_community/maths/combinatorics_test.gleam @@ -1,4 +1,6 @@ +import gleam/iterator import gleam/list +import gleam/option import gleam/set import gleam_community/maths/combinatorics import gleeunit/should @@ -28,20 +30,24 @@ pub fn int_factorial_test() { pub fn int_combination_test() { // Invalid input gives an error // Error on: n = -1 < 0 - combinatorics.combination(-1, 1) + combinatorics.combination( + -1, + 1, + option.Some(combinatorics.WithoutRepetitions), + ) |> should.be_error() // Valid input returns a result - combinatorics.combination(4, 0) + combinatorics.combination(4, 0, option.Some(combinatorics.WithoutRepetitions)) |> should.equal(Ok(1)) - combinatorics.combination(4, 4) + combinatorics.combination(4, 4, option.Some(combinatorics.WithoutRepetitions)) |> should.equal(Ok(1)) - combinatorics.combination(4, 2) + combinatorics.combination(4, 2, option.Some(combinatorics.WithoutRepetitions)) |> should.equal(Ok(6)) - combinatorics.combination(7, 5) + combinatorics.combination(7, 5, option.Some(combinatorics.WithoutRepetitions)) |> should.equal(Ok(21)) // NOTE: Tests with the 'combination' function that produce values that // exceed precision of the JavaScript 'Number' primitive will result in @@ -51,18 +57,28 @@ pub fn int_combination_test() { pub fn math_permutation_test() { // Invalid input gives an error // Error on: n = -1 < 0 - combinatorics.permutation(-1, 1) + combinatorics.permutation( + -1, + 1, + option.Some(combinatorics.WithoutRepetitions), + ) |> should.be_error() // Valid input returns a result - combinatorics.permutation(4, 0) + combinatorics.permutation(4, 0, option.Some(combinatorics.WithoutRepetitions)) |> should.equal(Ok(1)) - combinatorics.permutation(4, 4) - |> should.equal(Ok(1)) + combinatorics.permutation(4, 4, option.Some(combinatorics.WithoutRepetitions)) + |> should.equal(Ok(24)) - combinatorics.permutation(4, 2) + combinatorics.permutation(4, 2, option.Some(combinatorics.WithoutRepetitions)) |> 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() { @@ -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)]), ) } +// 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() { - // An empty lists returns one (empty) permutation - [] - |> combinatorics.list_permutation() - |> should.equal([[]]) +// // Singleton returns one (singleton) permutation +// // Also works regardless of type of list elements +// ["a"] +// |> combinatorics.list_permutation() +// |> iterator.to_list() +// |> should.equal([["a"]]) - // Singleton returns one (singleton) permutation - // Also works regardless of type of list elements - ["a"] - |> combinatorics.list_permutation() - |> should.equal([["a"]]) +// // Test with some arbitrary inputs +// [1, 2] +// |> combinatorics.list_permutation() +// |> iterator.to_list() +// |> set.from_list() +// |> should.equal(set.from_list([[1, 2], [2, 1]])) - // Test with some arbitrary inputs - [1, 2] - |> combinatorics.list_permutation() - |> set.from_list() - |> should.equal(set.from_list([[1, 2], [2, 1]])) +// // Test with some arbitrary inputs +// [1, 2, 3] +// |> combinatorics.list_permutation() +// |> iterator.to_list() +// |> 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 - [1, 2, 3] - |> combinatorics.list_permutation() - |> 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], - ]), - ) +// // Repeated elements are treated as distinct for the +// // purpose of permutations, so two identical elements +// // will appear "both ways round" +// [1.0, 1.0] +// |> combinatorics.list_permutation() +// |> iterator.to_list() +// |> should.equal([[1.0, 1.0], [1.0, 1.0]]) - // Repeated elements are treated as distinct for the - // purpose of permutations, so two identical elements - // will appear "both ways round" - [1.0, 1.0] - |> combinatorics.list_permutation() - |> should.equal([[1.0, 1.0], [1.0, 1.0]]) +// // This means lists with repeated elements return the +// // same number of permutations as ones without +// ["l", "e", "t", "t", "e", "r", "s"] +// |> combinatorics.list_permutation() +// |> iterator.to_list() +// |> list.length() +// |> should.equal(5040) - // This means lists with repeated elements return the - // same number of permutations as ones without - ["l", "e", "t", "t", "e", "r", "s"] - |> combinatorics.list_permutation() - |> list.length() - |> should.equal(5040) -} +// // Test that the number of generate permutations of the given input list aligns with the computed +// // number of possible permutations (using the formula for the binomial coefficient) +// let arr = ["a", "b", "c", "x", "y", "z"] +// let length = list.length(arr) +// let assert Ok(permuations) = +// combinatorics.permutation( +// length, +// length, +// option.Some(combinatorics.WithoutRepetitions), +// ) -pub fn list_combination_test() { - // A negative number returns an error - [] - |> combinatorics.list_combination(-1) - |> should.be_error() +// arr +// |> combinatorics.list_permutation() +// |> iterator.to_list() +// |> list.length() +// |> should.equal(permuations) +// } - // k is larger than given input list returns an error - [1, 2] - |> combinatorics.list_combination(3) - |> should.be_error() - // An empty lists returns an empty list - [] - |> combinatorics.list_combination(0) - |> should.equal(Ok([[]])) +// pub fn list_combination_test() { +// // Invaldi input: A negative number returns an error +// [] +// |> combinatorics.list_combination(-1) +// |> should.be_error() - // Test with some arbitrary inputs - [1, 2] - |> combinatorics.list_combination(1) - |> should.equal(Ok([[1], [2]])) +// // Invalid input: k is larger than given input list, so it returns an error +// [1, 2] +// |> combinatorics.list_combination(3) +// |> should.be_error() - // Test with some arbitrary inputs - [1, 2] - |> combinatorics.list_combination(2) - |> should.equal(Ok([[1, 2]])) +// // Valid input: An empty lists returns an empty list +// let assert Ok(combinations) = +// [] +// |> combinatorics.list_combination(0) - // Test with some arbitrary inputs - let assert Ok(result) = combinatorics.list_combination([1, 2, 3, 4], 2) - result - |> set.from_list() - |> should.equal( - set.from_list([[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]), - ) +// combinations +// |> iterator.to_list() +// |> should.equal([[]]) - // Test with some arbitrary inputs - let assert Ok(result) = combinatorics.list_combination([1, 2, 3, 4], 3) - result - |> set.from_list() - |> should.equal(set.from_list([[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]])) -} +// // Test with some arbitrary but valid inputs +// let assert Ok(combinations) = +// [1, 2] +// |> combinatorics.list_combination(1) + +// 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]])) +// }