高效算法,可针对特定目标组成有效的表达式
问题内容:
问题表示为:给定一个仅包含数字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))
问题答案:
面对这种编程挑战,我首先尝试回答以下问题:
- 表达式应如何表示?
- 我们可以减少可能表达的数量吗?
- 我们可以为每个表达式做更少的工作吗?
表示表达式
看起来像小型编程语言的问题往往使我想到Lisp。问题是要求我们生成系列:
123
(* 12 3)
(+ 12 3)
...
(- (- 1 2) 3)
基本上在3元组(operator, 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
是一个很大的数字,从这一点来看,任何表达式甚至都能够得到just的结果5
吗?我们可以完全跳过创建它们吗?
不幸的是,末尾的数字仍然可以做出重大更改:
(+ (* (* 42 81) 0) 5)
我们可能会避免一些分支,但是我们将不得不考虑大多数表达式。
减少工作
好的,鉴于我们实际上必须获得大量表达式的结果,是否还有其他方法可以节省工作量?
让我们想象一下,我们正在逐步生成一个序列,这三个最终表达式是一个接一个地生成的:
...
(* (- 8 (* 3 6)) 1)
(+ (- 8 (* 3 6)) 1)
(- (- 8 (* 3 6)) 1)
...
它们都给出不同的结果,[12, 13, 11]
但是内部部分(- 8 (* 3 6))
是相同的,并且将永远是12
。我们的解决方案应该利用这一优势。
一个答案
对于需要剧透的人,我为最初的实现建立了分支,该实现从顶部开始计算每个表达式,一个较小的更改可以存储计算结果,最后一个可以在生成表达式时预先计算结果以及一些小的调整。。
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))
通过存储有关操作的更多信息,而不仅仅是执行它们的功能,可以通过多种方法来解决。对于这个问题,这并不是真正需要的,其他可能的转换也不是必需的,例如组合多个表达式或排除不相关的部分。
最后的实现说明,很困难,我将迭代的方向和(最初)表达式的布局都向后。