From 076b9af0c72a55d3cf861b365e241e33756a2ceb Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:46:53 -0500 Subject: [PATCH 01/11] Add nthcombo functions --- src/combinations.jl | 97 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/combinations.jl b/src/combinations.jl index ec466a9..01c9d1b 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -273,3 +273,100 @@ function powerset(a, min::Integer=0, max::Integer=length(a)) min < 1 && append!(itrs, eltype(a)[]) Iterators.flatten(itrs) end + + +# Nth Combination + +""" + nthcombo(a, k::Int, n::Int) + +Compute the `n`th lexicographic k-combination of the vector `a`. + +# Examples +```jldoctest +julia> collect(combinations([1,2,3], 2)) +3-element Vector{Vector{Int64}}: + [1, 2] + [1, 3] + [2, 3] + +julia> nthperm([1, 2, 3], 2, 1) +2-element Vector{Int64}: + 1 + 2 + +julia> nthperm([1, 2, 3], 2, 2) +2-element Vector{Int64}: + 1 + 3 + +julia> nthperm([1, 2, 3], 2, 4) +ERROR: ArgumentError: combination k must satisfy 0 ≤ k ≤ 3, got 4 +[...] +``` +""" +function nthcombo(a, k::Int, n::Int) + len = length(a) + (k == 0 || k == len) && return collect(a)[1:k] + 0 < k < len || throw(ArgumentError("combination k must satisfy 0 ≤ k ≤ $len, got $k")) + + combo = eltype(a)[] + sizehint!(combo, k) + ncombos = binomial(len-1, k-1) + for i in eachindex(a) + if n ≤ ncombos + @inbounds push!(combo, a[i]) + isone(k) && return combo + k -= 1 + ncombos *= k + else + n -= ncombos + ncombos *= len - k + end + len -= 1 + ncombos ÷= len + end +end + +""" + nthcombo(a, c::Vector) + +Return the integer `n` that generated index-based lexicographic combination `c` from `a`. +Note that `nthcombo(a, nthcombo(a, k, n)) == n` for `1 ≤ n ≤ binomial(length(a), k)` and `unique(a) == a`. +In the case `unique(a) ≠ a`, returns the lowest `n` matching the combination, and +thus is not guaranteed to be the inverse of `nthcombo(a, k, n)`. + +# Examples +```jldoctest +julia> nthcombo([1:3...], nthcombo([1:3...], 2, 3)) + 3 + +julia> collect(combinations([1, 2, 3], 2)) +3-element Vector{Vector{Int64}}: + [1, 2] + [1, 3] + [2, 3] + +julia> nthcombo([1, 2, 3], [1, 2]) + 1 + +julia> nthcombo([1, 2, 3], [2, 3]) + 3 +``` +""" +function nthcombo(a, combo::Vector) + aunique = unique(a) + idxmap = Dict(zip(aunique, 1:length(aunique))) + idxs = [idxmap[v] for v in combo] + ranges = collect(zip([0; idxs[1:end-1]] .+ 1, idxs .- 1)) + m, k = length(a), length(combo) + + n = 1 + for i in 1:k + lower, upper = ranges[i] + if upper - lower ≥ 0 + n += sum(binomial.(m .- lower:upper, k - i)) + end + end + n +end From b9306c01f675e000a592ed3e4231e16daedccdc6 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:06:43 -0500 Subject: [PATCH 02/11] Update combinations.jl Updates docstrings and exports nthcombo --- src/combinations.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/combinations.jl b/src/combinations.jl index 01c9d1b..daef7e8 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -2,7 +2,8 @@ export combinations, CoolLexCombinations, multiset_combinations, with_replacement_combinations, - powerset + powerset, + nthcombo #The Combinations iterator struct Combinations @@ -290,17 +291,17 @@ julia> collect(combinations([1,2,3], 2)) [1, 3] [2, 3] -julia> nthperm([1, 2, 3], 2, 1) +julia> nthcombo([1, 2, 3], 2, 1) 2-element Vector{Int64}: 1 2 -julia> nthperm([1, 2, 3], 2, 2) +julia> nthcombo([1, 2, 3], 2, 2) 2-element Vector{Int64}: 1 3 -julia> nthperm([1, 2, 3], 2, 4) +julia> nthcombo([1, 2, 3], 2, 4) ERROR: ArgumentError: combination k must satisfy 0 ≤ k ≤ 3, got 4 [...] ``` From 0639a9868de53a53f3076b1477a7b43563c6ac4e Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:31:25 -0500 Subject: [PATCH 03/11] Update nthcombo Added `collect` for proper broadcasting behavior. --- src/combinations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/combinations.jl b/src/combinations.jl index daef7e8..59eaa17 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -366,7 +366,7 @@ function nthcombo(a, combo::Vector) for i in 1:k lower, upper = ranges[i] if upper - lower ≥ 0 - n += sum(binomial.(m .- lower:upper, k - i)) + n += sum(binomial.(m .- collect(lower:upper), k - i)) end end n From c560e5030022bcb26d7718657e9610402b9f17a1 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:42:11 -0500 Subject: [PATCH 04/11] Update combinations.jl --- src/combinations.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/combinations.jl b/src/combinations.jl index 59eaa17..7f45416 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -301,7 +301,7 @@ julia> nthcombo([1, 2, 3], 2, 2) 1 3 -julia> nthcombo([1, 2, 3], 2, 4) +julia> nthcombo([1, 2, 3], 4, 2) ERROR: ArgumentError: combination k must satisfy 0 ≤ k ≤ 3, got 4 [...] ``` @@ -340,7 +340,7 @@ thus is not guaranteed to be the inverse of `nthcombo(a, k, n)`. # Examples ```jldoctest julia> nthcombo([1:3...], nthcombo([1:3...], 2, 3)) - 3 +3 julia> collect(combinations([1, 2, 3], 2)) 3-element Vector{Vector{Int64}}: @@ -349,10 +349,10 @@ julia> collect(combinations([1, 2, 3], 2)) [2, 3] julia> nthcombo([1, 2, 3], [1, 2]) - 1 +1 julia> nthcombo([1, 2, 3], [2, 3]) - 3 +3 ``` """ function nthcombo(a, combo::Vector) From dd2c6ff3f6f7a788880c0fcfb4781555ef403995 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:20:13 -0500 Subject: [PATCH 05/11] add input validation --- src/combinations.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/combinations.jl b/src/combinations.jl index 7f45416..90bb854 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -308,12 +308,15 @@ ERROR: ArgumentError: combination k must satisfy 0 ≤ k ≤ 3, got 4 """ function nthcombo(a, k::Int, n::Int) len = length(a) + 0 ≤ k ≤ len || throw(ArgumentError("combination k must satisfy 0 ≤ k ≤ $len, got $k")) + ncombos = binomial(len, k) + 0 < n ≤ ncombos || throw(ArgumentError("n must satisfy 0 < n ≤ $ncombos, got $n")) (k == 0 || k == len) && return collect(a)[1:k] - 0 < k < len || throw(ArgumentError("combination k must satisfy 0 ≤ k ≤ $len, got $k")) combo = eltype(a)[] sizehint!(combo, k) - ncombos = binomial(len-1, k-1) + ncombos *= k + ncombos ÷= len for i in eachindex(a) if n ≤ ncombos @inbounds push!(combo, a[i]) @@ -356,6 +359,7 @@ julia> nthcombo([1, 2, 3], [2, 3]) ``` """ function nthcombo(a, combo::Vector) + length(combo) ≤ length(a) && all(∈(a), combo) || throw(ArgumentError("$combo not a combination of $a")) aunique = unique(a) idxmap = Dict(zip(aunique, 1:length(aunique))) idxs = [idxmap[v] for v in combo] From 2cd0a58d6286dba4ac6c127468bc0e573e279282 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:21:38 -0500 Subject: [PATCH 06/11] Add testing for nthcombo --- test/combinations.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/combinations.jl b/test/combinations.jl index f70a566..4ef520c 100644 --- a/test/combinations.jl +++ b/test/combinations.jl @@ -44,4 +44,24 @@ @test collect(powerset(['a', 'b', 'c'], 1)) == Any[['a'], ['b'], ['c'], ['a', 'b'], ['a', 'c'], ['b', 'c'], ['a', 'b', 'c']] @test collect(powerset(['a', 'b', 'c'], 1, 2)) == Any[['a'], ['b'], ['c'], ['a', 'b'], ['a', 'c'], ['b', 'c']] + # Nth Combo + @test nthcombo([1, 2, 3, 4], 0, 1) == [] + @test nthcombo([1, 2, 3, 4], 4, 1) == [1, 2, 3, 4] + @test nthcombo([1, 2, 3, 4], 3, 2) == [1, 2, 4] + @test all([nthcombo([1, 2, 3, 4], 2, n) for n in 1:binomial(4, 2)] .== collect(combinations([1, 2, 3, 4], 2))) + @test_throws ArgumentError nthcombo([1, 2, 3, 4], 0, 3) + @test_throws ArgumentError nthcombo([1, 2, 3, 4], 5, 3) + @test_throws ArgumentError nthcombo([1, 2, 3, 4], 2, 0) + @test_throws ArgumentError nthcombo([1, 2, 3], 2, 6) + + @test nthcombo([1, 2, 3, 4], []) == 1 + @test nthcombo([1, 2, 3, 4], [1, 2, 3, 4]) == 1 + @test nthcombo([1, 2, 3, 4], [1, 2, 4]) == 2 + @test [nthcombo(1:7, combo) for combo in combinations(1:7, 3)] == collect(1:binomial(7, 3)) + @test_throws ArgumentError nthcombo([1, 2, 3, 4], [1, 5]) + @test_throws ArgumentError nthcombo([1, 2, 3], [1, 2, 3, 3]) + + data = collect(1:n) + @test all([nthcombo(data, nthcombo(data, k, j)) == j for k in 1:7 for j in 1:binomial(7, k)]) + end From ddcc94b76c9fff1e31737fa14cf1ab67fbf5edf1 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:24:35 -0500 Subject: [PATCH 07/11] Update combinations.jl --- test/combinations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/combinations.jl b/test/combinations.jl index 4ef520c..2e230e3 100644 --- a/test/combinations.jl +++ b/test/combinations.jl @@ -61,7 +61,7 @@ @test_throws ArgumentError nthcombo([1, 2, 3, 4], [1, 5]) @test_throws ArgumentError nthcombo([1, 2, 3], [1, 2, 3, 3]) - data = collect(1:n) + data = collect(1:7) @test all([nthcombo(data, nthcombo(data, k, j)) == j for k in 1:7 for j in 1:binomial(7, k)]) end From bed7ccdaa7573666446ac9c49dd304d703a3939b Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:37:48 -0500 Subject: [PATCH 08/11] Update combinations.jl --- src/combinations.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/combinations.jl b/src/combinations.jl index 90bb854..953c8cc 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -359,6 +359,7 @@ julia> nthcombo([1, 2, 3], [2, 3]) ``` """ function nthcombo(a, combo::Vector) + isempty(combo) && return 1 length(combo) ≤ length(a) && all(∈(a), combo) || throw(ArgumentError("$combo not a combination of $a")) aunique = unique(a) idxmap = Dict(zip(aunique, 1:length(aunique))) From 78e73aef18cd4beffe359a4ec70e777506cf2283 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:31:22 -0500 Subject: [PATCH 09/11] add helper function iscombo --- src/combinations.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/combinations.jl b/src/combinations.jl index 953c8cc..1ad6bd0 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -359,8 +359,8 @@ julia> nthcombo([1, 2, 3], [2, 3]) ``` """ function nthcombo(a, combo::Vector) - isempty(combo) && return 1 - length(combo) ≤ length(a) && all(∈(a), combo) || throw(ArgumentError("$combo not a combination of $a")) + iscombo(a, combo) || throw(ArgumentError("$combo not a combination of $a")) + aunique = unique(a) idxmap = Dict(zip(aunique, 1:length(aunique))) idxs = [idxmap[v] for v in combo] @@ -376,3 +376,10 @@ function nthcombo(a, combo::Vector) end n end + +function iscombo(a, combo) + counts = Dict{eltype(a), Int}() + foreach(key -> counts[key] = get(counts, key, 0) + 1, a) + foreach(key -> counts[key] = get(counts, key, 0) - 1, combo) + all(≥(0), (0, values(counts)...)) +end From 085116414e6b33c6f58f1674527f1aa72bbad241 Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:35:05 -0500 Subject: [PATCH 10/11] adjust anonymous function for Julia 1.0 --- src/combinations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/combinations.jl b/src/combinations.jl index 1ad6bd0..2c02bc5 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -381,5 +381,5 @@ function iscombo(a, combo) counts = Dict{eltype(a), Int}() foreach(key -> counts[key] = get(counts, key, 0) + 1, a) foreach(key -> counts[key] = get(counts, key, 0) - 1, combo) - all(≥(0), (0, values(counts)...)) + all(v -> v ≥ 0, (0, values(counts)...)) end From f6c8eb2ab8951f8c7c423ba1f169ef8cc2b4275f Mon Sep 17 00:00:00 2001 From: depial <91621102+depial@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:37:46 -0500 Subject: [PATCH 11/11] support for Julia 1.0 --- src/combinations.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/combinations.jl b/src/combinations.jl index 2c02bc5..9237f41 100644 --- a/src/combinations.jl +++ b/src/combinations.jl @@ -359,6 +359,7 @@ julia> nthcombo([1, 2, 3], [2, 3]) ``` """ function nthcombo(a, combo::Vector) + isempty(combo) && return 1 iscombo(a, combo) || throw(ArgumentError("$combo not a combination of $a")) aunique = unique(a)