diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fc57b33..568e90c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,7 +95,7 @@ After following the steps above, your exercise should contain _at least_ the fol Further, an entry in `config.json` was added for the exercise. -It may contain further files, e.g. to add additional information or provide extra code. This is the bare minimum. +It may contain further files, e.g. to add additional information or provide extra code. This is the bare minimum. See `README.md` for further formatting standards. Take a look at the `exercise/` directory or commit history for examples, or at this [example](https://github.com/exercism/julia/pull/560) of what a PR adding a new exercise should look like. diff --git a/README.md b/README.md index 1692de22..d9ab52d3 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Likewise, `.meta/example.jl` or `.meta/exemplar.jl` does not need to `import`/`i ### config.json If helper files are needed, `.meta/config.json` should then take an entry under the `"files"` property. -1. If the file is meant to be visible to the student, use `editor`: +1. If the file is meant to be visible to the student (e.g. [Alphametics](https://github.com/exercism/julia/tree/main/exercises/practice/alphametics)), use `editor`: ```json "files": { diff --git a/exercises/practice/alphametics/.docs/instructions.append.md b/exercises/practice/alphametics/.docs/instructions.append.md index 60e62799..4c3f9525 100644 --- a/exercises/practice/alphametics/.docs/instructions.append.md +++ b/exercises/practice/alphametics/.docs/instructions.append.md @@ -1,3 +1,6 @@ # Instructions append You may (or may not!) want to call the function `permutations(a, t)` from [Combinatorics.jl](https://github.com/JuliaMath/Combinatorics.jl) in your solution. + +- If working either in the online editor or locally with `Combinatorics.jl` installed in your environment, `permutations(a, t)` is already in the namespace, but you would normally need to add one of: `using Combinatorics` or `using Combinatorics: permutations` to your solution file. +- If working locally, without `Combinatorics.jl` installed in your environment, you can uncomment the relevant code in `permutations.jl` and add `include("permutations.jl")` at the top of your solution file to access `permutations(a, t)`. You will also need to comment out or remove the line `using Combinatorics: permutations` in `runtests.jl`. diff --git a/exercises/practice/alphametics/.meta/config.json b/exercises/practice/alphametics/.meta/config.json index 9c7c703a..1aab440e 100644 --- a/exercises/practice/alphametics/.meta/config.json +++ b/exercises/practice/alphametics/.meta/config.json @@ -12,6 +12,9 @@ ], "example": [ ".meta/example.jl" + ], + "editor": [ + "permutations.jl" ] }, "blurb": "Given an alphametics puzzle, find the correct solution." diff --git a/exercises/practice/alphametics/.meta/example.jl b/exercises/practice/alphametics/.meta/example.jl index 31763738..9bbb2bb0 100644 --- a/exercises/practice/alphametics/.meta/example.jl +++ b/exercises/practice/alphametics/.meta/example.jl @@ -1,104 +1,53 @@ -### Simple helper functions - -""" - leading_letters(puzzle) - -Return each unique character that leads a word in the puzzle. -""" -function leading_letters(puzzle) - unique(first.(split(puzzle, r"[^A-Z]+"))) -end - -""" - letters(puzzle) - -Return the unique letters in the puzzle in a deterministic order. -""" -letters(puzzle) = unique(c for c in puzzle if c in 'A':'Z') - - -### British Informatics Olympiad inspired brute-force solution - -""" - parse_puzzle(puzzle) - -An alphametic puzzle like "A + A + B == AA" can be represented as an equation -and a list of letters used as leading digits. The equation looks like this: - - A + A + B == 10A + A - -We can rearrange and simplify that to a set of coefficients: - - 2A + B == 11A - -9A + B == 0 - -This function returns three vectors: - -- the unique letters of the puzzle in the order that they are used in the next - two vectors -- coefficients for each letter -- a boolean vector indicating if the letter is ever used as a leading digit - -""" -function parse_puzzle(puzzle::String) - key = letters(puzzle) - lhs = zeros(Int, length(key)) - rhs = zeros(Int, length(key)) - acc = lhs - for token in split(puzzle) - if token == "==" - acc = rhs - elseif token != "+" - parse_word!(acc, key, token) - end - end - coeffs = lhs .- rhs - return key, coeffs, map(∈(leading_letters(puzzle)), key) -end - -""" - parse_word!(acc, key, word) - -`acc` is a vector of coefficients to apply to the variables in `key`. -`word` is a string representing more coefficients for those variables. - -Update `acc` by adding these coefficients together. - -""" -function parse_word!(acc, key, word) - multiplier = 10 ^ (length(word) - 1) - for l in word - acc[findfirst(==(l), key)] += multiplier - multiplier ÷= 10 - end - acc -end - -""" - is_valid(p, coeffs, leads) - -Return true iff `sum(p .* coeffs) == 0` and no leading digit is 0. -""" -function is_valid(p, coeffs, leads) - # Using generators + zip leads to many fewer allocations than .* - sum(p_i * coeffs_i for (p_i, coeffs_i) in zip(p, coeffs)) == 0 && - all(p[l_i] != 0 for (l_i, l) in enumerate(leads) if l) -end - -""" - solve(puzzle::String) - -Return a Dict mapping each letter in the puzzle to an integer in 0:9. - -Words cannot start with 0. -No two letters can share the same value. -""" function solve(puzzle) - (key, coeffs, leads) = parse_puzzle(puzzle) - for p in permutations(0:9, length(key)) - if is_valid(p, coeffs, leads) - return Dict(key .=> p) - end + terms = reverse.(split(replace(puzzle, r"[+=]"=> ""), " ")) + leading = Set(last.(terms)) + addends, result = terms[1:end-1], last(terms) + solution, maxdigits = Dict(), maximum(length, addends) + + function prunedfs(term=1, digit=1, colsum=0) + if term ≤ length(addends) && digit ≤ length(addends[term]) + if haskey(solution, addends[term][digit]) + return prunedfs(term+1, digit, colsum + solution[addends[term][digit]]) + else + for i in 0:9 + iszero(i) && addends[term][digit] ∈ leading && continue + if i ∉ values(solution) + solution[addends[term][digit]] = i + check = prunedfs(term+1, digit, colsum + solution[addends[term][digit]]) + isnothing(check) ? (delete!(solution, addends[term][digit]); nothing) : return check + end + end + end + elseif term ≤ length(addends) + return prunedfs(term+1, digit, colsum) + else + if digit < maxdigits + if result[digit] ∈ leading && iszero(colsum%10) + nothing + elseif haskey(solution, result[digit]) && solution[result[digit]] == colsum%10 + return prunedfs(1, digit+1, colsum÷10) + elseif !haskey(solution, result[digit]) && colsum%10 ∉ values(solution) + solution[result[digit]] = colsum%10 + check = prunedfs(1, digit+1, colsum÷10) + isnothing(check) ? (delete!(solution, result[digit]); nothing) : return check + end + else + check, added, total = [], [], digits(colsum) + if length(total) == length(result[digit:end]) + for (i, ch) in enumerate(result[digit:end]) + !haskey(solution, ch) && total[i] ∈ values(solution) && break + ch ∈ leading && iszero(total[i]) && break + if !haskey(solution, ch) && total[i] ∉ values(solution) + solution[ch] = total[i] + push!(added, ch) + end + push!(check, solution[ch]) + end + check != total ? foreach(ch-> delete!(solution, ch), added) : return solution + end + end + end end - return nothing + + prunedfs() end diff --git a/exercises/practice/alphametics/permutations.jl b/exercises/practice/alphametics/permutations.jl new file mode 100644 index 00000000..04a5b4ed --- /dev/null +++ b/exercises/practice/alphametics/permutations.jl @@ -0,0 +1,115 @@ +# Extract of Combinatorics.jl +# +# You may (or may not!) want to call the function `permutations(a, t)` in your +# solution. +# +# License: +# +# Copyright (c) 2013-2015: Alessandro Andrioni, Jiahao Chen and other +# contributors. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +# Combinatorics/src/factorials.jl + +# TODO: Uncomment below for use locally (not available for web editor) + +# """ +# factorial(n, k) +# Compute ``n!/k!``. +# """ +# function Base.factorial(n::T, k::T) where T<:Integer +# if k < 0 || n < 0 || k > n +# throw(DomainError((n, k), "n and k must be nonnegative with k ≤ n")) +# end +# f = one(T) +# while n > k +# f = Base.checked_mul(f, n) +# n -= 1 +# end +# return f +# end + +# Base.factorial(n::Integer, k::Integer) = factorial(promote(n, k)...) + +# # Combinatorics/src/permutations.jl + +# struct Permutations{T} +# a::T +# t::Int +# end + +# Base.eltype(::Type{Permutations{T}}) where {T} = Vector{eltype(T)} + +# Base.length(p::Permutations) = (0 <= p.t <= length(p.a)) ? factorial(length(p.a), length(p.a)-p.t) : 0 + +# """ +# permutations(a) +# Generate all permutations of an indexable object `a` in lexicographic order. Because the number of permutations +# can be very large, this function returns an iterator object. +# Use `collect(permutations(a))` to get an array of all permutations. +# """ +# permutations(a) = Permutations(a, length(a)) + +# """ +# permutations(a, t) +# Generate all size `t` permutations of an indexable object `a`. +# """ +# function permutations(a, t::Integer) +# if t < 0 +# t = length(a) + 1 +# end +# Permutations(a, t) +# end + +# function Base.iterate(p::Permutations, s = collect(1:length(p.a))) +# (!isempty(s) && max(s[1], p.t) > length(p.a) || (isempty(s) && p.t > 0)) && return +# nextpermutation(p.a, p.t ,s) +# end + +# function nextpermutation(m, t, state) +# perm = [m[state[i]] for i in 1:t] +# n = length(state) +# if t <= 0 +# return(perm, [n+1]) +# end +# s = copy(state) +# if t < n +# j = t + 1 +# while j <= n && s[t] >= s[j]; j+=1; end +# end +# if t < n && j <= n +# s[t], s[j] = s[j], s[t] +# else +# if t < n +# reverse!(s, t+1) +# end +# i = t - 1 +# while i>=1 && s[i] >= s[i+1]; i -= 1; end +# if i > 0 +# j = n +# while j>i && s[i] >= s[j]; j -= 1; end +# s[i], s[j] = s[j], s[i] +# reverse!(s, i+1) +# else +# s[1] = n+1 +# end +# end +# return (perm, s) +# end diff --git a/exercises/practice/alphametics/runtests.jl b/exercises/practice/alphametics/runtests.jl index 7e56b08d..3e8bd90b 100644 --- a/exercises/practice/alphametics/runtests.jl +++ b/exercises/practice/alphametics/runtests.jl @@ -1,4 +1,5 @@ -using Test, Combinatorics +using Test +using Combinatorics: permutations # comment out or remove if Combinatorics.jl is not installed include("alphametics.jl") @@ -72,33 +73,35 @@ include("alphametics.jl") ) end - @testset "puzzle with ten letters" begin - @test solve("AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE") == Dict( - 'A' => 5, - 'D' => 3, - 'E' => 4, - 'F' => 7, - 'G' => 8, - 'N' => 0, - 'O' => 2, - 'R' => 1, - 'S' => 6, - 'T' => 9 - ) - end + # TODO: Combinatorics.jl is currently too slow for the following tests + + # @testset "puzzle with ten letters" begin + # @test solve("AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE") == Dict( + # 'A' => 5, + # 'D' => 3, + # 'E' => 4, + # 'F' => 7, + # 'G' => 8, + # 'N' => 0, + # 'O' => 2, + # 'R' => 1, + # 'S' => 6, + # 'T' => 9 + # ) + # end - @testset "puzzle with ten letters and 199 addends" begin - @test solve("THIS + A + FIRE + THEREFORE + FOR + ALL + HISTORIES + I + TELL + A + TALE + THAT + FALSIFIES + ITS + TITLE + TIS + A + LIE + THE + TALE + OF + THE + LAST + FIRE + HORSES + LATE + AFTER + THE + FIRST + FATHERS + FORESEE + THE + HORRORS + THE + LAST + FREE + TROLL + TERRIFIES + THE + HORSES + OF + FIRE + THE + TROLL + RESTS + AT + THE + HOLE + OF + LOSSES + IT + IS + THERE + THAT + SHE + STORES + ROLES + OF + LEATHERS + AFTER + SHE + SATISFIES + HER + HATE + OFF + THOSE + FEARS + A + TASTE + RISES + AS + SHE + HEARS + THE + LEAST + FAR + HORSE + THOSE + FAST + HORSES + THAT + FIRST + HEAR + THE + TROLL + FLEE + OFF + TO + THE + FOREST + THE + HORSES + THAT + ALERTS + RAISE + THE + STARES + OF + THE + OTHERS + AS + THE + TROLL + ASSAILS + AT + THE + TOTAL + SHIFT + HER + TEETH + TEAR + HOOF + OFF + TORSO + AS + THE + LAST + HORSE + FORFEITS + ITS + LIFE + THE + FIRST + FATHERS + HEAR + OF + THE + HORRORS + THEIR + FEARS + THAT + THE + FIRES + FOR + THEIR + FEASTS + ARREST + AS + THE + FIRST + FATHERS + RESETTLE + THE + LAST + OF + THE + FIRE + HORSES + THE + LAST + TROLL + HARASSES + THE + FOREST + HEART + FREE + AT + LAST + OF + THE + LAST + TROLL + ALL + OFFER + THEIR + FIRE + HEAT + TO + THE + ASSISTERS + FAR + OFF + THE + TROLL + FASTS + ITS + LIFE + SHORTER + AS + STARS + RISE + THE + HORSES + REST + SAFE + AFTER + ALL + SHARE + HOT + FISH + AS + THEIR + AFFILIATES + TAILOR + A + ROOFS + FOR + THEIR + SAFE == FORTRESSES") == Dict( - 'A' => 1, - 'E' => 0, - 'F' => 5, - 'H' => 8, - 'I' => 7, - 'L' => 2, - 'O' => 6, - 'R' => 3, - 'S' => 4, - 'T' => 9 - ) - end + # @testset "puzzle with ten letters and 199 addends" begin + # @test solve("THIS + A + FIRE + THEREFORE + FOR + ALL + HISTORIES + I + TELL + A + TALE + THAT + FALSIFIES + ITS + TITLE + TIS + A + LIE + THE + TALE + OF + THE + LAST + FIRE + HORSES + LATE + AFTER + THE + FIRST + FATHERS + FORESEE + THE + HORRORS + THE + LAST + FREE + TROLL + TERRIFIES + THE + HORSES + OF + FIRE + THE + TROLL + RESTS + AT + THE + HOLE + OF + LOSSES + IT + IS + THERE + THAT + SHE + STORES + ROLES + OF + LEATHERS + AFTER + SHE + SATISFIES + HER + HATE + OFF + THOSE + FEARS + A + TASTE + RISES + AS + SHE + HEARS + THE + LEAST + FAR + HORSE + THOSE + FAST + HORSES + THAT + FIRST + HEAR + THE + TROLL + FLEE + OFF + TO + THE + FOREST + THE + HORSES + THAT + ALERTS + RAISE + THE + STARES + OF + THE + OTHERS + AS + THE + TROLL + ASSAILS + AT + THE + TOTAL + SHIFT + HER + TEETH + TEAR + HOOF + OFF + TORSO + AS + THE + LAST + HORSE + FORFEITS + ITS + LIFE + THE + FIRST + FATHERS + HEAR + OF + THE + HORRORS + THEIR + FEARS + THAT + THE + FIRES + FOR + THEIR + FEASTS + ARREST + AS + THE + FIRST + FATHERS + RESETTLE + THE + LAST + OF + THE + FIRE + HORSES + THE + LAST + TROLL + HARASSES + THE + FOREST + HEART + FREE + AT + LAST + OF + THE + LAST + TROLL + ALL + OFFER + THEIR + FIRE + HEAT + TO + THE + ASSISTERS + FAR + OFF + THE + TROLL + FASTS + ITS + LIFE + SHORTER + AS + STARS + RISE + THE + HORSES + REST + SAFE + AFTER + ALL + SHARE + HOT + FISH + AS + THEIR + AFFILIATES + TAILOR + A + ROOFS + FOR + THEIR + SAFE == FORTRESSES") == Dict( + # 'A' => 1, + # 'E' => 0, + # 'F' => 5, + # 'H' => 8, + # 'I' => 7, + # 'L' => 2, + # 'O' => 6, + # 'R' => 3, + # 'S' => 4, + # 'T' => 9 + # ) + # end end