How to design an algorithm to calculate countdown style maths number puzzle

后端 未结 10 1099
清酒与你
清酒与你 2020-11-30 00:35

I have always wanted to do this but every time I start thinking about the problem it blows my mind because of its exponential nature.

The problem solver I want to be

相关标签:
10条回答
  • 2020-11-30 01:11

    A working solution in c++11 below.

    The basic idea is to use a stack-based evaluation (see RPN) and convert the viable solutions to infix notation for display purposes only.

    If we have N input digits, we'll use (N-1) operators, as each operator is binary.

    First we create valid permutations of operands and operators (the selector_ array). A valid permutation is one that can be evaluated without stack underflow and which ends with exactly one value (the result) on the stack. Thus 1 1 + is valid, but 1 + 1 is not.

    We test each such operand-operator permutation with every permutation of operands (the values_ array) and every combination of operators (the ops_ array). Matching results are pretty-printed.

    Arguments are taken from command line as [-s] <target> <digit>[ <digit>...]. The -s switch prevents exhaustive search, only the first matching result is printed.

    (use ./mathpuzzle 348 1 3 7 6 8 3 to get the answer for the original question)

    This solution doesn't allow concatenating the input digits to form numbers. That could be added as an additional outer loop.

    The working code can be downloaded from here. (Note: I updated that code with support for concatenating input digits to form a solution)

    See code comments for additional explanation.

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <stack>
    #include <iterator>
    #include <string>
    
    namespace {
    
    enum class Op {
        Add,
        Sub,
        Mul,
        Div,
    };
    
    const std::size_t NumOps = static_cast<std::size_t>(Op::Div) + 1;
    const Op FirstOp = Op::Add;
    
    using Number = int;
    
    class Evaluator {
        std::vector<Number> values_; // stores our digits/number we can use
        std::vector<Op> ops_; // stores the operators
        std::vector<char> selector_; // used to select digit (0) or operator (1) when evaluating. should be std::vector<bool>, but that's broken
    
        template <typename T>
        using Stack = std::stack<T, std::vector<T>>;
    
        // checks if a given number/operator order can be evaluated or not
        bool isSelectorValid() const {
            int numValues = 0;
            for (auto s : selector_) {
                if (s) {
                    if (--numValues <= 0) {
                        return false;
                    }
                }
                else {
                    ++numValues;
                }
            }
            return (numValues == 1);
        }
    
        // evaluates the current values_ and ops_ based on selector_
        Number eval(Stack<Number> &stack) const {
            auto vi = values_.cbegin();
            auto oi = ops_.cbegin();
            for (auto s : selector_) {
                if (!s) {
                    stack.push(*(vi++));
                    continue;
                }
                Number top = stack.top();
                stack.pop();
                switch (*(oi++)) {
                    case Op::Add:
                        stack.top() += top;
                        break;
                    case Op::Sub:
                        stack.top() -= top;
                        break;
                    case Op::Mul:
                        stack.top() *= top;
                        break;
                    case Op::Div:
                        if (top == 0) {
                            return std::numeric_limits<Number>::max();
                        }
                        Number res = stack.top() / top;
                        if (res * top != stack.top()) {
                            return std::numeric_limits<Number>::max();
                        }
                        stack.top() = res;
                        break;
                }
            }
            Number res = stack.top();
            stack.pop();
            return res;
        }
    
        bool nextValuesPermutation() {
            return std::next_permutation(values_.begin(), values_.end());
        }
    
        bool nextOps() {
            for (auto i = ops_.rbegin(), end = ops_.rend(); i != end; ++i) {
                std::size_t next = static_cast<std::size_t>(*i) + 1;
                if (next < NumOps) {
                    *i = static_cast<Op>(next);
                    return true;
                }
                *i = FirstOp;
            }
            return false;
        }
    
        bool nextSelectorPermutation() {
            // the start permutation is always valid
            do {
                if (!std::next_permutation(selector_.begin(), selector_.end())) {
                    return false;
                }
            } while (!isSelectorValid());
            return true;
        }
    
        static std::string buildExpr(const std::string& left, char op, const std::string &right) {
            return std::string("(") + left + ' ' + op + ' ' + right + ')';
        }
    
        std::string toString() const {
            Stack<std::string> stack;
            auto vi = values_.cbegin();
            auto oi = ops_.cbegin();
            for (auto s : selector_) {
                if (!s) {
                    stack.push(std::to_string(*(vi++)));
                    continue;
                }
                std::string top = stack.top();
                stack.pop();
                switch (*(oi++)) {
                    case Op::Add:
                        stack.top() = buildExpr(stack.top(), '+', top);
                        break;
                    case Op::Sub:
                        stack.top() = buildExpr(stack.top(), '-', top);
                        break;
                    case Op::Mul:
                        stack.top() = buildExpr(stack.top(), '*', top);
                        break;
                    case Op::Div:
                        stack.top() = buildExpr(stack.top(), '/', top);
                        break;
                }
            }
            return stack.top();
        }
    
    public:
        Evaluator(const std::vector<Number>& values) :
                values_(values),
                ops_(values.size() - 1, FirstOp),
                selector_(2 * values.size() - 1, 0) {
            std::fill(selector_.begin() + values_.size(), selector_.end(), 1);
            std::sort(values_.begin(), values_.end());
        }
    
        // check for solutions
        // 1) we create valid permutations of our selector_ array (eg: "1 1 + 1 +",
        //    "1 1 1 + +", but skip "1 + 1 1 +" as that cannot be evaluated
        // 2) for each evaluation order, we permutate our values
        // 3) for each value permutation we check with each combination of
        //    operators
        // 
        // In the first version I used a local stack in eval() (see toString()) but
        // it turned out to be a performance bottleneck, so now I use a cached
        // stack. Reusing the stack gives an order of magnitude speed-up (from
        // 4.3sec to 0.7sec) due to avoiding repeated allocations.  Using
        // std::vector as a backing store also gives a slight performance boost
        // over the default std::deque.
        std::size_t check(Number target, bool singleResult = false) {
            Stack<Number> stack;
    
            std::size_t res = 0;
            do {
                do {
                    do {
                        Number value = eval(stack);
                        if (value == target) {
                            ++res;
                            std::cout << target << " = " << toString() << "\n";
                            if (singleResult) {
                                return res;
                            }
                        }
                    } while (nextOps());
                } while (nextValuesPermutation());
            } while (nextSelectorPermutation());
            return res;
        }
    };
    
    } // namespace
    
    int main(int argc, const char **argv) {
        int i = 1;
        bool singleResult = false;
        if (argc > 1 && std::string("-s") == argv[1]) {
            singleResult = true;
            ++i;
        }
        if (argc < i + 2) {
            std::cerr << argv[0] << " [-s] <target> <digit>[ <digit>]...\n";
            std::exit(1);
        }
        Number target = std::stoi(argv[i]);
        std::vector<Number> values;
        while (++i <  argc) {
            values.push_back(std::stoi(argv[i]));
        }
        Evaluator evaluator{values};
        std::size_t res = evaluator.check(target, singleResult);
        if (!singleResult) {
            std::cout << "Number of solutions: " << res << "\n";
        }
        return 0;
    }
    
    0 讨论(0)
  • 2020-11-30 01:12

    Very quick and dirty solution in Java:

    public class JavaApplication1
    {
    
        public static void main(String[] args)
        {
            List<Integer> list = Arrays.asList(1, 3, 7, 6, 8, 3);
            for (Integer integer : list) {
                List<Integer> runList = new ArrayList<>(list);
                runList.remove(integer);
                Result result = getOperations(runList, integer, 348);
                if (result.success) {
                    System.out.println(integer + result.output);
                    return;
                }
            }
        }
    
        public static class Result
        {
    
            public String output;
            public boolean success;
        }
    
        public static Result getOperations(List<Integer> numbers, int midNumber, int target)
        {
            Result midResult = new Result();
            if (midNumber == target) {
                midResult.success = true;
                midResult.output = "";
                return midResult;
            }
            for (Integer number : numbers) {
                List<Integer> newList = new ArrayList<Integer>(numbers);
                newList.remove(number);
                if (newList.isEmpty()) {
                    if (midNumber - number == target) {
                        midResult.success = true;
                        midResult.output = "-" + number;
                        return midResult;
                    }
                    if (midNumber + number == target) {
                        midResult.success = true;
                        midResult.output = "+" + number;
                        return midResult;
                    }
                    if (midNumber * number == target) {
                        midResult.success = true;
                        midResult.output = "*" + number;
                        return midResult;
                    }
                    if (midNumber / number == target) {
                        midResult.success = true;
                        midResult.output = "/" + number;
                        return midResult;
                    }
                    midResult.success = false;
                    midResult.output = "f" + number;
                    return midResult;
                } else {
                    midResult = getOperations(newList, midNumber - number, target);
                    if (midResult.success) {
                        midResult.output = "-" + number + midResult.output;
                        return midResult;
                    }
                    midResult = getOperations(newList, midNumber + number, target);
                    if (midResult.success) {
                        midResult.output = "+" + number + midResult.output;
                        return midResult;
                    }
                    midResult = getOperations(newList, midNumber * number, target);
                    if (midResult.success) {
                        midResult.output = "*" + number + midResult.output;
                        return midResult;
                    }
                    midResult = getOperations(newList, midNumber / number, target);
                    if (midResult.success) {
                        midResult.output = "/" + number + midResult.output;
                        return midResult
                    }
                }
    
            }
            return midResult;
        }
    }
    

    UPDATE

    It's basically just simple brute force algorithm with exponential complexity. However you can gain some improvemens by leveraging some heuristic function which will help you to order sequence of numbers or(and) operations you will process in each level of getOperatiosn() function recursion.

    Example of such heuristic function is for example difference between mid result and total target result.

    This way however only best-case and average-case complexities get improved. Worst case complexity remains untouched.

    Worst case complexity can be improved by some kind of branch cutting. I'm not sure if it's possible in this case.

    0 讨论(0)
  • 2020-11-30 01:15

    I wrote a terminal application to do this: https://github.com/pg328/CountdownNumbersGame/tree/main

    Inside, I've included an illustration of the calculation of the size of the solution space (it's n*((n-1)!^2)*(2^n-1), so: n=6 -> 2,764,800. I know, gross), and more importantly why that is. My implementation is there if you care to check it out, but in case you don't I'll explain here.

    Essentially, at worst it is brute force because as far as I know it's impossible to determine whether any specific branch will result in a valid answer without explicitly checking. Having said that, the average case is some fraction of that; it's {that number} divided by the number of valid solutions (I tend to see around 1000 on my program, where 10 or so are unique and the rest are permutations fo those 10). If I handwaved a number, I'd say roughly 2,765 branches to check which takes like no time. (Yes, even in Python.)

    TL;DR: Even though the solution space is huge and it takes a couple million operations to find all solutions, only one answer is needed. Best route is brute force til you find one and spit it out.

    0 讨论(0)
  • 2020-11-30 01:19

    I think, you need to strictly define the problem first. What you are allowed to do and what you are not. You can start by making it simple and only allowing multiplication, division, substraction and addition.

    Now you know your problem space- set of inputs, set of available operations and desired input. If you have only 4 operations and x inputs, the number of combinations is less than:

    The number of order in which you can carry out operations (x!) times the possible choices of operations on every step: 4^x. As you can see for 6 numbers it gives reasonable 2949120 operations. This means that this may be your limit for brute force algorithm.

    Once you have brute force and you know it works, you can start improving your algorithm with some sort of A* algorithm which would require you to define heuristic functions.

    In my opinion the best way to think about it is as the search problem. The main difficulty will be finding good heuristics, or ways to reduce your problem space (if you have numbers that do not add up to the answer, you will need at least one multiplication etc.). Start small, build on that and ask follow up questions once you have some code.

    0 讨论(0)
  • 2020-11-30 01:23

    I wrote a slightly simpler version:

    1. for every combination of 2 (distinct) elements from the list and combine them using +,-,*,/ (note that since a>b then only a-b is needed and only a/b if a%b=0)
    2. if combination is target then record solution
    3. recursively call on the reduced lists
    0 讨论(0)
  • 2020-11-30 01:27

    By far the easiest approach is to intelligently brute force it. There is only a finite amount of expressions you can build out of 6 numbers and 4 operators, simply go through all of them.

    How many? Since you don't have to use all numbers and may use the same operator multiple times, This problem is equivalent to "how many labeled strictly binary trees (aka full binary trees) can you make with at most 6 leaves, and four possible labels for each non-leaf node?".

    The amount of full binary trees with n leaves is equal to catalan(n-1). You can see this as follows:

    Every full binary tree with n leaves has n-1 internal nodes and corresponds to a non-full binary tree with n-1 nodes in a unique way (just delete all the leaves from the full one to get it). There happen to be catalan(n) possible binary trees with n nodes, so we can say that a strictly binary tree with n leaves has catalan(n-1) possible different structures.

    There are 4 possible operators for each non-leaf node: 4^(n-1) possibilities The leaves can be numbered in n! * (6 choose (n-1)) different ways. (Divide this by k! for each number that occurs k times, or just make sure all numbers are different)

    So for 6 different numbers and 4 possible operators you get Sum(n=1...6) [ Catalan(n-1) * 6!/(6-n)! * 4^(n-1) ] possible expressions for a total of 33,665,406. Not a lot.

    How do you enumerate these trees?

    Given a collection of all trees with n-1 or less nodes, you can create all trees with n nodes by systematically pairing all of the n-1 trees with the empty tree, all n-2 trees with the 1 node tree, all n-3 trees with all 2 node tree etc. and using them as the left and right sub trees of a newly formed tree.

    So starting with an empty set you first generate the tree that has just a root node, then from a new root you can use that either as a left or right sub tree which yields the two trees that look like this: / and . And so on.

    You can turn them into a set of expressions on the fly (just loop over the operators and numbers) and evaluate them as you go until one yields the target number.

    0 讨论(0)
提交回复
热议问题