Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions exercises/practice/alphametics/.docs/instructions.append.md
Original file line number Diff line number Diff line change
@@ -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`.
3 changes: 3 additions & 0 deletions exercises/practice/alphametics/.meta/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
],
"example": [
".meta/example.jl"
],
"editor": [
"permutations.jl"
]
},
"blurb": "Given an alphametics puzzle, find the correct solution."
Expand Down
151 changes: 50 additions & 101 deletions exercises/practice/alphametics/.meta/example.jl
Original file line number Diff line number Diff line change
@@ -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
115 changes: 115 additions & 0 deletions exercises/practice/alphametics/permutations.jl
Original file line number Diff line number Diff line change
@@ -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
61 changes: 32 additions & 29 deletions exercises/practice/alphametics/runtests.jl
Original file line number Diff line number Diff line change
@@ -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")

Expand Down Expand Up @@ -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