Skip to content

Commit

Permalink
correct implementation based on array
Browse files Browse the repository at this point in the history
now also taking combinations of subexpressions into account
also probably much faster than the digraph based approach.
  • Loading branch information
Martin Rehfeld committed May 13, 2012
1 parent 6c248f2 commit 6cc784b
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 89 deletions.
210 changes: 135 additions & 75 deletions apps/countdown/src/cd_solver.erl
Expand Up @@ -4,91 +4,150 @@
-export([solutions/2]).

%% only exported for eunit
-export([add/2, subtract/2, multiply/2, divide/2, available_indexes/2]).
-export([add/2, subtract/2, multiply/2, divide/2]).

-define(OPERATORS, [{'+', fun add/2},
{'-', fun subtract/2},
{'*', fun multiply/2},
{'/', fun divide/2}]).

-record(vertex, {result, used}).
-record(edge, {operator, operand}).
-record(solution, {result, expression}).


solutions(Target, Numbers) ->
{G, Root} = build_graph(Numbers),
best_solutions(G, Root, Target).

build_graph(Numbers) ->
G = digraph:new([acyclic]),
Used = 0,
Root = digraph:add_vertex(G, #vertex{result = 0, used = Used}),
{add_next_level(G, Root, available_indexes(Used, length(Numbers)), Numbers),
Root}.

add_next_level(G, VStart, AvailableIndexes, Numbers) ->
lists:foldl(fun(Index, G1) ->
add_operations(G1, VStart, Index, Numbers, ?OPERATORS)
end, G, AvailableIndexes).

add_operations(G, VStart, Index, Numbers, Operators) ->
lists:foldl(fun(Operator, G1) ->
add_compute_step(G1, VStart, Index, Numbers, Operator)
end, G, Operators).

add_compute_step(G, #vertex{result=N1,used=Bitmap0}=VStart, Index, Numbers, Operator) ->
N2 = lists:nth(Index, Numbers),
{OperatorName, OperatorFun} = Operator,

case OperatorFun(N1, N2) of
no_op -> G;
not_allowed -> G;

Result ->
Bitmap1 = Bitmap0 bor (1 bsl Index),
Vertex= #vertex{result = Result, used = Bitmap1},
VNew = digraph:add_vertex(G, Vertex),
digraph:add_edge(G, VStart, VNew, #edge{operator = OperatorName, operand = N2}),
%% error_logger:info_msg("New Op ~p~n",
%% [digraph:edge(G, lists:nth(1, digraph:out_edges(G, VStart)))]),
add_next_level(G, VNew, available_indexes(Bitmap1, length(Numbers)),
Numbers)
A = initialize_solutions(Target, Numbers),
A1 = combine(A, Target, ?OPERATORS, Numbers),
BestSolutions = best_solutions(A1),
error_logger:info_msg("Best solution(s):~n~s~n", [string:join(format_solutions(BestSolutions), "\n")]),
BestSolutions.


%% @doc: Seed an array with the base expressions.
%% The solutions array uses a bitmap of used numbers for a given expression as
%% index and has elements of the form:
%% [{DeltaToTargetValue, ExpressionString}, ...]
initialize_solutions(Target, Numbers) ->
MapSize = 1 bsl length(Numbers),
A = array:new(MapSize, {default, []}),

F = fun(Index, Array) ->
UsedBitmap = bitmap_for_index(Index),
Number = lists:nth(Index, Numbers),
Delta = abs(Target - Number),
Expression = integer_to_list(Number),
S = #solution{result=Number, expression=Expression},
Solutions = orddict:from_list([{Delta, S}]),
array:set(UsedBitmap, Solutions, Array)
end,
lists:foldl(F, A, lists:seq(1, length(Numbers))).


best_solutions(A) ->
BestPerUsedNumbers = best_per_used_numbers(A),
best_of_best(BestPerUsedNumbers).


best_per_used_numbers(A) ->
F = fun(_UsedBitmap, Solutions, Acc) ->
OrderedSolutions = orddict:to_list(Solutions),
BestSolution = lists:nth(1, OrderedSolutions),
[BestSolution | Acc]
end,
array:sparse_foldl(F, [], A).


best_of_best(Solutions) ->
OrderedSolutions = lists:keysort(1, Solutions),
{LowestDelta, _} = lists:nth(1, OrderedSolutions),
lists:takewhile(fun ({D, _}) -> D =:= LowestDelta end, OrderedSolutions).


format_solutions(Entries) ->
F = fun(Entry) ->
{Delta, #solution{result=Result, expression=Expression}} = Entry,
io_lib:format("~s = ~p # delta: ~p", [Expression, Result, Delta])
end,
lists:map(F, Entries).

combine(A, Target, Operators, Numbers) ->
combine(A, Target, Operators, Numbers, array_length(A)).

combine(A, Target, Operators, Numbers, NumberOfSolutions) ->
%% error_logger:info_msg("combine(~p, ~p, ~p, ~p, ~p)~n", [A, Target, Operators, Numbers, NumberOfSolutions]),
FJ =
fun(J, Solutions2, {I, Solutions1, Array}) ->
case I band J of
0 -> % Bitmaps do not overlap
NewArray = combine_solutions(I, J, Solutions1, Solutions2, Array, Target),
{I, Solutions1, NewArray};

_ -> % Bitmaps *do* overlap
{I, Solutions1, Array}
end
end,

FI =
fun(I, Solutions, Array) ->
{I, Solutions, NewArray} = array:sparse_foldl(FJ, {I, Solutions, Array}, Array),
NewArray
end,

A1 = array:sparse_foldl(FI, A, A),

case array_length(A1) of
NumberOfSolutions -> % no new solutions found
A1;
IncreasedNumberOfSolutions ->
combine(A1, Target, Operators, Numbers, IncreasedNumberOfSolutions)
end.

available_indexes(Bitmap, Size) ->
lists:filter(fun(Index) ->
Mask = bnot (1 bsl Index),
Bitmap bor Mask =:= Mask
end, lists:seq(1, Size)).

best_solutions(G, Root, Target) ->
Vertices = digraph:vertices(G),
Deltas =
lists:map(fun(V) ->
{abs(V#vertex.result - Target), V}
end, Vertices),
OrderedDeltas =
lists:sort(fun(A, B) ->
{Delta1, _Vertex1} = A,
{Delta2, _Vertex2} = B,
Delta1 =< Delta2
end, Deltas),
{_BestDelta, BestVertex} = hd(OrderedDeltas),
Solutions = [{BestVertex#vertex.result, calculations(G, Root, BestVertex)}],
error_logger:info_msg("Found best solution (out of ~p unique results):~n~p~n",
[length(Vertices), Solutions]),
Solutions.

calculations(G, Root, BestVertex) ->
calculations(G, Root, BestVertex, []).

calculations(G, Root, EndVertex, Operations) ->
InEdges = digraph:in_edges(G, EndVertex),
E = hd(InEdges),
{_, SourceVertex, _, Operation} = digraph:edge(G, E),
case SourceVertex of
Root -> [Operation|Operations];
_ -> calculations(G, Root, SourceVertex, [Operation|Operations])
end.

combine_solutions(I, J, Solutions1, Solutions2, A, Target) ->
%% error_logger:info_msg("combine_solutions(~p, ~p, ~p, ~p, _, _)~n", [I, J, Solutions1, Solutions2]),
FT =
fun(_T, Solution2, {Solution1, Array}) ->
NewArray = combine_operators(I, J, Solution1, Solution2, Array, Target),
{Solution1, NewArray}
end,

FS =
fun(_S, Solution1, Array) ->
{Solution1, NewArray} = orddict:fold(FT, {Solution1, Array}, Solutions2),
NewArray
end,

orddict:fold(FS, A, Solutions1).


combine_operators(I, J, S1, S2, A, Target) ->
F = fun({OpSymbol, OpFn}, Array) ->
case OpFn(S1#solution.result, S2#solution.result) of
not_allowed -> Array;
no_op -> Array;
Result ->
Operator = atom_to_list(OpSymbol),
Expression =
"(" ++ S1#solution.expression ++
Operator ++
S2#solution.expression ++ ")",
Solution = #solution{result=Result, expression=Expression},
Delta = abs(Target - Result),
Index = I bor J,
Entries = array:get(Index, Array),
NewEntries = orddict:store(Delta, Solution, Entries),
array:set(I bor J, NewEntries, Array)
end
end,
lists:foldl(F, A, ?OPERATORS).


%% @doc: count all defined elements in an array
array_length(A) ->
F = fun(_Index, _Value, Count) -> Count + 1 end,
array:sparse_foldl(F, 0, A).


bitmap_for_index(Index) -> 1 bsl (Index - 1).

%%
%% Operators
Expand All @@ -104,6 +163,7 @@ subtract(N1, N2) when N1 >= N2 -> N1 - N2.
multiply(N1, N2) when N2 =/= 1 -> N1 * N2;
multiply(_N1, _N2) -> no_op.

divide(_N1, N2) when N2 =:= 0 -> not_allowed;
divide(N1, N2) when N1 rem N2 =/= 0 -> not_allowed;
divide(_N1, N2) when N2 =:= 1 -> no_op;
divide(N1, N2) when N1 rem N2 =:= 0 -> N1 div N2.
25 changes: 11 additions & 14 deletions apps/countdown/test/cd_solver_tests.erl
Expand Up @@ -18,11 +18,11 @@
%% TESTS
%%

available_indexes_test() ->
?assertEqual([], cd_solver:available_indexes(2,1)),
?assertEqual([1], cd_solver:available_indexes(0,1)),
?assertEqual([2], cd_solver:available_indexes(2,2)),
?assertEqual([3], cd_solver:available_indexes(6,3)).
%% available_indexes_test() ->
%% ?assertEqual([], cd_solver:available_indexes(2,1)),
%% ?assertEqual([1], cd_solver:available_indexes(0,1)),
%% ?assertEqual([2], cd_solver:available_indexes(2,2)),
%% ?assertEqual([3], cd_solver:available_indexes(6,3)).

add_test() ->
?assertEqual(no_op, cd_solver:add(1, 0)),
Expand All @@ -42,12 +42,9 @@ divide_test() ->
?assertEqual(not_allowed, cd_solver:divide(1, 2)),
?assertEqual(1, cd_solver:divide(2, 2)).

solver_test_() ->
%% Solutions: [{actual_result, [n1, op1, n2, op2, ...]}, ...]
{timeout, 600, fun() ->
Solutions1 = cd_solver:solutions(178, [100, 75, 3]),
%% Solutions2 = cd_solver:solutions(203, [50, 100, 4, 2, 2, 4]),
[?_assert(proplists:lookup(178, Solutions1) =/= none)
%% ,?_assert(proplists:lookup(203, Solutions2) =/= none)
]
end}.
solver_test() ->
%% Solutions: [{delta, #solution}, ...]
Solutions1 = cd_solver:solutions(178, [100, 75, 3]),
Solutions2 = cd_solver:solutions(203, [50, 100, 4, 2, 2, 4]),
?assertMatch([{0, _}], Solutions1),
?assertMatch([{0, _} | _], Solutions2).

0 comments on commit 6cc784b

Please sign in to comment.