Skip to content

Commit 8a7a8ac

Browse files
natemcintoshbkaminsararslan
authored
faster permutations, based on what rdeits suggested (#122)
* update permutations to what rdeits suggested also add testsets * remove old code * add suggestions from bkamins * add offset arrays for testing purposes only * add necessary libs to test dependencies * Added tests for offsetarrays attempt at fix does not work yet * Apply suggestions from code review Thanks @bkamins for the help on this! Co-authored-by: Bogumił Kamiński <[email protected]> * all tests now passing * apply whitespace suggestions * try to make julia 1.0 happy * Add OffsetArrays as a test dependency Julia 1.0/1.1 syntax. See https://pkgdocs.julialang.org/v1/creating-packages/#Test-specific-dependencies-in-Julia-1.2-and-above * Apply suggestions from code review Co-authored-by: Bogumił Kamiński <[email protected]> * everything seems to be working now * Return empty vector for invalid t args * special case for t=1 * updates to reduce size of type union * A few more explanatory comments * Add suggestions from palday and jishnub * Change name of iterator to match convention * Update src/permutations.jl Co-authored-by: Alex Arslan <[email protected]> * Update src/permutations.jl Co-authored-by: Alex Arslan <[email protected]> * Update src/permutations.jl Co-authored-by: Alex Arslan <[email protected]> * Update src/permutations.jl Co-authored-by: Alex Arslan <[email protected]> * fix tests, and don't use Big --------- Co-authored-by: Bogumił Kamiński <[email protected]> Co-authored-by: Alex Arslan <[email protected]>
1 parent d1b633b commit 8a7a8ac

File tree

10 files changed

+456
-322
lines changed

10 files changed

+456
-322
lines changed

Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ julia = "1"
77

88
[extras]
99
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
10+
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
1011
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1112

1213
[targets]
13-
test = ["LinearAlgebra", "Test"]
14+
test = ["LinearAlgebra", "OffsetArrays", "Test"]

src/permutations.jl

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,66 +10,122 @@ export
1010

1111

1212
struct Permutations{T}
13-
a::T
14-
t::Int
13+
data::T
14+
length::Int
1515
end
1616

17-
Base.eltype(::Type{Permutations{T}}) where {T} = Vector{eltype(T)}
17+
function has_repeats(state::Vector{Int})
18+
# This can be safely marked inbounds because of the type restriction in the signature.
19+
# If the type restriction is ever loosened, please check safety of the `@inbounds`
20+
@inbounds for outer in eachindex(state)
21+
for inner in (outer+1):lastindex(state)
22+
if state[outer] == state[inner]
23+
return true
24+
end
25+
end
26+
end
27+
return false
28+
end
29+
30+
function increment!(state::Vector{Int}, min::Int, max::Int)
31+
state[end] += 1
32+
for i in reverse(eachindex(state))[firstindex(state):end-1]
33+
if state[i] > max
34+
state[i] = min
35+
state[i-1] += 1
36+
end
37+
end
38+
end
39+
40+
function next_permutation!(state::Vector{Int}, min::Int, max::Int)
41+
while true
42+
increment!(state, min, max)
43+
has_repeats(state) || break
44+
end
45+
end
46+
47+
function Base.iterate(p::Permutations, state::Vector{Int}=fill(firstindex(p.data), p.length))
48+
next_permutation!(state, firstindex(p.data), lastindex(p.data))
49+
if first(state) > lastindex(p.data)
50+
return nothing
51+
end
52+
[p.data[i] for i in state], state
53+
end
54+
55+
function Base.length(p::Permutations)
56+
length(p.data) < p.length && return 0
57+
return Int(prod(length(p.data) - p.length + 1:length(p.data)))
58+
end
59+
60+
Base.eltype(p::Permutations) = Vector{eltype(p.data)}
61+
62+
Base.IteratorSize(p::Permutations) = Base.HasLength()
1863

19-
Base.length(p::Permutations) = (0 <= p.t <= length(p.a)) ? factorial(length(p.a), length(p.a)-p.t) : 0
2064

2165
"""
2266
permutations(a)
2367
2468
Generate all permutations of an indexable object `a` in lexicographic order. Because the number of permutations
2569
can be very large, this function returns an iterator object.
2670
Use `collect(permutations(a))` to get an array of all permutations.
71+
Only works for `a` with defined length.
2772
"""
28-
permutations(a) = Permutations(a, length(a))
73+
permutations(a) = permutations(a, length(a))
2974

3075
"""
3176
permutations(a, t)
3277
3378
Generate all size `t` permutations of an indexable object `a`.
79+
Only works for `a` with defined length.
80+
If `(t <= 0) || (t > length(a))`, then returns an empty vector of eltype of `a`
3481
"""
3582
function permutations(a, t::Integer)
36-
if t < 0
37-
t = length(a) + 1
83+
if t == 0
84+
# Correct behavior for a permutation of length 0 is a vector containing a single empty vector
85+
return [Vector{eltype(a)}()]
86+
elseif t == 1
87+
# Easy case, just return each element in its own vector
88+
return [[ai] for ai in a]
89+
elseif t < 0 || t > length(a)
90+
# Correct behavior for a permutation of these lengths is a an empty vector (of the correct type)
91+
return Vector{Vector{eltype(a)}}()
3892
end
39-
Permutations(a, t)
93+
return Permutations(a, t)
4094
end
4195

42-
function Base.iterate(p::Permutations, s = collect(1:length(p.a)))
43-
(!isempty(s) && max(s[1], p.t) > length(p.a) || (isempty(s) && p.t > 0)) && return
44-
nextpermutation(p.a, p.t ,s)
45-
end
4696

4797
function nextpermutation(m, t, state)
4898
perm = [m[state[i]] for i in 1:t]
4999
n = length(state)
50100
if t <= 0
51-
return(perm, [n+1])
101+
return (perm, [n + 1])
52102
end
53103
s = copy(state)
54104
if t < n
55105
j = t + 1
56-
while j <= n && s[t] >= s[j]; j+=1; end
106+
while j <= n && s[t] >= s[j]
107+
j += 1
108+
end
57109
end
58110
if t < n && j <= n
59111
s[t], s[j] = s[j], s[t]
60112
else
61113
if t < n
62-
reverse!(s, t+1)
114+
reverse!(s, t + 1)
63115
end
64116
i = t - 1
65-
while i>=1 && s[i] >= s[i+1]; i -= 1; end
117+
while i >= 1 && s[i] >= s[i+1]
118+
i -= 1
119+
end
66120
if i > 0
67121
j = n
68-
while j>i && s[i] >= s[j]; j -= 1; end
122+
while j > i && s[i] >= s[j]
123+
j -= 1
124+
end
69125
s[i], s[j] = s[j], s[i]
70-
reverse!(s, i+1)
126+
reverse!(s, i + 1)
71127
else
72-
s[1] = n+1
128+
s[1] = n + 1
73129
end
74130
end
75131
return (perm, s)
@@ -94,18 +150,18 @@ function Base.length(c::MultiSetPermutations)
94150
else
95151
g = [factorial(i) for i in 0:t]
96152
end
97-
p = [g[t+1]; zeros(Float64,t)]
153+
p = [g[t+1]; zeros(Float64, t)]
98154
for i in 1:length(c.f)
99155
f = c.f[i]
100156
if i == 1
101157
for j in 1:min(f, t)
102-
p[j+1] = g[t+1]/g[j+1]
158+
p[j+1] = g[t+1] / g[j+1]
103159
end
104160
else
105161
for j in t:-1:1
106162
q = 0
107-
for k in (j+1):-1:max(1,j+1-f)
108-
q += p[k]/g[j+2-k]
163+
for k in (j+1):-1:max(1, j + 1 - f)
164+
q += p[k] / g[j+2-k]
109165
end
110166
p[j+1] = q
111167
end
@@ -134,7 +190,7 @@ function multiset_permutations(m, f::Vector{<:Integer}, t::Integer)
134190
MultiSetPermutations(m, f, t, ref)
135191
end
136192

137-
function Base.iterate(p::MultiSetPermutations, s = p.ref)
193+
function Base.iterate(p::MultiSetPermutations, s=p.ref)
138194
(!isempty(s) && max(s[1], p.t) > length(p.ref) || (isempty(s) && p.t > 0)) && return
139195
nextpermutation(p.m, p.t, s)
140196
end
@@ -151,7 +207,7 @@ function nthperm!(a::AbstractVector, k::Integer)
151207
f = factorial(oftype(k, n))
152208
0 < k <= f || throw(ArgumentError("permutation k must satisfy 0 < k ≤ $f, got $k"))
153209
k -= 1 # make k 1-indexed
154-
for i=1:n-1
210+
for i = 1:n-1
155211
f ÷= n - i + 1
156212
j = k ÷ f
157213
k -= j * f
@@ -182,7 +238,7 @@ function nthperm(p::AbstractVector{<:Integer})
182238
isperm(p) || throw(ArgumentError("argument is not a permutation"))
183239
k, n = 1, length(p)
184240
for i = 1:n-1
185-
f = factorial(n-i)
241+
f = factorial(n - i)
186242
for j = i+1:n
187243
k += ifelse(p[j] < p[i], f, 0)
188244
end
@@ -193,9 +249,10 @@ end
193249

194250
# Parity of permutations
195251

196-
const levicivita_lut = cat([0 0 0; 0 0 1; 0 -1 0],
197-
[0 0 -1; 0 0 0; 1 0 0],
198-
[0 1 0; -1 0 0; 0 0 0]; dims=3)
252+
const levicivita_lut = cat([0 0 0; 0 0 1; 0 -1 0],
253+
[0 0 -1; 0 0 0; 1 0 0],
254+
[0 1 0; -1 0 0; 0 0 0];
255+
dims=3)
199256

200257
"""
201258
levicivita(p)

test/Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[deps]
2+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
3+
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
4+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

test/combinations.jl

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
1-
@test [combinations([])...] == []
2-
@test [combinations(['a', 'b', 'c'])...] == [['a'],['b'],['c'],['a','b'],['a','c'],['b','c'],['a','b','c']]
3-
4-
@test [combinations("abc",3)...] == [['a','b','c']]
5-
@test [combinations("abc",2)...] == [['a','b'],['a','c'],['b','c']]
6-
@test [combinations("abc",1)...] == [['a'],['b'],['c']]
7-
@test [combinations("abc",0)...] == [[]]
8-
@test [combinations("abc",-1)...] == []
9-
10-
@test filter(x->iseven(x[1]),[combinations([1,2,3],2)...]) == Any[[2,3]]
11-
12-
# multiset_combinations
13-
@test [multiset_combinations("aabc", 5)...] == Any[]
14-
@test [multiset_combinations("aabc", 2)...] == Any[['a','a'],['a','b'],['a','c'],['b','c']]
15-
@test [multiset_combinations("aabc", 1)...] == Any[['a'],['b'],['c']]
16-
@test [multiset_combinations("aabc", 0)...] == Any[Char[]]
17-
@test [multiset_combinations("aabc", -1)...] == Any[]
18-
@test [multiset_combinations("", 1)...] == Any[]
19-
@test [multiset_combinations("", 0)...] == Any[Char[]]
20-
@test [multiset_combinations("", -1)...] == Any[]
21-
22-
# with_replacement_combinations
23-
@test [with_replacement_combinations("abc", 2)...] == Any[['a','a'],['a','b'],['a','c'],
24-
['b','b'],['b','c'],['c','c']]
25-
@test [with_replacement_combinations("abc", 1)...] == Any[['a'],['b'],['c']]
26-
@test [with_replacement_combinations("abc", 0)...] == Any[Char[]]
27-
@test [with_replacement_combinations("abc", -1)...] == Any[]
28-
@test [with_replacement_combinations("", 1)...] == Any[]
29-
@test [with_replacement_combinations("", 0)...] == Any[Char[]]
30-
@test [with_replacement_combinations("", -1)...] == Any[]
31-
32-
33-
#cool-lex iterator
34-
@test_throws DomainError [CoolLexCombinations(-1, 1)...]
35-
@test_throws DomainError [CoolLexCombinations(5, 0)...]
36-
@test [CoolLexCombinations(4,2)...] == Vector[[1,2], [2,3], [1,3], [2,4], [3,4], [1,4]]
37-
@test isa(iterate(CoolLexCombinations(1000, 20))[2], Combinatorics.CoolLexIterState{BigInt})
38-
39-
# Power set
40-
@test collect(powerset([])) == Any[[]]
41-
@test collect(powerset(['a', 'b', 'c'])) == Any[[],['a'],['b'],['c'],['a','b'],['a','c'],['b','c'],['a','b','c']]
42-
@test collect(powerset(['a', 'b', 'c'], 1)) == Any[['a'],['b'],['c'],['a','b'],['a','c'],['b','c'],['a','b','c']]
43-
@test collect(powerset(['a', 'b', 'c'], 1, 2)) == Any[['a'],['b'],['c'],['a','b'],['a','c'],['b','c']]
1+
@testset "combinations" begin
2+
@test [combinations([])...] == []
3+
@test [combinations(['a', 'b', 'c'])...] == [['a'], ['b'], ['c'], ['a', 'b'], ['a', 'c'], ['b', 'c'], ['a', 'b', 'c']]
4+
5+
@test [combinations("abc", 3)...] == [['a', 'b', 'c']]
6+
@test [combinations("abc", 2)...] == [['a', 'b'], ['a', 'c'], ['b', 'c']]
7+
@test [combinations("abc", 1)...] == [['a'], ['b'], ['c']]
8+
@test [combinations("abc", 0)...] == [[]]
9+
@test [combinations("abc", -1)...] == []
10+
11+
@test filter(x -> iseven(x[1]), [combinations([1, 2, 3], 2)...]) == Any[[2, 3]]
12+
13+
# multiset_combinations
14+
@test [multiset_combinations("aabc", 5)...] == Any[]
15+
@test [multiset_combinations("aabc", 2)...] == Any[['a', 'a'], ['a', 'b'], ['a', 'c'], ['b', 'c']]
16+
@test [multiset_combinations("aabc", 1)...] == Any[['a'], ['b'], ['c']]
17+
@test [multiset_combinations("aabc", 0)...] == Any[Char[]]
18+
@test [multiset_combinations("aabc", -1)...] == Any[]
19+
@test [multiset_combinations("", 1)...] == Any[]
20+
@test [multiset_combinations("", 0)...] == Any[Char[]]
21+
@test [multiset_combinations("", -1)...] == Any[]
22+
23+
# with_replacement_combinations
24+
@test [with_replacement_combinations("abc", 2)...] == Any[['a', 'a'], ['a', 'b'], ['a', 'c'],
25+
['b', 'b'], ['b', 'c'], ['c', 'c']]
26+
@test [with_replacement_combinations("abc", 1)...] == Any[['a'], ['b'], ['c']]
27+
@test [with_replacement_combinations("abc", 0)...] == Any[Char[]]
28+
@test [with_replacement_combinations("abc", -1)...] == Any[]
29+
@test [with_replacement_combinations("", 1)...] == Any[]
30+
@test [with_replacement_combinations("", 0)...] == Any[Char[]]
31+
@test [with_replacement_combinations("", -1)...] == Any[]
32+
33+
34+
#cool-lex iterator
35+
@test_throws DomainError [CoolLexCombinations(-1, 1)...]
36+
@test_throws DomainError [CoolLexCombinations(5, 0)...]
37+
@test [CoolLexCombinations(4, 2)...] == Vector[[1, 2], [2, 3], [1, 3], [2, 4], [3, 4], [1, 4]]
38+
@test isa(iterate(CoolLexCombinations(1000, 20))[2], Combinatorics.CoolLexIterState{BigInt})
39+
40+
# Power set
41+
@test collect(powerset([])) == Any[[]]
42+
@test collect(powerset(['a', 'b', 'c'])) == Any[[], ['a'], ['b'], ['c'], ['a', 'b'], ['a', 'c'], ['b', 'c'], ['a', 'b', 'c']]
43+
@test collect(powerset(['a', 'b', 'c'], 1)) == Any[['a'], ['b'], ['c'], ['a', 'b'], ['a', 'c'], ['b', 'c'], ['a', 'b', 'c']]
44+
@test collect(powerset(['a', 'b', 'c'], 1, 2)) == Any[['a'], ['b'], ['c'], ['a', 'b'], ['a', 'c'], ['b', 'c']]
45+
46+
end

test/factorials.jl

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,41 @@
1-
@test factorial(7,3) == 7*6*5*4
2-
@test_throws DomainError factorial(3,7)
3-
@test_throws DomainError factorial(-3,-7)
4-
@test_throws DomainError factorial(-7,-3)
5-
#JuliaLang/julia#9943
6-
@test factorial(big(100), (80)) == 1303995018204712451095685346159820800000
7-
#JuliaLang/julia#9950
8-
@test_throws OverflowError factorial(1000,80)
9-
10-
# derangement
11-
@test derangement(4) == subfactorial(4) == 9
12-
@test derangement(24) == parse(BigInt,"228250211305338670494289")
13-
14-
# partialderangement
15-
@test partialderangement(7, 3) == 315
16-
@test_throws DomainError partialderangement(8, 9)
17-
@test_throws DomainError partialderangement(-8, 0)
18-
19-
# doublefactorial
20-
@test doublefactorial(70) == parse(BigInt,"355044260642859198243475901411974413130137600000000")
21-
@test_throws DomainError doublefactorial(-1)
22-
23-
# hyperfactorial
24-
@test hyperfactorial(8) == parse(BigInt,"55696437941726556979200000")
25-
@test hyperfactorial(0) == parse(BigInt,"1")
26-
@test hyperfactorial(1) == parse(BigInt,"1")
27-
@test hyperfactorial(2) == parse(BigInt,"4")
28-
29-
# multifactorial
30-
@test multifactorial(40,2) == doublefactorial(40)
31-
@test_throws DomainError multifactorial(-1,1)
32-
33-
# multinomial
34-
@test multinomial(1,4,4,2) == 34650
35-
36-
# primorial
37-
@test primorial(17) == 510510
38-
@test_throws DomainError primorial(-1)
1+
@testset "factorials" begin
2+
@test factorial(7, 3) == 7 * 6 * 5 * 4
3+
@test_throws DomainError factorial(3, 7)
4+
@test_throws DomainError factorial(-3, -7)
5+
@test_throws DomainError factorial(-7, -3)
6+
#JuliaLang/julia#9943
7+
@test factorial(big(100), (80)) == 1303995018204712451095685346159820800000
8+
#JuliaLang/julia#9950
9+
@test_throws OverflowError factorial(1000, 80)
10+
11+
# derangement
12+
@test derangement(4) == subfactorial(4) == 9
13+
@test derangement(24) == parse(BigInt, "228250211305338670494289")
14+
15+
# partialderangement
16+
@test partialderangement(7, 3) == 315
17+
@test_throws DomainError partialderangement(8, 9)
18+
@test_throws DomainError partialderangement(-8, 0)
19+
20+
# doublefactorial
21+
@test doublefactorial(70) == parse(BigInt, "355044260642859198243475901411974413130137600000000")
22+
@test_throws DomainError doublefactorial(-1)
23+
24+
# hyperfactorial
25+
@test hyperfactorial(8) == parse(BigInt, "55696437941726556979200000")
26+
@test hyperfactorial(0) == parse(BigInt, "1")
27+
@test hyperfactorial(1) == parse(BigInt, "1")
28+
@test hyperfactorial(2) == parse(BigInt, "4")
29+
30+
# multifactorial
31+
@test multifactorial(40, 2) == doublefactorial(40)
32+
@test_throws DomainError multifactorial(-1, 1)
33+
34+
# multinomial
35+
@test multinomial(1, 4, 4, 2) == 34650
36+
37+
# primorial
38+
@test primorial(17) == 510510
39+
@test_throws DomainError primorial(-1)
40+
41+
end

0 commit comments

Comments
 (0)