Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

correct implementation based on array

now also taking combinations of subexpressions into account
also probably much faster than the digraph based approach.
  • Loading branch information...
commit 6cc784bef10ef462f0971e7cebd7cb8db83dd071 1 parent 6c248f2
@martinrehfeld authored
Showing with 146 additions and 89 deletions.
  1. +135 −75 apps/countdown/src/cd_solver.erl
  2. +11 −14 apps/countdown/test/cd_solver_tests.erl
View
210 apps/countdown/src/cd_solver.erl
@@ -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
@@ -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.
View
25 apps/countdown/test/cd_solver_tests.erl
@@ -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)),
@@ -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).
Please sign in to comment.
Something went wrong with that request. Please try again.