From c83f1a8e5c1c6b059a87e3712c381ff06511a15c Mon Sep 17 00:00:00 2001 From: densmojd <93928435+densmojd@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:00:10 -0400 Subject: [PATCH 1/3] Added functions compositions(n, k) and weak_compositions(n, k) for calculating (strong) compositions and weak compositions, respectively, of n into k parts. --- src/Combinatorics.jl | 1 + src/compositions.jl | 168 +++++++++++++++++++++++++++++++++++++++++++ test/compositions.jl | 160 +++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 4 files changed, 330 insertions(+) create mode 100644 src/compositions.jl create mode 100644 test/compositions.jl diff --git a/src/Combinatorics.jl b/src/Combinatorics.jl index 442fd04..76f534d 100644 --- a/src/Combinatorics.jl +++ b/src/Combinatorics.jl @@ -6,6 +6,7 @@ include("factorials.jl") include("combinations.jl") include("permutations.jl") include("partitions.jl") +include("compositions.jl") include("multinomials.jl") include("youngdiagrams.jl") diff --git a/src/compositions.jl b/src/compositions.jl new file mode 100644 index 0000000..eb73739 --- /dev/null +++ b/src/compositions.jl @@ -0,0 +1,168 @@ +export composition, weak_composition + +abstract type AbstractComposition end + +# iterator for generating (strong) compositions of n into k parts +# uses an iterator for generating k - 1 combinations of n - 1 +struct Composition <: AbstractComposition + + # data + n::Int + k::Int + combination::Combinations + + # constructor + Composition(n, k) = new(n, k, Combinations(n - 1, k - 1)) + +end + +# iterator for generating weak compositions of n into k parts +# uses an iterator for generating k - 1 combinations of n + k - 1 +struct WeakComposition <: AbstractComposition + + # data + n::Int + k::Int + combination::Combinations + + # constructor + WeakComposition(n, k) = new(n, k, Combinations(n + k - 1, k - 1)) + +end + +# Base.iterate specializations +function Base.iterate(c::AbstractComposition) + + # get the next combination (and state) + next = iterate(c.combination) + + # return the corresponding composition (and state) + return next_composition(c, next) + +end +function Base.iterate(c::AbstractComposition, state) + + # get the next combination (and state) + next = iterate(c.combination, state) + + # return the corresponding composition (and state) + return next_composition(c, next) + +end + +# common functionality for generating next (strong) composition +function next_composition(c::Composition, next) + + # if we are out of combinations, we are done + next === nothing && return + + # extract the combination and state + combination, state = next + + # set up storage for the composition + q = Vector{Int}(undef, c.k) + + if c.k == 1 + + # k == 1 is a special case, there is one composition n + q[1] = c.n + + else + + # general case - calculate each element of the composition from the + # combination + q[1] = combination[1] + for i in 2 : c.k - 1 + q[i] = combination[i] - combination[i - 1] + end + q[c.k] = c.n - combination[c.k - 1] + + end + + # return the composition and state + return q, state + +end + +# common functionality for generating next weak composition +function next_composition(w::WeakComposition, next) + + # if we are out of combinations, we are done + next === nothing && return + + # extract the combination and state + combination, state = next + + # set up storage for the composition + q = Vector{Int}(undef, w.k) + + if w.k == 1 + + # k == 1 is a special case, there is one composition n + q[1] = w.n + + else + + # general case - calculate each element of the composition from the + # combination + q[1] = combination[1] - 1 + for i in 2 : w.k - 1 + q[i] = combination[i] - combination[i - 1] - 1 + end + q[w.k] = w.n + w.k - combination[w.k - 1] - 1 + + end + + # return the composition and state + return q, state + +end + +# Base.length specializations +Base.length(c::Composition) = binomial(c.n - 1, c.k - 1) +Base.length(w::WeakComposition) = binomial(w.n + w.k - 1, w.n) + +# Base.eltype specialization +Base.eltype(::AbstractComposition) = Vector{Int} + +""" + composition(n, k) + +Generate all arrays of `k` positive integers that sum to `n`, i.e., (strong) +compositions of `n` into `k` parts. Because the number of compositions can be +very large, this function returns an iterator object. Use +`collect(composition(n, k))` to get an array of all compositions. The number of +compositions to generate can be efficiently computed using +`length(compositions(n, k))`. +""" +function composition(n, k) + + # check that n and k are valid + n < k && throw(DomainError(n, "n must be greater than or equal to k")) + k < 1 && throw(DomainError(k, "k must be positive")) + + # return iterator object + return Composition(n, k) + +end + +""" + weak_composition(n, k) + +Generate all arrays of `k` non-negative integers that sum to `n`, i.e., weak +compositions of `n` into `k` parts. Because the number of weak compositions can +be very large, this function returns an iterator object. Use +`collect(weak_composition(n, k))` to get an array of all weak compositions. The +number of weak compositions to generate can be efficiently computed using +`length(weak_compositions(n, k))`. +""" +function weak_composition(n, k) + + # check that n and k are valid + n < 0 && throw(DomainError(n, "n must be non-negative")) + k < 1 && throw(DomainError(k, "k must be positive")) + + # return iterator object + return WeakComposition(n, k) + +end diff --git a/test/compositions.jl b/test/compositions.jl new file mode 100644 index 0000000..fd23aed --- /dev/null +++ b/test/compositions.jl @@ -0,0 +1,160 @@ +@testset "compositions" begin + + # test (strong) compositions + @testset "composition" begin + + # n = 1, k = 1 + @test collect(composition(1, 1)) == [[1]] + @test length(composition(1, 1)) == 1 + + # n = 2, k = 1 + @test collect(composition(2, 1)) == [[2]] + @test length(composition(2, 1)) == 1 + + # n = 2, k = 2 + @test collect(composition(2, 2)) == [[1, 1]] + @test length(composition(2, 2)) == 1 + + # n = 3, k = 1 + @test collect(composition(3, 1)) == [[3]] + @test length(composition(3, 1)) == 1 + + # n = 3, k = 2 + @test collect(composition(3, 2)) == [[1, 2], [2, 1]] + @test length(composition(3, 2)) == 2 + + # n = 3, k = 3 + @test collect(composition(3, 3)) == [[1, 1, 1]] + @test length(composition(3, 3)) == 1 + + # n = 4, k = 1 + @test collect(composition(4, 1)) == [[4]] + @test length(composition(4, 1)) == 1 + + # n = 4, k = 2 + @test collect(composition(4, 2)) == [[1, 3], [2, 2], [3, 1]] + @test length(composition(4, 2)) == 3 + + # n = 4, k = 3 + @test collect(composition(4, 3)) == [[1, 1, 2], [1, 2, 1], [2, 1, 1]] + @test length(composition(4, 3)) == 3 + + # n = 4, k = 4 + @test collect(composition(4, 4)) == [[1, 1, 1, 1]] + @test length(composition(4, 4)) == 1 + + # n = 5, k = 1 + @test collect(composition(5, 1)) == [[5]] + @test length(composition(5, 1)) == 1 + + # n = 5, k = 2 + @test collect(composition(5, 2)) == [[1, 4,], [2, 3], [3, 2], [4, 1]] + @test length(composition(5, 2)) == 4 + + # n = 5, k = 3 + @test collect(composition(5, 3)) == [[1, 1, 3], + [1, 2, 2], + [1, 3, 1], + [2, 1, 2], + [2, 2, 1], + [3, 1, 1]] + @test length(composition(5, 3)) == 6 + + # n = 5, k = 4 + @test collect(composition(5, 4)) == [[1, 1, 1, 2], + [1, 1, 2, 1], + [1, 2, 1, 1], + [2, 1, 1, 1]] + @test length(composition(5, 4)) == 4 + + # n = 5, k = 5 + @test collect(composition(5, 5)) == [[1, 1, 1, 1, 1]] + @test length(composition(5, 5)) == 1 + + # check n less than k throws an error + @test_throws DomainError composition(1, 2) + + # check non-positive k throws an error + @test_throws DomainError composition(1, 0) + + end + + # test weak compositions + @testset "weak_composition" begin + + # n = 0, k = 1 + @test collect(weak_composition(0, 1)) == [[0]] + @test length(weak_composition(0, 1)) == 1 + + # n = 0, k = 2 + @test collect(weak_composition(0, 2)) == [[0, 0]] + @test length(weak_composition(0, 2)) == 1 + + # n = 0, k = 3 + @test collect(weak_composition(0, 3)) == [[0, 0, 0]] + @test length(weak_composition(0, 3)) == 1 + + # n = 1, k = 1 + @test collect(weak_composition(1, 1)) == [[1]] + @test length(weak_composition(1, 1)) == 1 + + # n = 1, k = 2 + @test collect(weak_composition(1, 2)) == [[0, 1], [1, 0]] + @test length(weak_composition(1, 2)) == 2 + + # n = 1, k = 3 + @test collect(weak_composition(1, 3)) == [[0, 0, 1], + [0, 1, 0], + [1, 0, 0]] + @test length(weak_composition(1, 3)) == 3 + + # n = 2, k = 1 + @test collect(weak_composition(2, 1)) == [[2]] + @test length(weak_composition(2, 1)) == 1 + + # n = 2, k = 2 + @test collect(weak_composition(2, 2)) == [[0, 2], [1, 1], [2, 0]] + @test length(weak_composition(2, 2)) == 3 + + # n = 2, k = 3 + @test collect(weak_composition(2, 3)) == [[0, 0, 2], + [0, 1, 1], + [0, 2, 0], + [1, 0, 1], + [1, 1, 0], + [2, 0, 0]] + @test length(weak_composition(2, 3)) == 6 + + # n = 3, k = 1 + @test collect(weak_composition(3, 1)) == [[3]] + @test length(weak_composition(3, 1)) == 1 + + # n = 3, k = 2 + @test collect(weak_composition(3, 2)) == [[0, 3], + [1, 2], + [2, 1], + [3, 0]] + @test length(weak_composition(3, 2)) == 4 + + # n = 3, k = 3 + @test collect(weak_composition(3, 3)) == [[0, 0, 3], + [0, 1, 2], + [0, 2, 1], + [0, 3, 0], + [1, 0, 2], + [1, 1, 1], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0], + [3, 0, 0]] + @test length(weak_composition(3, 3)) == 10 + + # check negative n throws an error + @test_throws DomainError weak_composition(-1, 1) + + # check non-positive k throws an error + @test_throws DomainError weak_composition(1, 0) + + end + +end diff --git a/test/runtests.jl b/test/runtests.jl index d790670..d78dab5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,5 +6,6 @@ include("factorials.jl") include("combinations.jl") include("permutations.jl") include("partitions.jl") +include("compositions.jl") include("multinomials.jl") include("youngdiagrams.jl") From 88389063504c78648097f2e10e7a3feadd75b4e8 Mon Sep 17 00:00:00 2001 From: densmojd <93928435+densmojd@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:40:25 -0400 Subject: [PATCH 2/3] Updated documentation to include new compositions functionality. --- docs/src/api.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/api.md b/docs/src/api.md index fbe74fc..6c97d18 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -7,6 +7,13 @@ Modules = [Combinatorics] Pages = ["combinations.jl"] ``` +## Compositions + +```@autodocs +Modules = [Combinatorics] +Pages = ["compositions.jl"] +``` + ## Factorials ```@autodocs From 3b3446c9fd233a06839b97f49b64ac074270465f Mon Sep 17 00:00:00 2001 From: densmojd <93928435+densmojd@users.noreply.github.com> Date: Thu, 28 Sep 2023 10:52:07 -0400 Subject: [PATCH 3/3] Fixed eltype to take type argument rather than instance. Added a corresponding test. --- src/compositions.jl | 2 +- test/compositions.jl | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/compositions.jl b/src/compositions.jl index eb73739..f74f5c2 100644 --- a/src/compositions.jl +++ b/src/compositions.jl @@ -123,7 +123,7 @@ Base.length(c::Composition) = binomial(c.n - 1, c.k - 1) Base.length(w::WeakComposition) = binomial(w.n + w.k - 1, w.n) # Base.eltype specialization -Base.eltype(::AbstractComposition) = Vector{Int} +Base.eltype(::Type{<:AbstractComposition}) = Vector{Int} """ composition(n, k) diff --git a/test/compositions.jl b/test/compositions.jl index fd23aed..846eb3b 100644 --- a/test/compositions.jl +++ b/test/compositions.jl @@ -77,6 +77,9 @@ # check non-positive k throws an error @test_throws DomainError composition(1, 0) + # test eltype + @test eltype(composition(1, 1)) == Vector{Int} + end # test weak compositions @@ -155,6 +158,9 @@ # check non-positive k throws an error @test_throws DomainError weak_composition(1, 0) + # test eltype + @test eltype(weak_composition(1, 1)) == Vector{Int} + end end