如何设计算法来计算倒计时风格的数学数字拼图

时间:2013-03-08 11:44:41

标签: java c++ algorithm combinatorics

我一直都想这样做,但每当我开始思考这个问题时,它就会因为它的指数性而引起我的注意。

我希望能够理解和编码的问题解决方案是倒计时数学问题:

给定数字X1到X5的集合计算如何使用数学运算来组合Y. 您可以应用乘法,除法,加法和减法。

那么1,3,7,6,8,3如何制作348

答案:(((8 * 7) + 3) -1) *6 = 348

如何编写可以解决此问题的算法?在尝试解决这样的问题时你从哪里开始?在设计这样的算法时你需要考虑哪些重要的考虑因素?

11 个答案:

答案 0 :(得分:6)

当然它是指数但它很小所以一个好的(足够的)天真的实现将是一个良好的开端。我建议你删除通常的中缀符号括号,并使用postfix,它更容易编程。您总是可以将输出美化为一个单独的阶段。

首先列出并评估所有(有效)数字和运算符序列。例如(在后缀中):

1 3 7 6 8 3 + + + + + -> 28
1 3 7 6 8 3 + + + + - -> 26

我的Java很可笑,我不会来这里被嘲笑所以我会把这个编码留给你。

对所有聪明的人来说:是的,我知道即使像这样的小问题也有更聪明的方法可能会更快,我只是将OP指向一个初步的工作解决方案。其他人可以用更智能的解决方案来写答案。

所以,回答你的问题:

  • 我从一个算法开始,我认为这会让我快速找到一个有效的解决方案。在这种情况下,显而易见的(对我来说)选择是详尽的枚举和测试所有可能的计算。
  • 如果显而易见的算法由于性能原因而看起来没有吸引力,我会开始更深入地思考它,回想一下我所知道的其他可能提供更好性能的算法。我可能会先开始编写其中一个。
  • 如果我坚持使用详尽的算法并发现运行时间实际上太长了,那么我可能会回到上一步并重新编码。但是我必须付出相同的成本/收益评估 - 只要我的代码能超越Rachel Riley,我就会感到满意。
  • 重要的考虑因素包括我的时间 vs 计算机时间,我的成本更高。

答案 1 :(得分:6)

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;
    }
}

<强>更新

它基本上只是具有指数复杂性的简单蛮力算法。 但是,您可以通过利用一些启发式函数来获得一些改进,这将有助于您订购数字序列或(和)您将在getOperatiosn()函数递归的每个级别处理的操作。

此类启发式函数的示例是例如中间结果与总目标结果之间的差异。

然而,这种方式只会改善最佳案例和平均案例的复杂性。最糟糕的案件复杂性仍未受到影响。

通过某种分支切割可以提高最坏情况的复杂性。在这种情况下,我不确定是否可能。

答案 2 :(得分:6)

下面的c ++ 11中的工作解决方案。

基本思想是使用基于堆栈的评估(请参阅RPN)并将可行解决方案转换为infix notation仅用于显示目的。

如果我们有N个输入数字,我们将使用(N-1)个运算符,因为每个运算符都是二进制数。

首先,我们创建操作数和运算符的有效排列selector_数组)。有效排列是可以在没有堆栈下溢的情况下进行评估并且在堆栈上以恰好一个值(结果)结束的排列。因此1 1 +有效,但1 + 1不是。

我们使用操作数的每个排列(values_数组)和每个运算符组合(ops_数组)来测试每个这样的操作数 - 运算符置换。匹配的结果很漂亮。

从命令行获取参数[-s] <target> <digit>[ <digit>...]-s开关可防止穷举搜索,只打印第一个匹配结果。

(使用./mathpuzzle 348 1 3 7 6 8 3获取原始问题的答案)

此解决方案不允许将输入数字连接到表单编号。这可以作为额外的外部循环添加。

可以从here下载工作代码。 (注意:我更新了该代码,支持连接输入数字以形成解决方案)

有关其他说明,请参阅代码注释。

#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;
}

答案 3 :(得分:5)

输入显然是一组数字和运算符:D = {1,3,3,6,7,8,3}和Op = {+, - ,*,/}。最直接的算法是brute force求解器,enumerates这些集合的所有可能组合。可以根据需要经常使用集合Op的元素,但集合D中的元素只使用一次。伪代码:

D={1,3,3,6,7,8,3}
Op={+,-,*,/}
Solution=348
for each permutation D_ of D:
   for each binary tree T with D_ as its leafs:
       for each sequence of operators Op_ from Op with length |D_|-1:
           label each inner tree node with operators from Op_
           result = compute T using infix traversal
           if result==Solution
              return T
return nil

除此之外:阅读jedrus07和HPM的答案。

答案 4 :(得分:0)

我认为,您需要先严格定义问题。你被允许做什么,不做什么。你可以从简单开始,只允许乘法,除法,减法和加法。

现在您知道您的问题空间输入集,可用操作集和所需输入。如果只有4个操作和x输入,则组合数小于:

您可以执行操作的次数(x!)乘以每个步骤的可能操作选择:4 ^ x。正如您所看到的6个数字,它提供了合理的2949120操作。这意味着这可能是您对暴力算法的限制。

一旦你有蛮力并且你知道它有效,你就可以开始使用某种A* algorithm改进你的算法,这需要你定义启发式函数。

在我看来,最好的思考方式就是搜索问题。主要的困难是寻找良好的启发式方法,或者减少问题空间的方法(如果你的数字不能与答案相加,你至少需要一次乘法等)。从小处着手,在此基础上进行构建,并在获得一些代码后询问后续问题。

答案 5 :(得分:0)

很久以前,我从Oxford's Computer Science Docs(使用Java源代码)找到了很好的算法。每次阅读这个解决方案时我都很佩服。我相信这会有所帮助。

答案 6 :(得分:0)

到目前为止,最简单的方法是对其进行智能暴力破解。您只能通过6个数字和4个运算符来构建有限数量的表达式。

多少?由于您不必使用所有数字,并且可以多次使用同一运算符,因此,此问题等同于“最多可以使用6个叶子和四个可能的标签来制作多少个带标签的严格二叉树(aka完整的二叉树)每个非叶子节点?”。

具有n个叶子的完整二​​叉树的数量等于catalan(n-1)。您可以看到以下内容:

每个具有n个叶子的完整二​​叉树都有n-1个内部节点,并以独特的方式对应于一个具有n-1个节点的非完整二叉树(只需从完整的二叉树中删除所有叶子即可得到它)。碰巧有可能有n个节点的加泰罗尼亚语(n)二叉树,所以我们可以说,具有n个叶子的严格二叉树具有加泰罗尼亚语(n-1)可能不同的结构。

每个非叶节点有4种可能的运算符:4 ^(n-1)种可能性 叶子可以用n编号! *(6选择(n-1))不同的方式。 (对于出现k次的每个数字,将其除以k !,或者只是确保所有数字都不同)

因此,对于6个不同的数字和4个可能的运算符,您将获得Sum(n = 1 ... 6)[加泰罗尼亚语(n-1)* 6!/(6-n)! * 4 ^(n-1)]种可能的表达式,总计33,665,406。不是很多。

您如何枚举这些树?

给出所有具有n-1个或更少节点的树的集合,可以通过系统地将所有n-1个树与空树,所有n-2个树与1个节点树系统地配对来创建具有n个节点的所有树,所有n-3棵树以及所有2个节点树等,并将它们用作新形成的树的左右子树。

因此,从一个空集开始,首先生成一个只有一个根节点的树,然后从一个新的根开始,将其用作左子树或右子树,产生两个看起来像这样的树:/和。依此类推。

您可以即时将它们转换为一组表达式(只需在运算符和数字之间循环),然后进行评估,直到得出目标数字为止。

答案 7 :(得分:0)

我已经用Python编写了自己的倒数求解器。

这是代码;它也可以在GitHub上使用:

#!/usr/bin/env python3

import sys
from itertools import combinations, product, zip_longest
from functools import lru_cache

assert sys.version_info >= (3, 6)


class Solutions:

    def __init__(self, numbers):
        self.all_numbers = numbers
        self.size = len(numbers)
        self.all_groups = self.unique_groups()

    def unique_groups(self):
        all_groups = {}
        all_numbers, size = self.all_numbers, self.size
        for m in range(1, size+1):
            for numbers in combinations(all_numbers, m):
                if numbers in all_groups:
                    continue
                all_groups[numbers] = Group(numbers, all_groups)
        return all_groups

    def walk(self):
        for group in self.all_groups.values():
            yield from group.calculations


class Group:

    def __init__(self, numbers, all_groups):
        self.numbers = numbers
        self.size = len(numbers)
        self.partitions = list(self.partition_into_unique_pairs(all_groups))
        self.calculations = list(self.perform_calculations())

    def __repr__(self):
        return str(self.numbers)

    def partition_into_unique_pairs(self, all_groups):
        # The pairs are unordered: a pair (a, b) is equivalent to (b, a).
        # Therefore, for pairs of equal length only half of all combinations
        # need to be generated to obtain all pairs; this is set by the limit.
        if self.size == 1:
            return
        numbers, size = self.numbers, self.size
        limits = (self.halfbinom(size, size//2), )
        unique_numbers = set()
        for m, limit in zip_longest(range((size+1)//2, size), limits):
            for numbers1, numbers2 in self.paired_combinations(numbers, m, limit):
                if numbers1 in unique_numbers:
                    continue
                unique_numbers.add(numbers1)
                group1, group2 = all_groups[numbers1], all_groups[numbers2]
                yield (group1, group2)

    def perform_calculations(self):
        if self.size == 1:
            yield Calculation.singleton(self.numbers[0])
            return
        for group1, group2 in self.partitions:
            for calc1, calc2 in product(group1.calculations, group2.calculations):
                yield from Calculation.generate(calc1, calc2)

    @classmethod
    def paired_combinations(cls, numbers, m, limit):
        for cnt, numbers1 in enumerate(combinations(numbers, m), 1):
            numbers2 = tuple(cls.filtering(numbers, numbers1))
            yield (numbers1, numbers2)
            if cnt == limit:
                return

    @staticmethod
    def filtering(iterable, elements):
        # filter elements out of an iterable, return the remaining elements
        elems = iter(elements)
        k = next(elems, None)
        for n in iterable:
            if n == k:
                k = next(elems, None)
            else:
                yield n

    @staticmethod
    @lru_cache()
    def halfbinom(n, k):
        if n % 2 == 1:
            return None
        prod = 1
        for m, l in zip(reversed(range(n+1-k, n+1)), range(1, k+1)):
            prod = (prod*m)//l
        return prod//2


class Calculation:

    def __init__(self, expression, result, is_singleton=False):
        self.expr = expression
        self.result = result
        self.is_singleton = is_singleton

    def __repr__(self):
        return self.expr

    @classmethod
    def singleton(cls, n):
        return cls(f"{n}", n, is_singleton=True)

    @classmethod
    def generate(cls, calca, calcb):
        if calca.result < calcb.result:
            calca, calcb = calcb, calca
        for result, op in cls.operations(calca.result, calcb.result):
            expr1 = f"{calca.expr}" if calca.is_singleton else f"({calca.expr})"
            expr2 = f"{calcb.expr}" if calcb.is_singleton else f"({calcb.expr})"
            yield cls(f"{expr1} {op} {expr2}", result)

    @staticmethod
    def operations(x, y):
        yield (x + y, '+')
        if x > y:                     # exclude non-positive results
            yield (x - y, '-')
        if y > 1 and x > 1:           # exclude trivial results
            yield (x * y, 'x')
        if y > 1 and x % y == 0:      # exclude trivial and non-integer results
            yield (x // y, '/')


def countdown_solver():
    # input: target and numbers. If you want to play with more or less than
    # 6 numbers, use the second version of 'unsorted_numbers'.
    try:
        target = int(sys.argv[1])
        unsorted_numbers = (int(sys.argv[n+2]) for n in range(6))  # for 6 numbers
#        unsorted_numbers = (int(n) for n in sys.argv[2:])         # for any numbers
        numbers = tuple(sorted(unsorted_numbers, reverse=True))
    except (IndexError, ValueError):
        print("You must provide a target and numbers!")
        return

    solutions = Solutions(numbers)
    smallest_difference = target
    bestresults = []
    for calculation in solutions.walk():
        diff = abs(calculation.result - target)
        if diff <= smallest_difference:
            if diff < smallest_difference:
                bestresults = [calculation]
                smallest_difference = diff
            else:
                bestresults.append(calculation)
    output(target, smallest_difference, bestresults)


def output(target, diff, results):
    print(f"\nThe closest results differ from {target} by {diff}. They are:\n")
    for calculation in results:
        print(f"{calculation.result} = {calculation.expr}")


if __name__ == "__main__":
countdown_solver()

该算法的工作原理如下:

  1. 数字按降序放入长度为6的元组中。然后,创建所有长度为1到6的唯一子组,首先是最小的组。

    例如:(75,50,5,9,1,1)-> {(75,(50),(9),(5),(1),(75,50),(75, 9),(75、5),...,(75、50、9、5、1、1)}。

  2. 接下来,这些组被组织成一个层次树:每个组都被划分为其非空子组的所有唯一的无序对。

    示例:(9,5,1,1)-> [(9,5,1)+(1),(9,1,1)+(5),(5,1,1)+( 9),(9、5)+(1、1),(9、1)+(5、1)]。

  3. 在每组数字中,将执行计算并存储结果。对于长度为1的组,结果只是数字本身。对于较大的组,对每对子组进行计算:在每对子组中,使用+,-,x和/将第一个子组的所有结果与第二个子组的所有结果合并,并存储有效结果。

    示例:(75,5)由((75),(5))对组成。 (75)的结果是75; (5)的结果是5; (75,5)的结果是[75 + 5 = 80,75-5 = 70,75 * 5 = 375,75/5 = 15]。

  4. 以这种方式生成所有结果,从最小的组到最大的组。最后,该算法会迭代所有结果,然后选择与目标数字最匹配的结果。

对于一组m个数,算术运算的最大数量为

comps[m] = 4*sum(binom(m, k)*comps[k]*comps[m-k]//(1 + (2*k)//m) for k in range(1, m//2+1))

对于所有长度为1到6的组,则最大计算总数为

total = sum(binom(n, m)*comps[m] for m in range(1, n+1))

,即1144386。实际上,它要少得多,因为该算法重用了重复组的结果,忽略了琐碎的运算(加0,乘以1等),并且因为游戏规则规定了中间结果必须为正整数(这限制了除法运算符的使用)。

答案 8 :(得分:0)

我写了一个稍微简单的版本:

  1. 对于列表中2个(不同的)元素的每种组合,并使用+,-,*,/进行组合(请注意,由于a> b则仅需要ab,而如果a%b = 0则仅需a / b)
  2. 如果组合是目标,则记录解决方案
  3. 递归调用简化列表

答案 9 :(得分:0)

鉴于Rachel Reiley在镜头前做这些(而Carole Vordeman在过去的表演中做了),我在想有一个非暴力解决方案潜伏在某个地方。

答案 10 :(得分:0)

我编写了一个终端应用程序来执行此操作: https://github.com/pg328/CountdownNumbersGame/tree/main

在内部,我提供了一个求解空间大小的计算示例(它是n *((n-1)!^ 2)*(2 ^ n-1),因此:n = 6- > 2,764,800。我知道,总的来说,更重要的是为什么。如果您想检查一下,可以在这里找到我的实现,但如果不这样做,我将在这里进行解释。

本质上,最糟糕的是暴力破解,因为据我所知,如果不进行明确检查,就无法确定任何特定分支是否会得出有效答案。话虽如此,平均情况只是其中的一小部分;它是{那个数字}除以有效解决方案的数量(我倾向于在程序上看到1000,其中10个左右是唯一的,其余是10个的排列)。如果我随手摇了一个数字,我会说大约2765个分支来检查,这几乎没有时间。 (是的,即使在Python中也是如此。)

TL; DR:即使解决方案空间很大,并且要花费数百万次操作才能找到所有解决方案,但仅需要一个答案。最好的方法是蛮力,直到找到并吐出为止。