【问题标题】:Efficient Algorithm to compose valid expressions with specific target用特定目标组合有效表达式的高效算法
【发布时间】:2017-05-07 23:37:52
【问题描述】:

问题表述为:给定一个仅包含数字 0-9 和目标值的字符串,返回通过在数字之间添加一些二元运算符(+、- 或 *)创建的所有表达式,以便它们计算为目标值。在某些情况下,可能没有任何二元运算符会创建有效的表达式,在这种情况下,函数应该返回一个空数组。新表达式中的数字不应包含前导零。

该函数应返回所有计算结果为目标的有效表达式,并按字典顺序排序。

例如:

digits = "123" 和 target = 6,应该返回:["1*2*3", "1+2+3"]

我目前的算法如下。它有点慢,所以我正在寻找一种更有效的方法来解决这个问题。我当前的算法产生操作数和运算符的所有组合。对于上面的例子,它会产生

操作数:

[['1', '2', '3'], ['1', '23'], ['12', '3'], ['123']]

运营商:

{0: [()], 1: [('+',), ('-',), ('*',)], 2: [('+', '+'), ('+', '-'), ('+', '*'), ('-', '+'), ('-', '-'), ('-', '*'), ('*', '+'), ('*', '-'), ('*', '*')]}

然后它将操作数和运算符的所有可能组合组合起来,并对每个组合进行评估。

数字有 2 ≤ digits.length ≤ 10. 的约束,所以它不是那么糟糕,但是使用这个算法,长度为 10 的数字大约需要 4.3 秒,而它应该只需要 4 秒(最长)。

我还尝试使用以下替代方法加速 eval() 函数:

if eval(temp) == target:

exp_as_func = eval('lambda: ' + temp)
if exp_as_func() == target:

compiled = compile(temp, '<string>', 'eval')
if compiled == target:

在使用 Python 3 时,所有这些仍然需要大约相同的时间。

代码:

import itertools
import time

def getValidExp(digits, target):    
    def getSign_combination(length):
        signCombo = {}
        for i in range(0, length):
            signCombo[i] = [c for c in itertools.product(('+', '-', '*'), repeat=i)]
        return signCombo

    def generate_combination(source, comb):
        res = []
        for x, action in zip(source, comb + (0,)):      
            res.append(x)
            if action == 0:
                #####IF ITS A 0, YIELD STRING.  IF NOT COMBINE NEXT ONE
                yield "".join(res)
                res = []


    #####PRODUCT GENERATES (0,0,1).  ALL COMBINATIONS.  0 MEANS BY ITSELF, 1 APPEND NEXT ITEM.

    elementCombo = [list(generate_combination(digits, c)) for c in itertools.product((0, 1), repeat=len(digits) - 1)]

    signCombo = getSign_combination(len(digits))

    result = []
    for e in elementCombo:
        signs = signCombo[len(e)-1]

        for i,sign in enumerate(signs):

            temp = [ item for tple in zip(e, sign) for item in tple ]
            temp.append(e[-1])
            temp = "".join(temp)

            try:
                if eval(temp) == target:
                    result.append(temp)
            except:
                pass

    return sorted(result)

digits = "3456237490"
target = 9180
print("Answer:", getValidExp(digits, target))

使用计算器函数的代码(没有 eval()),几乎具有相同的速度:

from itertools import combinations, permutations
import itertools
import time

def getValidExp(digits, target):

    def calculate(s):
        operands, operators = [], []
        operand = ""
        for i in reversed(range(len(s))):
            if s[i].isdigit():
                operand += s[i]
                if i == 0 or not s[i - 1].isdigit():
                    operands.append(int(operand[::-1]))
                    operand = ""
            elif s[i] == '*':
                operators.append(s[i])
            elif s[i] == '+' or s[i] == '-':
                while operators and operators[-1] == '*':
                    compute(operands, operators)
                operators.append(s[i])

        while operators:
            compute(operands, operators)

        return operands[-1]

    def compute(operands, operators):
        left, right = operands.pop(), operands.pop()
        op = operators.pop()
        if op == '+':
            operands.append(left + right)
        elif op == '-':
            operands.append(left - right)
        elif op == '*':
            operands.append(left * right)

    def getSign_combination(length):
        signCombo = {}
        for i in range(0, length):
            signCombo[i] = [c for c in itertools.product(('+', '-', '*'), repeat=i)]
        return signCombo

    def generate_combination(source, comb):
        res = []
        for x, action in zip(source, comb + (0,)):
            res.append(x)
            if action == 0:
                yield "".join(res)
                res = []

    start = time.clock()

    #####PRODUCT GENERATES (0,0,1).  ALL COMBINATIONS.  0 MEANS BY ITSELF, 1 APPEND NEXT ITEM.
    elementCombo = [list(generate_combination(digits, c)) for c in itertools.product((0, 1), repeat=len(digits) - 1)]

    signCombo = getSign_combination(len(digits))

    result = []
    for e in elementCombo:
        signs = signCombo[len(e)-1]
        for i,sign in enumerate(signs):
            temp = ""
            valid = True

            for num in e:
                if num[0] == '0' and len(num) > 1:
                    valid = False
                    break

            if valid:
                for num,operator in zip(e,sign):
                    temp += num
                    temp += operator

                temp += e[-1]

                ####USING CALCULATOR CODE
                if calculate(temp) == target:
                    result.append(temp)

    print(time.clock() - start)
    return sorted(result)


digits = "3456237490"
target = 9180
print("Answer:", getValidExp(digits, target))

【问题讨论】:

标签: python algorithm combinations


【解决方案1】:

面对这种编程挑战,我首先尝试回答以下问题:

  • 表达式应该如何表示?
  • 我们可以减少可能表达的数量吗?
  • 我们可以为每个表达式做更少的工作吗?

表示表达式

看起来像小型编程语言的问题往往让我想到 Lisp。问题是要求我们生成系列:

123
(* 12 3)
(+ 12 3)
...
(- (- 1 2) 3)

基本上是(operator, left, right) 的三元组中的二进制表达式,其中 left 和 right 也可以是表达式。组件的顺序实际上并不重要。 Python 有元组,在operator 模块中,它具有用于各种二进制操作的函数。因此,我计划以以下形式构建表达式:

(operator.sub, (operator.sub, 1, 2), 3)

然后可以使用(大部分)简单的递归函数对其进行评估:

def compute(expr):
    if isinstance(expr, tuple):
            op, left, right = expr
            return op(compute(left), compute(right))
    return expr

减少可能性

从问题描述来看,每个给定的数字似乎都有指数级的可能表达式。我们可以通过创建所有排列来消除其中的一些吗?

例如,输入一个六位数字,目标结果为5。在创建排列的过程中,假设从前四位创建了以下表达式,还有两个需要处理:

(* 42 81) '??'

3696 是一个很大的数字,从这一点开始的任何表达式甚至都能够得到5 的结果吗?我们可以完全跳过创建它们吗?

不幸的是,接近末尾的数字仍然可以进行重大更改:

(+ (* (* 42 81) 0) 5)

我们可能会避免一些分支,但我们将不得不考虑大多数表达式。

做更少的工作

好的,鉴于我们实际上必须得到大量表达式的结果,还有其他方法可以省力吗?

假设我们正在生成一个序列,这三个最终表达式一个接一个地生成:

...
(* (- 8 (* 3 6)) 1)
(+ (- 8 (* 3 6)) 1)
(- (- 8 (* 3 6)) 1)
...

它们都给出不同的结果,[12, 13, 11],但内部部分(- 8 (* 3 6)) 是相同的,并且永远是12。我们的解决方案应该利用这一点。

答案

对于任何需要剧透的人,我已经提交了 branches 用于计算从顶部开始的每个表达式的 initial implementationminor change that memoises the calculation 和最后一个 precomputes results,因为表达式正在生成加上一些minor tweaks

  • 17.40s elapsed 6180k max mem原题
  • 20.60s elapsed 6284k max mem 没有问题的评估
  • 4.65s elapsed 5356k max mem我的首字母
  • 2.71s elapsed 5316k max mem我的记忆
  • 1.50s elapsed 5356k max mem我的预计算

关于我的实现的一些说明。 generate() 函数通过考虑字符串中的每个点并创建可能的下一个状态来创建候选表达式。例如,在开始时,两者都将标记移动,并将第一个数字分开:

'3|456237490' ->
    '34|56237490' -> ...
    3 '4|56237490' ->

每个未决状态都被推送到一个列表中,并且每次循环都会弹出当前要考虑的状态。从最后的状态继续,下一个可能性是再次移动标记,并将一个数字拆分为三个表达式之一。

        3 '45|6237490' -> ...
        (* 3 4) '5|6237490' -> ...
        (+ 3 4) '5|6237490' -> ...
        (- 3 4) '5|6237490' -> ...

到目前为止,我已经掩盖了运算符优先级的一个问题。在处理乘法时,我们可能需要重写现有的表达式。考虑:

(+ 1 2) '3|' ->
    (* (+ 1 2) 3) '' # ???
    (+ (+ 1 2) 3) ''
    (- (+ 1 2) 3) ''

对于加法和减法,这很好,顺序无关紧要。但是,2 * 3 必须在 1 + ... 之前发生。总之,我们需要把乘法推入里面:

(+ 1 2) 3 -> (+ 1 (* 2 3))

有很多巧妙的方法来处理这个问题,方法是存储更多关于你的操作的信息,而不仅仅是执行它们的函数。对于这个不是真正需要的问题,也不需要其他可能的转换,例如组合多个表达式或分解不相关的部分。

最后的实现说明,只是为了比较困难,我将迭代方向和(最初)表达式的布局都向后。

【讨论】:

  • 我使用计算器功能添加了我的代码。不过似乎几乎一样。
  • 您更新后的代码仍在最后的计算步骤中做了大量工作,这些工作应该提前完成或完全避免。我将用一些关于如何处理此类问题的详细信息来编辑我的答案,并将我汇总的内容联系起来,它的速度大约快了一个数量级。
  • 是的,试图优化计算功能,但没有运气。我很想看看您如何实现更优化的计算器功能
  • 是的,绝对有帮助。只是想研究您实施不同答案的方式。你在这里提供了很多信息,需要一段时间才能沉浸在大声笑中。感谢您的详尽解释。
  • 对于数字 = "105" 和目标 = 5,您的代码返回 ["1*0+5", "1*5", "10-5"]。应该只是 ["1*0+5", "10-5"]。 “1 * 5”无效,因为您必须在解决方案中包含所有数字。尝试更新您的 generate() 函数,但似乎找不到更新的位置
猜你喜欢
  • 2018-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-09
  • 2022-08-09
  • 1970-01-01
相关资源
最近更新 更多