16

我正在写一些问答游戏,如果玩家未能解决问题,我需要计算机来解决问答中的 1 个游戏。

给定数据:

  1. 要使用的 6 个数字的列表,例如 4、8、6、2、15、50。
  2. 目标值,其中 0 < 值 < 1000,例如 590。
  3. 可用的操作是除法、加法、乘法和除法。
  4. 可以使用括号。

生成评估等于或尽可能接近目标值的数学表达式。例如,对于上面给出的数字,表达式可以是:(6 + 4) * 50 + 15 * (8 - 2) = 590

我的算法如下:

  1. 从上面的(1)生成给定数字的所有子集的所有排列
  2. 对于每个排列,生成所有括号和运算符组合
  3. 在算法运行时跟踪最接近的值

我想不出对上述蛮力算法的任何智能优化,这将加速它的数量级。另外我必须针对最坏的情况进行优化,因为许多问答游戏将在服务器上同时运行。

今天为解决这个问题而编写的代码是(从项目中提取的相关内容):

from operator import add, sub, mul, div
import itertools


ops = ['+', '-', '/', '*']
op_map = {'+': add, '-': sub, '/': div, '*': mul}

# iterate over 1 permutation and generates parentheses and operator combinations
def iter_combinations(seq):
    if len(seq) == 1:
        yield seq[0], str(seq[0])
    else:
        for i in range(len(seq)):
            left, right = seq[:i], seq[i:]  # split input list at i`th place
            # generate cartesian product
            for l, l_str in iter_combinations(left):
                for r, r_str in iter_combinations(right):
                    for op in ops:
                        if op_map[op] is div and r == 0:  # cant divide by zero
                            continue
                        else:
                            yield op_map[op](float(l), r), \
                                  ('(' + l_str + op + r_str + ')')

numbers = [4, 8, 6, 2, 15, 50]
target = best_value = 590
best_item = None

for i in range(len(numbers)):
    for current in itertools.permutations(numbers, i+1): # generate perms
        for value, item in iter_combinations(list(current)):
            if value < 0:
                continue

            if abs(target - value) < best_value:
                best_value = abs(target - value)
                best_item = item

print best_item

它打印:((((4*6)+50)*8)-2)。用不同的值对其进行了一些测试,它似乎工作正常。我还有一个功能可以删除不必要的括号,但它与问题无关,所以没有发布。

问题在于,由于所有这些排列、组合和评估,这运行非常缓慢。在我的 mac book air 上,它运行了几分钟,例如 1 个。我想让它在几秒钟内运行在同一台机器上,因为许多问答游戏实例将同时在服务器上运行。所以问题是:

  1. 我可以以某种方式(按数量级)加速当前算法吗?
  2. 我是否缺少针对此问题的其他一些运行速度更快的算法?
4

8 回答 8

13

您可以使用给定的数字构建所有可能的表达式树并评估它们。您不需要将它们全部保存在内存中,只需在找到目标编号时打印它们:

首先我们需要一个类来保存表达式。最好将其设计为不可变的,因此可以预先计算其值。像这样的东西:

class Expr:
    '''An Expr can be built with two different calls:
           -Expr(number) to build a literal expression
           -Expr(a, op, b) to build a complex expression. 
            There a and b will be of type Expr,
            and op will be one of ('+','-', '*', '/').
    '''
    def __init__(self, *args):
        if len(args) == 1:
            self.left = self.right = self.op = None
            self.value = args[0]
        else:
            self.left = args[0]
            self.right = args[2]
            self.op = args[1]
            if self.op == '+':
                self.value = self.left.value + self.right.value
            elif self.op == '-':
                self.value = self.left.value - self.right.value
            elif self.op == '*':
                self.value = self.left.value * self.right.value
            elif self.op == '/':
                self.value = self.left.value // self.right.value

    def __str__(self):
        '''It can be done smarter not to print redundant parentheses,
           but that is out of the scope of this problem.
        '''
        if self.op:
            return "({0}{1}{2})".format(self.left, self.op, self.right)
        else:
            return "{0}".format(self.value)

现在我们可以编写一个递归函数,它使用给定的表达式集构建所有可能的表达式树,并打印等于我们目标值的那些。我们将使用该itertools模块,这总是很有趣。

我们可以使用itertools.combinations()or itertools.permutations(),区别在于顺序。我们的一些操作是可交换的,有些不是,所以我们可以使用permutations()并假设我们会得到许多非常相似的解决方案。或者combinations(),当操作不可交换时,我们可以使用并手动重新排序值。

import itertools
OPS = ('+', '-', '*', '/')
def SearchTrees(current, target):
    ''' current is the current set of expressions.
        target is the target number.
    '''
    for a,b in itertools.combinations(current, 2):
        current.remove(a)
        current.remove(b)
        for o in OPS:
            # This checks whether this operation is commutative
            if o == '-' or o == '/':
                conmut = ((a,b), (b,a))
            else:
                conmut = ((a,b),)

            for aa, bb in conmut:
                # You do not specify what to do with the division.
                # I'm assuming that only integer divisions are allowed.
                if o == '/' and (bb.value == 0 or aa.value % bb.value != 0):
                    continue
                e = Expr(aa, o, bb)
                # If a solution is found, print it
                if e.value == target:
                    print(e.value, '=', e)
                current.add(e)
                # Recursive call!
                SearchTrees(current, target)
                # Do not forget to leave the set as it were before
                current.remove(e)
        # Ditto
        current.add(b)
        current.add(a)

然后是主要调用:

NUMBERS = [4, 8, 6, 2, 15, 50]
TARGET = 590

initial = set(map(Expr, NUMBERS))
SearchTrees(initial, TARGET)

并做了!有了这些数据,我在 21 秒内就获得了 719 种不同的解决方案!当然,其中许多是同一表达式的微不足道的变体。

于 2013-12-15T19:23:46.850 回答
2

六数、四运算和括号的所有组合最大为 5 * 9!至少。所以我认为你应该使用一些人工智能算法。使用遗传编程或优化似乎是要遵循的路径。

在第 11 章进化智能的编程集体智能一书中,你会找到你想要的以及更多。该章解释了如何找到一个结合运算和数字(如你所愿)来匹配结果的数学函数。你会惊讶于这样的任务是多么容易。

PD:这些示例是使用 Python 编写的。

于 2013-12-17T20:10:53.427 回答
1

24 游戏是 4 个数字来定位 24,您的游戏是 6 个数字来定位 x (0 < x < 1000)。

这很相似。

这是快速解决方案,获取所有结果并在我的 rMBP 中仅打印一个 about 1-3s,我认为在这个游戏中打印一个解决方案是可以的 :),我稍后会解释:

def mrange(mask):
    #twice faster from Evgeny Kluev
    x = 0
    while x != mask:
        x = (x - mask) & mask
        yield x 

def f( i ) :
    global s
    if s[i] :
        #get cached group
        return s[i]
    for x in mrange(i & (i - 1)) :
        #when x & i == x
        #x is a child group in group i
        #i-x is also a child group in group i
        fk = fork( f(x), f(i-x) )
        s[i] = merge( s[i], fk )
    return s[i] 

def merge( s1, s2 ) :
    if not s1 :
        return s2
    if not s2 :
        return s1
    for i in s2 :
        #print just one way quickly
        s1[i] = s2[i]
        #combine all ways, slowly
        # if i in s1 :
        #   s1[i].update(s2[i])
        # else :
        #   s1[i] = s2[i]
    return s1   

def fork( s1, s2 ) :
    d = {}
    #fork s1 s2
    for i in s1 :
        for j in s2 :
            if not i + j in d :
                d[i + j] = getExp( s1[i], s2[j], "+" )
            if not i - j in d :
                d[i - j] = getExp( s1[i], s2[j], "-" )
            if not j - i in d :
                d[j - i] = getExp( s2[j], s1[i], "-" )
            if not i * j in d :
                d[i * j] = getExp( s1[i], s2[j], "*" )
            if j != 0 and not i / j in d :
                d[i / j] = getExp( s1[i], s2[j], "/" )
            if i != 0 and not j / i in d :
                d[j / i] = getExp( s2[j], s1[i], "/" )
    return d    

def getExp( s1, s2, op ) :
    exp = {}
    for i in s1 :
        for j in s2 :
            exp['('+i+op+j+')'] = 1
            #just print one way
            break
        #just print one way
        break
    return exp  

def check( s ) :
    num = 0
    for i in xrange(target,0,-1):
        if i in s :
            if i == target :
                print numbers, target, "\nFind ", len(s[i]), 'ways'
                for exp in s[i]:
                    print exp, ' = ', i
            else :
                print numbers, target, "\nFind nearest ", i, 'in', len(s[i]), 'ways'
                for exp in s[i]:
                    print exp, ' = ', i
            break
    print '\n'  

def game( numbers, target ) :
    global s
    s = [None]*(2**len(numbers))
    for i in xrange(0,len(numbers)) :
        numbers[i] = float(numbers[i])
    n = len(numbers)
    for i in xrange(0,n) :
        s[2**i] = { numbers[i]: {str(numbers[i]):1} }   

    for i in xrange(1,2**n) :
        #we will get the f(numbers) in s[2**n-1]
        s[i] = f(i) 

    check(s[2**n-1])    



numbers = [4, 8, 6, 2, 2, 5]
s = [None]*(2**len(numbers))    

target = 590
game( numbers, target ) 

numbers = [1,2,3,4,5,6]
target = 590
game( numbers, target )

假设A是您的 6 个数字列表。

我们定义f(A)的是所有可以通过所有A数字计算的结果,如果我们搜索f(A),我们将找到目标是否在其中并得到答案或最接近的答案。

我们可以拆分A为两个真正的子组:A1A-A1( A1 不为空且不等于 A ) ,这将问题从f(A)f(A1)f(A-A1)。因为我们知道f(A) = Union( a+b, a-b, b-a, a*b, a/b(b!=0), b/a(a!=0) ),其中a in A,b in A-A1

我们使用 forkf(A) = Union( fork(A1,A-A1) )代表这样的过程。我们可以删除所有重复的值fork(),这样我们就可以减少范围,使程序更快

因此,如果A = [1,2,3,4,5,6],则f(A) = fork( f([1]),f([2,3,4,5,6]) ) U ... U fork( f([1,2,3]), f([4,5,6]) ) U ... U代表联合。

我们将看到 f([2,3,4,5,6]) = fork( f([2,3]), f([4,5,6]) ) U ... , f([3, 4,5,6]) = fork( f([3]), f([4,5,6]) ) U ...,两者都使用了 f([4,5,6])。

因此,如果我们可以缓存每个 f([...]) 程序可以更快

我们可以在 A 中得到2^len(A) - 2(A1,A-A1)。我们可以使用二进制来表示。

例如:A = [1,2,3,4,5,6],A1 = [1,2,3],则二进制000111(7) 代表 A1。A2 = [1,3,5],二进制010101(21)代表A2。A3 = [1],那么二进制000001(1)代表A3...

所以我们得到了一个代表 A 中所有组的方式,我们可以缓存它们并使所有过程更快!

于 2013-12-17T09:08:25.567 回答
1

我会尝试使用 AST,至少它
会使你的表达式生成部分更容易
(不需要弄乱括号)。

http://en.wikipedia.org/wiki/Abstract_syntax_tree

1) 生成一些带有 N 个节点的树
(N = 你拥有的数字的数量)。
我之前读过你有多少,
随着 N 的增长,它们的大小很严重。
我所说的严肃,至少可以说不仅仅是多项式。

2)现在只需开始更改
非叶节点中的操作并继续评估
结果。

但这又是回溯和过多的自由度。
这是您提出的一项计算复杂的任务。我相信如果你
像你一样问这个问题:“让我们在输出上生成一个数字 K,
使得 |KV| 最小”(这里 V 是预定义的期望结果,
即在你的示例中为 590),那么我猜这个问题甚至是 NP -完全的。

如果我的直觉对我撒谎,请有人纠正我。

所以我认为即使是所有可能的 AST 的生成(假设只
允许 1 个操作)也是 NP 完整的,因为它们的计数不是多项式的。不要说
这里允许超过 1 次操作,也不要谈论最小差异要求
(结果和期望结果之间)。

于 2013-12-12T22:38:07.940 回答
1

1. 快速的完全在线算法

这个想法不是搜索目标值的单个表达式,而是搜索目标值包含在等式的一部分中并且两个部分具有几乎相等数量的操作(2和3)的等式。由于方程的每个部分都相对较小,因此为给定的输入值生成所有可能的表达式并不需要太多时间。在生成方程的两个部分之后,可以扫描包含这些表达式值的一对排序数组,并在其中找到一对相等(或至少是最佳匹配)的值。找到两个匹配值后,我们可以得到对应的表达式并将它们连接成一个表达式(换句话说,求解方程)。

要将两棵表达式树连接在一起,我们可以从一棵树的根下降到“目标”叶,对于该路径上的每个节点,反转相应的操作 ('*' to '/', '/' to '*' or '/', '+' to '-', '-' to '+' or '-'),并将“反转”的根节点移动到另一棵树(也作为根节点)。

当所有操作都是可逆的时,该算法更快更容易实现。所以最好使用浮点除法(如我的实现)或有理除法。截断整数除法是最困难的情况,因为它对不同的输入产生相同的结果(42/25=1 和 25/25 也是 1)。使用零余整数除法,该算法在获得精确结果时几乎立即给出结果,但在需要近似结果时需要进行一些修改才能正常工作。

请参阅 Ideone 上的实现


2. 更快的离线预处理方法

正如@WolframH 所注意到的,没有那么多可能的输入数字组合。如果可以重复,则只有 3*3*( 4 9+4-1 ) = 4455。或 3*3*( 4 9 ) = 1134 没有重复。这使我们能够离线预处理所有可能的输入,以紧凑的形式存储结果,并在需要某些特定结果时快速解压缩预处理值之一。

预处理程序应采用 6 个数字的数组并为所有可能的表达式生成值。然后它应该丢弃超出范围的值,并为所有没有完全匹配的情况找到最接近的结果。所有这些都可以通过@Tim 提出的算法来执行。他的代码需要最少的修改来做到这一点。它也是最快的选择(迄今为止)。由于预处理是离线的,我们可以使用比解释 Python 更好的东西。一种选择是 PyPy,另一种是使用一些快速解释语言。预处理所有可能的输入不应超过几分钟。

谈到存储所有预处理值所需的内存,唯一的问题是结果表达式。如果以字符串形式存储,它们将占用 4455*999*30 字节或 120Mb。但是每个表达式都可以被压缩。它可以用后缀表示法表示,如下所示: arg1 arg2 + arg3 arg4 + *. 为了存储它,我们需要 10 位来存储所有参数的排列,10 位来存储 5 个操作,8 位来指定参数和操作如何交错(6 个参数 + 5 个操作 - 3 个预定义位置:前两个始终是参数,最后一个始终是操作)。每棵树 28 位或 4 字节,这意味着整个数据集有重复数据集只有 20Mb,没有重复数据集只有 5Mb。


3.慢速完全在线算法

OP中有一些加速算法的方法:

  1. 如果我们避免尝试每个交换操作两次并使递归树不那么多枝,则可以实现最大的速度提升。
  2. 通过删除除法运算结果为零的所有分支,可以进行一些优化。
  3. 记忆(动态编程)在这里不能显着提高速度,但它仍然可能有用。

在使用这些想法增强 OP 的方法后,实现了大约 30 倍的加速:

from itertools import combinations

numbers = [4, 8, 6, 2, 15, 50]
target = best_value = 590
best_item = None
subsets = {}


def get_best(value, item):
    global best_value, target, best_item

    if value >= 0 and abs(target - value) < best_value:
        best_value = abs(target - value)
        best_item = item

    return value, item


def compare_one(value, op, left, right):
    item = ('(' + left + op + right + ')')
    return get_best(value, item)


def apply_one(left, right):
    yield compare_one(left[0] + right[0], '+', left[1], right[1])
    yield compare_one(left[0] * right[0], '*', left[1], right[1])
    yield compare_one(left[0] - right[0], '-', left[1], right[1])
    yield compare_one(right[0] - left[0], '-', right[1], left[1])

    if right[0] != 0 and left[0] >= right[0]:
        yield compare_one(left[0] / right[0], '/', left[1], right[1])

    if left[0] != 0 and right[0] >= left[0]:
        yield compare_one(right[0] / left[0], '/', right[1], left[1])


def memorize(seq):
    fs = frozenset(seq)

    if fs in subsets:
        for x in subsets[fs].items():
            yield x
    else:
        subsets[fs] = {}
        for value, item in try_all(seq):
            subsets[fs][value] = item
            yield value, item


def apply_all(left, right):
    for l in memorize(left):
        for r in memorize(right):
            for x in apply_one(l, r):
                yield x;


def try_all(seq):
    if len(seq) == 1:
        yield get_best(numbers[seq[0]], str(numbers[seq[0]]))

    for length in range(1, len(seq)):
        for x in combinations(seq[1:], length):
            for value, item in apply_all(list(x), list(set(seq) - set(x))):
                yield value, item


for x, y in try_all([0, 1, 2, 3, 4, 5]): pass

print best_item

如果您为问题添加一些约束,则可能会提高速度:

  1. 如果仅当余数为零时才可以进行整数除法。
  2. 如果所有中间结果都是非负数和/或低于 1000。
于 2013-12-15T17:42:21.970 回答
0

动态规划呢,因为您需要相同的结果来计算其他选项?

于 2013-12-20T17:13:58.473 回答
0

实际上,您可以做两件事来将时间加快到毫秒

您正在尝试通过生成数字和目标数字来找到给定测验的解决方案。相反,您可以生成解决方案并删除操作。您可以构建一些智能的东西来生成多个测验并选择最有趣的一个,但是在这种情况下您会失去尽可能接近的选项。

另一种方法是预先计算。解决 100 个测验,将它们用作应用程序的内置内容,并即时生成新的测验,尝试将测验堆栈保持在 100,并尝试只给用户新的测验。我在圣经游戏中遇到了同样的问题,我用这种方法来加快速度。当我在后台生成新问题并始终将我的堆栈保持在 100 时,而不是 10 秒的问题

于 2013-12-19T09:31:00.177 回答
0

好吧,我不会放弃。按照您问题的所有答案,我提出了另一种算法。该算法给出了平均时间为 3 毫秒的解。

#! -*- coding: utf-8 -*-
import copy

numbers = [4, 8, 6, 2, 15, 50]
target = 590

operations  = {
    '+': lambda x, y: x + y, 
    '-': lambda x, y: x - y,
    '*': lambda x, y: x * y,
    '/': lambda x, y: y == 0 and 1e30 or x / y   # Handle zero division
}

def chain_op(target, numbers, result=None, expression=""):
    if len(numbers) == 0:
        return (expression, result)
    else:
        for choosen_number in numbers:
            remaining_numbers = copy.copy(numbers)
            remaining_numbers.remove(choosen_number)
            if result is None:
                return chain_op(target, remaining_numbers, choosen_number, str(choosen_number))
            else:
                incomming_results = []
                for key, op in operations.items():
                    new_result = op(result, choosen_number)
                    new_expression = "%s%s%d" % (expression, key, choosen_number)
                    incomming_results.append(chain_op(target, remaining_numbers, new_result, new_expression))
                diff = 1e30
                selected = None
                for exp_result in incomming_results:
                    exp, res = exp_result
                    if abs(res - target) < diff:
                        diff = abs(res - target)
                        selected = exp_result
                    if diff == 0:
                        break
                return selected

if __name__ == '__main__':
    print chain_op(target, numbers)

勘误: 此算法不包括包含括号的解决方案。它总是达到目标或最接近的结果,我的错。还是挺快的。它可以适应支持括号而不需要太多工作。

于 2013-12-17T22:05:57.540 回答