Solving twenty-four puzzles
blog.plover.com
blog.plover.com
The first line of each instance gave the number of numbers you had to work with, the second line gave those numbers separated by space, and the third line gave the target number.
Conciseness was measured by the number of tokens in the source code. Here is a copy of the problem specification: http://computer-programming-forum.com/47-c-language/3e0a04a3...
Here was the token counter used for that round: https://pastebin.com/bdaEedD7
Here is the first runner up and the winner: https://pastebin.com/JcamKieX
The runner up is 336 tokens. The winner is 335 tokens.
The best I was able to do was 460 tokens. (Well, the best without exploiting limitations of the token counter, but I was on dial up and so did not have the time to upload a 4 GB source file...)
First, let us again start with a suitable operator definition, so that we can use × as an infix operator in expressions:
:- op(400, yfx, ×).
Next, let us declaratively describe what we mean by a "clock", using integer constraints: clock(Vs) :-
Vs = [H0,H1,M1,M2],
Vs ins 0..9,
H0*10 + H1 #< 24,
M1*10 + M2 #< 60,
label(Vs).
For example: ?- clock(Vs).
Vs = [0, 0, 0, 0] ;
Vs = [0, 0, 0, 1] ;
Vs = [0, 0, 0, 2] ;
Vs = [0, 0, 0, 3] .
We can collect all solutions as a quick verification: ?- findall(., clock(Vs), Ls), length(Ls, L).
Ls = ['.', '.', '.', '.', '.', '.', '.', '.', '.'|...],
L = 1440.
This confirms that there are 1440 puzzles of this kind, one for each possible clock time.The puzzle now asks for expressions that evaluate to 0. We can relate any given clock display to an expression like this:
clock_solution(Vs, Expr) :-
clock(Vs),
numbers_tree(Vs, T),
tree_op_expr_value(T, _, Expr, 0).
This reuses predicates I posted earlier, which I only need to slightly modify to prohibit multiplication by zero, and which I repeat here for completeness: numbers_tree([N], leaf(N)).
numbers_tree(Vs0, binary(Left, Right)) :-
permutation(Vs0, Vs),
append([L|Ls], [R|Rs], Vs),
numbers_tree([L|Ls], Left),
numbers_tree([R|Rs], Right).
tree_op_expr_value(leaf(N), _, N, N).
tree_op_expr_value(binary(Left0, Right0), Op, Expr, Value) :-
tree_op_expr_value(Left0, _, Left, VL),
tree_op_expr_value(Right0, _, Right, VR),
op_values_value(Op, VL, VR, Value),
Expr =.. [Op,Left,Right].
op_values_value(+, A, B, V) :- V is A + B.
op_values_value(-, A, B, V) :- V is A - B.
op_values_value(×, A, B, V) :- A =\= 0, B =\= 0, V is A * B.
op_values_value(/, A, B, V) :- B =\= 0, V is A rdiv B.
We can use this to enumerate all times that have a solution: ?- clock(C),
once(clock_solution(C, S)).
C = [0, 0, 0, 0],
S = 0+(0+(0+0)) ;
C = [0, 0, 0, 1],
S = 0/(0+(0+1)) ;
C = [0, 0, 0, 2],
S = 0/(0+(0+2)) ;
C = [0, 0, 0, 3],
S = 0/(0+(0+3)) .
And here is the list of all times that don't have a solution: ?- clock(C),
\+ clock_solution(C, S).
C = [1, 6, 4, 8] ;
C = [1, 7, 4, 9] ;
C = [1, 7, 5, 9] ;
C = [1, 8, 4, 6] ;
C = [1, 9, 4, 7] ;
C = [1, 9, 5, 7] ;
false.Let us start by defining suitable operators so that we can conveniently use the domain-specific language to reason about such tasks, using infix notation:
:- op(400, yfx, ×).
:- op(400, yfx, ÷).
Next, I describe the relation between a list of numbers and possible shapes of binary trees we can build from these numbers. We distinguish between leaves and inner nodes of the tree, and consequently use two functors that let us symbolically distinguish the different cases: numbers_tree([N], leaf(N)).
numbers_tree(Vs0, binary(Left, Right)) :-
permutation(Vs0, Vs),
append([L|Ls], [R|Rs], Vs),
numbers_tree([L|Ls], Left),
numbers_tree([R|Rs], Right).
For example: ?- numbers_tree([1,2], T).
T = binary(leaf(1), leaf(2)) ;
T = binary(leaf(2), leaf(1)) ;
false.
This does not yet say anything about the arithmetic operators that we can use. Note that this definition may yield redundant solutions, as in: ?- numbers_tree([1,1], T).
T = binary(leaf(1), leaf(1)) ;
T = binary(leaf(1), leaf(1)) ;
false.
That's completely OK though, and we can think about making it more efficient if necessary.Now we are almost done. It only remains to describe how different arithmetic operations affect the result of the evaluation. This can be expressed with the following relations:
tree_op_expr_value(leaf(N), _, N, N).
tree_op_expr_value(binary(Left0, Right0), Op, Expr, Value) :-
tree_op_expr_value(Left0, _, Left, VL),
tree_op_expr_value(Right0, _, Right, VR),
op_values_value(Op, VL, VR, Value),
Expr =.. [Op,Left,Right].
op_values_value(+, A, B, V) :- V is A + B.
op_values_value(-, A, B, V) :- V is A - B.
op_values_value(×, A, B, V) :- V is A * B.
op_values_value(÷, A, B, V) :- B =\= 0, V is A rdiv B.
Time to try it out, using for example the following query: ?- time((numbers_tree([2,5,6,6], T),
tree_op_expr_value(T, _, Expr, 17))).
This yields: % 6,060 inferences, 0.004 CPU in 0.004 seconds (92% CPU, 1551459 Lips)
T = binary(binary(leaf(2), binary(leaf(5), leaf(6))), leaf(6)),
Expr = (2+5÷6)×6 .
It seems to work nicely. On backtracking, all solutions are generated. Note that this solution uses built-in rational numbers that are available in several Prolog systems.in this problem we have 4 operators (+, -, /, *) to consider instead of just 1 (+), so it is more complex than subset sum in this regard. also as a result of having different operators, the ordering of the numbers matter in this case, which doesn't matter in subset sum. however the fact that we have to use each number exactly once significantly reduces the search space