I am often asked βwhat is APL good forβ? I reply that APL is good for almost anything but that it is also very good at prototyping. With it you can experiment and use it as a tool for thinking about the problem at hand. It is easy in APL to manipulate data and build tools to get a better view of the problem and come up with solutions. In the following text we will use APL to think of a solution to a problem involving calculations to solve a mathematical problem. The problem originates from Kakuro[1], a popular puzzle found in newspapers, where you need to know the sets of numbers making up a solution.
The problem
In this problem we need to come up with all the sets of N unique positive single digit numbers (1..9) making up a particular sum S. N and S are the key numbers here, they will be the input to our problem. The output is all the possible sets. The format is unimportant; it could be e.g. a list of sets or a square matrix, N wide.
Most of the code should work in any modern APL. However, the examples were created with Dyalog Version 14. When features are used which are available in Dyalog APL only this is mentioned. βIOβ1
is assumed.
For example, there are only 2 sets of 4 unique digits 1 to 9 adding up to 12: (1 2 3 6) and (1 2 4 5).
Attempt #1
The first thought is the easiest: brute force. Can we generate all the possibilities and screen out unwanted ones?
We need to form sequences of N numbers, each from 1 to 9. For example, pairs are (1,1) (1,2) (1,3)β¦(2,1) (2,2)β¦ (9,9), 81 combinations in all. We can use catenate (,
) to put numbers together:
i3 β β³3 β define a vector of the numbers 1, 2 and 3 i3 , i3 1 2 3 1 2 3
Not quite what we want, we want to catenate each number to each other:
i3 ,Β¨ i3 1 1 2 2 3 3
In Dyalog APL V14 there is a new user command that allows us to box enclosed arrays automatically to better see their nature:
]box on Was OFF i3 ,Β¨ i3 βββββ¬ββββ¬ββββ β1 1β2 2β3 3β βββββ΄ββββ΄ββββ
Again this is not what we want, what we want is to do the catenation for each element in i3
to each other element in i3
, like this:
1 ,Β¨ i3 βββββ¬ββββ¬ββββ β1 1β1 2β1 3β βββββ΄ββββ΄ββββ 2 ,Β¨ i3 βββββ¬ββββ¬ββββ β2 1β2 2β2 3β βββββ΄ββββ΄ββββ 3 ,Β¨ i3 βββββ¬ββββ¬ββββ β3 1β3 2β3 3β βββββ΄ββββ΄ββββ
APL allows us to do this nicely, distributing the function ,
without looping, using jot-dot (β.
) :
i3 β., i3 βββββ¬ββββ¬ββββ β1 1β1 2β1 3β βββββΌββββΌββββ€ β2 1β2 2β2 3β βββββΌββββΌββββ€ β3 1β3 2β3 3β βββββ΄ββββ΄ββββ
Thatβs better. This will work is all modern APLs. In Dyalog we can use commute (β¨
) to avoid repeating the argument. Commute normally swaps (commutes) the arguments of a function so aββ¨b
becomes bβa
, but when used monadically it repeats the argument so +β¨a
becomes a+a
:
β., β¨ i3 βββββ¬ββββ¬ββββ β1 1β1 2β1 3β βββββΌββββΌββββ€ β2 1β2 2β2 3β βββββΌββββΌββββ€ β3 1β3 2β3 3β βββββ΄ββββ΄ββββ
Let’s do it for the numbers 1 to 9:
β., β¨ β³9 βββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ β1 1β1 2β1 3β1 4β1 5β1 6β1 7β1 8β1 9β βββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββ€ β2 1β2 2β2 3β2 4β2 5β2 6β2 7β2 8β2 9β βββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββ€ β3 1β3 2β3 3β3 4β3 5β3 6β3 7β3 8β3 9β βββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββ€ β4 1β4 2β4 3β4 4β4 5β4 6β4 7β4 8β4 9β βββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββ€ β5 1β5 2β5 3β5 4β5 5β5 6β5 7β5 8β5 9β βββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββ€ β6 1β6 2β6 3β6 4β6 5β6 6β6 7β6 8β6 9β βββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββ€ β7 1β7 2β7 3β7 4β7 5β7 6β7 7β7 8β7 9β βββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββ€ β8 1β8 2β8 3β8 4β8 5β8 6β8 7β8 8β8 9β βββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββΌββββ€ β9 1β9 2β9 3β9 4β9 5β9 6β9 7β9 8β9 9β βββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ
Let’s keep this list in a variable and find the sum of each and let’s find those that add up to, say, 6:
vβ , β.,β¨ β³9 β turn the table into a list with ravel (,) sumβ +/Β¨ v β sum each set sixβ 6=sum β find the 6s six/v β extract them βββββ¬ββββ¬ββββ¬ββββ¬ββββ β1 5β2 4β3 3β4 2β5 1β βββββ΄ββββ΄ββββ΄ββββ΄ββββ
Let’s write a function to find pairs adding up to a specific number. Here weβll use Dyalogβs dynamic functions:
pairsβ{okββ΅=+/Β¨allβ, β., β¨ β³9 β ok/all} pairs 6 βββββ¬ββββ¬ββββ¬ββββ¬ββββ β1 5β2 4β3 3β4 2β5 1β βββββ΄ββββ΄ββββ΄ββββ΄ββββ
There are 2 problems with this code:
#1, the rule says digits must be unique, so letβs add code to only keep the numbers that are different:
pairsβ{okββ΅=+/Β¨allβ, β.,β¨ β³9 β okβok β§ β /Β¨all β ok/all} pairs 6 βββββ¬ββββ¬ββββ¬ββββ β1 5β2 4β4 2β5 1β βββββ΄ββββ΄ββββ΄ββββ
That’s better, but we still need to solve problem #2: some are duplicates, e.g. (1 5) and (5 1) are the same. We should remove them. Let’s create a sorting function to reorder the sets:
Sortβ{β΅[ββ΅]}
and use it in our function to reorder each set and use unique (βͺ
) to extract the unique ones:
pairsβ{okββ΅=+/Β¨allβ,β.,β¨β³9 β okβokβ§β /Β¨all β βͺ SortΒ¨ ok/all} pairs 6 β pairs that add up to 6 βββββ¬ββββ β1 5β2 4β βββββ΄ββββ pairs 9 β pairs that add up to 9 βββββ¬ββββ¬ββββ¬ββββ β1 8β2 7β3 6β4 5β βββββ΄ββββ΄ββββ΄ββββ
Looks good. What about triples? We can use β.,
twice:
i3 β., i3 β., i3 β generate a 3 x 3 x 3 of 1, 2 and 3s βββββββ¬ββββββ¬ββββββ β1 1 1β1 1 2β1 1 3β βββββββΌββββββΌββββββ€ β1 2 1β1 2 2β1 2 3β βββββββΌββββββΌββββββ€ β1 3 1β1 3 2β1 3 3β βββββββ΄ββββββ΄ββββββ βββββββ¬ββββββ¬ββββββ β2 1 1β2 1 2β2 1 3β βββββββΌββββββΌββββββ€ β2 2 1β2 2 2β2 2 3β βββββββΌββββββΌββββββ€ β2 3 1β2 3 2β2 3 3β βββββββ΄ββββββ΄ββββββ βββββββ¬ββββββ¬ββββββ β3 1 1β3 1 2β3 1 3β βββββββΌββββββΌββββββ€ β3 2 1β3 2 2β3 2 3β βββββββΌββββββΌββββββ€ β3 3 1β3 3 2β3 3 3β βββββββ΄ββββββ΄ββββββ triplesβ{ (β΅=+/Β¨all)/ allβ, i β., i β., iββ³9 } triples 6 βββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ β1 1 4β1 2 3β1 3 2β1 4 1β2 1 3β2 2 2β2 3 1β3 1 2β3 2 1β4 1 1β βββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ
Some doubles are still there – we can’t use β
this time. β /
on more than 2 numbers is meaningless:
β /4 1 1 β same as 4β (1β 1) or 4β FALSE!!! 1
Weβll use the nub (unique) of each set to see if it is valid with function {β΅β‘βͺβ΅}
: if the digits are unique weβll keep the set. Weβll then sort each set and keep the unique ones:
cleanβ{ βͺSortΒ¨ ({β΅β‘βͺβ΅}Β¨β΅)/β΅ } triplesβ{allβ(β΅=+/Β¨all)/allβ,i β., i β., iββ³9 β clean all} triples 6 βββββββ β1 2 3β βββββββ
That’s better; we now need to write a function that will do it for any number of digits. The left argument will be the number of digits required. That means looping over β.,
until we have the proper number of iterations. In Dyalog the power operator (β£
) will help with this, it will do the looping for us:
(i3 β., i3 β., i3) β‘ (i3 (β., β£ 2) i3) 1
So we can write (for N digits we need to run β.,
N-1 times)
NCatβ{β΅ (β., β£(βΊ-1)) β΅}
Or, since the argument is the same on both sides, we can use β¨
:
NCatβ{ β., β£(βΊ-1)β¨ β΅} ntupleβ{okββ΅=+/Β¨allβ, βΊ NCat β³9 β allβok/all β clean all }
Letβs find how many ways we can make twelve with four different numbers:
4 ntuple 12 βββββββββ¬ββββββββ β1 2 3 6β1 2 4 5β βββββββββ΄ββββββββ β΄4 ntuple 12 2
Just two. There should also be only one way for 9 digits to add up to 45 (all the numbers 1 to 9):
β΄9 ntuple 45 WS FULL NCat[0] NCatβ{β.,β£(βΊ-1)β¨β΅} β§
Oops! Looks like we have a problem Houston. We’re trying to generate
9 Γ 9*9 3486784401
more than 3 billion numbers! This is too big on my machine, even with βwa=64039748
.
6 ntuple 35 βββββββββββββ¬ββββββββββββ¬ββββββββββββ¬ββββββββββββ β1 4 6 7 8 9β2 3 6 7 8 9β2 4 5 7 8 9β3 4 5 6 8 9β βββββββββββββ΄ββββββββββββ΄ββββββββββββ΄ββββββββββββ 7 ntuple 39 WS FULL NCat[0] NCatβ{β.,β£(βΊ-1)β¨β΅} β§
Looking at
i3 β., i3 β., i3 βββββββ¬ββββββ¬ββββββ β1 1 1β1 1 2β1 1 3β βββββββΌββββββΌββββββ€ β1 2 1β1 2 2β1 2 3β βββββββΌββββββΌββββββ€ β1 3 1β1 3 2β1 3 3β βββββββ΄ββββββ΄ββββββ β¦
we can see that the numbers in the boxes are the indices of each box. APL has a primitive to produce the indices of any structure: iota (β³
) :
(i3 β., i3 β., i3 ) β‘ β³ 3 3 3 1
This primitive should take less space to generate and is a lot faster than looping over :
ntupleBβ{okββ΅=+/Β¨allβ,β³ βΊβ΄9 β allβok/all β clean all}
Letβs see how much faster it is. There is a user command in Dyalog that allows us compare timings:
]runtime "6 ntuple 35" "6 ntupleB 35" -compare 6 ntuple 35 β 2.4EΒ―1 | 0% ββββββββββββββββββββββββββββββββββββ 6 ntupleB 35 β 1.4EΒ―1 | -43% βββββββββββββββββββ
But it still suffers from space problems:
7 ntupleB 39 WS FULL ntuple2[0] ntupleBβ{okββ΅=+/Β¨allβ,β³βΊβ΄9 β allβok/all β clean all} β§
But wait, maybe we can do it another way. How about using encode? Here are the 81 pairs again:
1+ 9 9 β€ Β―1+ β³9*2 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 5 5 5 5 5 5 5 6 6 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 9 9 9 9 9 9 9 9 2 3 4 5 6 7 8 9
Again, that would also require too many numbers when N is greater than 6. This brute force method fails for large N, let’s see if we can modify it.
Attempt #2
Maybe we can refine the process. Letβs have a look at pairs again:
> pairs {okββ΅=+/Β¨allβ,β.,β¨β³9 β okβokβ§ β /Β¨all β βͺSortΒ¨ok/all}
We donβt really need Sort
nor the unique (βͺ
) after. We can eliminate a lot of cases by reordering the checks and taking into account that the numbers in the sets must be in increasing order:
pairs2β {allβ(</Β¨all)/allβ, i9 β., i9ββ³9 β (β΅=+/Β¨all)/all} pairs2 9 βββββ¬ββββ¬ββββ¬ββββ β1 8β2 7β3 6β4 5β βββββ΄ββββ΄ββββ΄ββββ
Triples are similar. We cannot use </
on 3 numbers but since the last ones are already ordered we can use </
on the first 2 . We could write
triples2β{ allβ(</Β¨2βΒ¨all) /allβ, i9 β., (</Β¨all)/allβ,i9 β., i9ββ³9 (β΅=+/Β¨all)/all } triples2 19 βββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ β2 8 9β3 7 9β4 6 9β4 7 8β5 6 8β βββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ
Seems to work. Quadruples would work similarly. We should use a function, like Ncat, to generate the combinations, something like
Genβ{ ((</Β¨2βΒ¨all)/ allβ,(β³9) β., β΅}
We can use the user command ]ROWS
(newly introduced in Version 14.0 of Dyalog) to cut the output to the width of the window (paper here):
]rows -style=cut Was -style=long Gen β³9 βββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬βββ’ β1 2β1 3β1 4β1 5β1 6β1 7β1 8β1 9β2 3β2 4β2 5β2 6β2 7β2 8β2 9β3 4β3 β’ βββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄βββ’ Gen Gen β³9 βββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬β’ β1 2 3β1 2 4β1 2 5β1 2 6β1 2 7β1 2 8β1 2 9β1 3 4β1 3 5β1 3 6β1 3 7ββ’ βββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄β’ Gen Gen Gen β³9 βββββββββ¬ββββββββ¬ββββββββ¬ββββββββ¬ββββββββ¬ββββββββ¬ββββββββ¬ββββββββ¬βββ’ β1 2 3 4β1 2 3 5β1 2 3 6β1 2 3 7β1 2 3 8β1 2 3 9β1 2 4 5β1 2 4 6β1 β’ βββββββββ΄ββββββββ΄ββββββββ΄ββββββββ΄ββββββββ΄ββββββββ΄ββββββββ΄ββββββββ΄βββ’
Letβs try it:
ntuple2β{ okββ΅=+/Β¨allβ Genβ£(βΊ-1) β³9 β ok/all} 3 ntuple2 19 βββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ β2 8 9β3 7 9β4 6 9β4 7 8β5 6 8β βββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ
The ultimate test: can it find the only solution to a 9 digits sequence adding up to 45 +/β³9
?
9 ntuple2 45 βββββββββββββββββββ β1 2 3 4 5 6 7 8 9β βββββββββββββββββββ
It works! We now have a solution. Mission accomplished. Letβs see how much space is needed to run it; the user command ]spaceneeded
will provide that information:
]space "9 ntuple2 45" 85824
Not bad. How much CPU does it take?
]runtime "9 ntuple2 45" -repeat=1s * Benchmarking "9 ntuple2 45", repeat=1s Exp CPU (avg): 2.835227273 Elapsed: 2.866477273
3 ms. Thatβs good enough.
Out of curiosity, could we have done better?
Attempt #3
Looking carefully at pairs we see that the first number can be 1 to 9 and that the second number can be whatever remains (as long as it is in the range 1 to 9) but not the same, i.e. for pairs adding up to a specific Sum e.g. 10, we have 1 and (10-1), 2 and (10-2), 3 and (10-3), etc. In APL β
:
(β³9) ,Β¨ 10-β³9 βββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ¬ββββ β1 9β2 8β3 7β4 6β5 5β6 4β7 3β8 2β9 1β βββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ΄ββββ
The sets should be ordered in increasing order so we can limit the numbers 1 to 4 as first number because the largest combination we will have will be n followed by n+1, i.e. 10=n+n+1 or n=4.5, or 4 since we only deal with integers.
pairs3β {i,Β¨ β΅-iβ β³β (β΅-1)Γ·2 } pairs3 9 βββββ¬ββββ¬ββββ¬ββββ β1 8β2 7β3 6β4 5β βββββ΄ββββ΄ββββ΄ββββ pairs3 10 βββββ¬ββββ¬ββββ¬ββββ β1 9β2 8β3 7β4 6β βββββ΄ββββ΄ββββ΄ββββ
Fine. How about triples? Using the same idea we find that the largest set will be n,(n+1),(n+2) so we can use the numbers from 1 to β(Sum-3)Γ·3
followed by all the pairs of Sum
minus that number. For example
Sumβ10 (Sum-3) Γ· 3 β we start with the numbers 1 and 2 2.333333333 1,Β¨ pairs3 Sum-1 βββββββ¬ββββββ¬ββββββ¬ββββββ β1 1 8β1 2 7β1 3 6β1 4 5β βββββββ΄ββββββ΄ββββββ΄ββββββ 2,Β¨ pairs3 Sum-2 βββββββ¬ββββββ¬ββββββ β2 1 7β2 2 6β2 3 5β βββββββ΄ββββββ΄ββββββ
Not quite. We should ignore any pair starting with a number smaller or equal to our first number.
Letβs modify pairs3
to accept a (optional) left argument specifying the starting numbers to skip:
pairs3β {βΊβ0 β i,Β¨ β΅-iβ βΊβ β³β (β΅-1)Γ·2 } pairs3 9 βββββ¬ββββ¬ββββ¬ββββ β1 8β2 7β3 6β4 5β βββββ΄ββββ΄ββββ΄ββββ 1 pairs3 9 βββββ¬ββββ¬ββββ β2 7β3 6β4 5β βββββ΄ββββ΄ββββ
1,Β¨ 1 pairs3 Sum-2 βββββββ¬ββββββ β1 2 6β1 3 5β βββββββ΄ββββββ 2,Β¨ 2 pairs3 Sum-2 βββββββ β2 3 5β βββββββ
Triples?
triples3β{iβ β³β (β΅-3)Γ·3 β i,Β¨ i pairs3Β¨ β΅-i } triples3 9 βββββββββββββ¬ββββββββ ββββ¬ββββ¬ββββββββ¬βββββ ββ1β2 6β3 5βββ2β3 4ββ ββββ΄ββββ΄ββββββββ΄βββββ βββββββββββββ΄ββββββββ
Not quite, we need to use each (Β¨
) twice:
triples3β{iβ β³β (β΅-3)Γ·3 β i,¨¨ i pairs3Β¨ β΅-i } triples3 9 βββββββββββββββ¬ββββββββ ββββββββ¬βββββββββββββββ ββ1 2 6β1 3 5βββ2 3 4ββ ββββββββ΄βββββββββββββββ βββββββββββββββ΄ββββββββ
Thatβs better but this enclosing business is getting out of hand. Letβs work with matrices:
pairs3Bβ{βΊβ0 β i,βͺβ΅-iββΊββ³β(β΅-1)Γ·2} pairs3B 9 1 8 2 7 3 6 4 5 2 pairs3B 9 3 6 4 5 triples3Bβ {iβ β³β (β΅-3)Γ·3 β ββͺ/i,Β¨ i pairs3BΒ¨ β΅-i } triples3B 9 1 2 6 1 3 5 2 3 4
Seems to work. But there is a pattern here. It looks like a recursive definition.
- If we want a single digit set then the set is the sum if it is below 10.
- If we want a N digit set then it is all the digits from 1 to
β(Sum-+/β³N-1)Γ·N
followed by the N-1 digit set ofSum
minus that number.
Weβll need to adjust the starting number and remove any number smaller or equal to the first digit. We need to supply that number as argument, something like:
ntuple3β { β Generate all monotonic combinations of βΊ numbers adding to β΅ (nn sn)β2ββΊ β # of numbers needed, numbers to skip nn=1 : (β΅β€9)βΏ βͺβ΅ β solution for 1 number is β΅ if β€9 β More than 1 #, drop any number β€ sn n1βsnββ³8β0ββ(β΅- +/ β³ nn-1)Γ·nn β all possible starting # 0ββ΄n1 : 0 nnβ΄0 β no solution? β All are starting # followed by the new combination ββͺ/ n1 ,Β¨ ((nn-1),Β¨n1) βΒ¨ β΅-n1 }
This solution returns a matrix instead of a list of vectors. Letβs try it:
4 ntuple3 12 1 2 3 6 1 2 4 5 9 ntuple3 45 1 2 3 4 5 6 7 8 9
How does it compare with the previous solution?
]runtime "9 ntuple2 45 " "9 ntuple3 45" -compare 9 ntuple2 45 β 2.9EΒ―3 | 0% βββββββββββββββββββββββββββββββββββ * 9 ntuple3 45 β 5.3EΒ―5 | -99% β ]space "9 ntuple3 45" 3620
Quite a difference. With just a little more effort we improved our original solution a lot. And there are even faster solutions.
Conclusion
APL provides us with an environment where we can experiment βhands onβ to study situations, verify results and make better decisions. With it we can even come up with prototypes that we can use to make further forays into our problem. There is a lot of thinking that could have been done mentally but being able to use the computer really is a bonus.
If you want, there is a video accompanying this article you can have a look at. Go to YouTube and look for βBrute force method to finding all the sets in a row of Kakuroβ. You can also try this link: http://youtu.be/bJssWsdXjmY
Have fun!