给定n
个整数,是否有O(n)
或O(n log n)
算法可以计算可以通过插入运算符-
获得的数学表达式的最大值, +
,*
和给定数字之间的括号?假设只有运算符的二元变量,所以没有一元减号,除非在第一个元素之前需要。
例如,给定-3 -4 5
,我们可以构建表达式(-3) * (-4) * 5
,其值为60
,并且最大可能。
背景
我在研究遗传算法时偶然发现了这个问题,并且了解到它可以通过经典的遗传算法很简单地解决。然而,这种情况运行缓慢,理论上它只是简单,因为代码在实践中变得相当丑陋(评估表达式,检查括号的正确放置等)。更重要的是,我们无法保证找到绝对最大值。
遗传算法的所有这些缺点让我想知道:既然我们不必担心分裂,有没有办法用更经典的方法有效地做到这一点,比如动态编程或贪婪策略?
更新
这是一个F#程序,它实现了@Keith Randall提出的DP解决方案以及我的改进,我在他的帖子评论中写道。这是非常低效的,但我认为它是多项式并且具有立方复杂性。它可以在几秒钟内运行~50个元素阵列。如果以完全强制的方式编写它可能会更快,因为很多时候可能浪费在构建和遍历列表上。
open System
open System.IO
open System.Collections.Generic
let Solve (arr : int array) =
let memo = new Dictionary<int * int * int, int>()
let rec Inner st dr last =
if st = dr then
arr.[st]
else
if memo.ContainsKey(st, dr, last) then
memo.Item(st, dr, last)
else
match last with
| 0 -> memo.Add((st, dr, last),
[
for i in [st .. dr - 1] do
for j in 0 .. 2 do
for k in 0 .. 2 do
yield (Inner st i j) * (Inner (i + 1) dr k)
] |> List.max)
memo.Item(st, dr, last)
| 1 -> memo.Add((st, dr, last),
[
for i in [st .. dr - 1] do
for j in 0 .. 2 do
for k in 0 .. 2 do
yield (Inner st i j) + (Inner (i + 1) dr k)
] |> List.max)
memo.Item(st, dr, last)
| 2 -> memo.Add((st, dr, last),
[
for i in [st .. dr - 1] do
for j in 0 .. 2 do
for k in 0 .. 2 do
yield (Inner st i j) - (Inner (i + 1) dr k)
] |> List.max)
memo.Item(st, dr, last)
let noFirst = [ for i in 0 .. 2 do yield Inner 0 (arr.Length - 1) i ] |> List.max
arr.[0] <- -1 * arr.[0]
memo.Clear()
let yesFirst = [ for i in 0 .. 2 do yield Inner 0 (arr.Length - 1) i ] |> List.max
[noFirst; yesFirst] |> List.max
let _ =
printfn "%d" <| Solve [|-10; 10; -10|]
printfn "%d" <| Solve [|2; -2; -1|]
printfn "%d" <| Solve [|-5; -3; -2; 0; 1; -1; -1; 6|]
printfn "%d" <| Solve [|-5; -3; -2; 0; 1; -1; -1; 6; -5; -3; -2; 0; 1; -1; -1; 6; -5; -3; -2; 0; 1; -1; -1; 6; -5; -3; -2; 0; 1; -1; -1; 6; -5; -3; -2; 0; 1; -1; -1; 6; -5; -3; -2; 0; 1; -1; -1; 6;|]
结果:
1000
6
540个
2147376354
最后一个很可能是由于溢出造成的错误,我只是想表明一个相对较大的测试运行速度太快而无法指数化。
答案 0 :(得分:4)
这是一个建议的解决方案:
def max_result(a_):
memo = {}
a = list(a_)
a.insert(0, 0)
return min_and_max(a, 0, len(a)-1, memo)[1]
def min_and_max(a, i, j, memo):
if (i, j) in memo:
return memo[i, j]
if i == j:
return (a[i], a[i])
min_val = max_val = None
for k in range(i, j):
left = min_and_max(a, i, k, memo)
right = min_and_max(a, k+1, j, memo)
for op in "*-+":
for x in left:
for y in right:
val = apply(x, y, op)
if min_val == None or val < min_val: min_val = val
if max_val == None or val > max_val: max_val = val
ret = (min_val, max_val)
memo[i, j] = ret
return ret
def apply(x, y, op):
if op == '*': return x*y
if op == '+': return x+y
return x-y
max_result是主函数,min_and_max是辅助函数。后者返回可通过子序列a [i..j]实现的最小和最大结果。
假设序列的最大和最小结果由子序列的最大和最小结果组成。在这个假设下,问题具有最优的子结构,可以通过动态编程(或记忆)来解决。运行时间为O(n ^ 3)。
我没有证明是正确的,但我已经用蛮力解决方案验证了它的输出,其中包含数千个随机生成的小输入。
它通过在序列的开头插入零来处理前导一元减去的可能性。
修改强>
更多地考虑这个问题,我相信它可以简化为一个更简单的问题,其中所有值都是(严格地)正的,只允许运算符*和+。
只需从序列中删除所有零,并用负数替换它们的绝对值。
此外,如果结果序列中没有,则结果只是所有数字的乘积。
减少之后,简单的动态编程算法就可以了。
编辑2
基于之前的见解,我认为我找到了线性解决方案:
def reduce(a):
return filter(lambda x: x > 0, map(abs, a))
def max_result(a):
b = reduce(a)
if len(b) == 0: return 0
return max_result_aux(b)
def max_result_aux(b):
best = [1] * (len(b) + 1)
for i in range(len(b)):
j = i
sum = 0
while j >= 0 and i-j <= 2:
sum += b[j]
best[i+1] = max(best[i+1], best[j] * sum)
j -= 1
return best[len(b)]
best [i]是子序列b [0 ..(i-1)]可以达到的最大结果。
编辑3
这是基于以下假设支持O(n)
算法的论据:
您可以使用表单
的表达式始终获得最大结果 +/- (a_1 +/- ... +/- a_i) * ... * (a_j +/- ... +/- a_n)
那是:由代数和项之和组成的因子的乘积(包括只有一个因子的情况)。
我还将使用以下易于证明的引理:
引理1:x*y >= x+y
适用于所有x,y
,x,y >= 2
引理2:abs(x_1) + ... + abs(x_n) >= abs(x_1 +/- ... +/- x_n)
在这里。
每个因素的符号无关紧要,因为您始终可以使用前导一元减号来使产品为正。因此,为了最大化产品,我们需要最大化每个因素的绝对值。
抛开所有数字为零的普通情况,在最优解决方案中,没有因子将仅由零组成。因此,由于零在每个术语总和中没有影响,并且每个因子将至少有一个非零数字,我们可以删除所有零。从现在开始,我们假设没有零。
让我们分别集中在每个术语的总和中:
(x_1 +/- x_2 +/- ... +/- x_n)
通过引理2 ,每个因子可以达到的最大绝对值是每个项的绝对值之和。这可以通过以下方式实现:
如果x_1为正数,请添加所有正项并减去所有负项。如果x_1为负数,则减去所有正数项并添加所有否定项。
这意味着每个术语的符号无关紧要,我们可以考虑每个数字的绝对值,只使用operator + inside因子。从现在开始,让我们考虑所有数字都是正数。
导致O(n)
算法的关键步骤是证明使用最多3个项的因子总能达到最大结果。
假设我们有一个超过3个项的因子,通过引理1 我们可以将它分成两个或更多个项的两个较小的因子(因此,每个加起来为2或更多),不减少总结果。我们可以反复分解,直到没有超过3个术语的因素。
这完成了论证。我还没有找到最初假设的完整理由。但是我用数百万随机生成的案例测试了我的代码,并且无法破解它。
答案 1 :(得分:3)
在O(N)中可以找到合理的大值。认为这是一种贪婪的算法。
时间的复杂性是:
1。 O(N),2. O(N),3。O(N),4. O(N),5. O(1),6. O(N),7. O(N),8。 (N),9。O(1),10.O(1),11.O(1),12.O(1),13.O(1),
所以整个算法在O(N)时间运行。
示例会话:
-3 -4 5
5×-3×-4 = 60
-5 -3 -2 0 1 -1 -1 6
6×-5×-3×-2×-1 + 1 - (-1)+ 0 = 182
2 -2 -1
2 - (-1) - (-2)= 5.
答案 2 :(得分:2)
您应该可以通过动态编程来完成此操作。让x_i
成为您的输入数字。然后,让M(a,b)
成为子序列x_a
到x_b
的最大值。然后,您可以计算:
M(a,a) = x_a
M(a,b) = max_i(max(M(a,i)*M(i+1,b), M(a,i)+M(i+1,b), M(a,i)-M(i+1,b))
编辑:
我认为您需要使用每个子序列计算最大和最小可计算值。所以
Max(a,a) = Min(a,a) = x_a
Max(a,b) = max_i(max(Max(a,i)*Max(i+1,b),
Max(a,i)*Min(i+1,b),
Min(a,i)*Max(i+1,b),
Min(a,i)*Min(i+1,b),
Max(a,i)+Max(i+1,b),
Max(a,i)-Min(i+1,b))
...similarly for Min(a,b)...
答案 3 :(得分:1)
反向抛光工作 - 这样你就不必处理括号。接下来把 - 在每个数字前面(从而使其成为正数)。最后将它们全部加在一起。不确定复杂性,可能是O(N)
。
编辑:忘记了0.如果它出现在您的输入集中,请将其添加到结果中。
答案 4 :(得分:0)
虽然我还没有想出如何减少,但这让NP感觉完全。如果我是对的,那么我可以说
O(n log n)
了,但大多数计算机科学家都怀疑它没有。O(n)
或O(n log n)
算法来估算最大值?”答案 5 :(得分:0)
这是我关于stackoverflow的第一篇文章,所以我提前道歉,因为错过任何初步礼仪。此外,为了充分披露,戴夫将此问题提请我注意。
这是一个O(N^2logN)
解决方案,主要是因为for循环中重复的排序步骤。
绝对值:删除零元素并按绝对值排序。由于您被允许在最终结果前面放置一个负号,因此您的答案是否为正或负是无关紧要的。只有集合中所有数字的绝对值才重要。
仅针对数字的乘法&gt; 1:我们观察到,对于任何大于1的正整数集(例如{2,3,4}
),最大的结果来自乘法。这可以通过枚举技术或允许操作+和 - 的矛盾论证来显示。例如(2+3)*4 = 2*4 + 3*4 < 3*4 + 3*4 = 2*(3*4)
。换句话说,乘法是最“强大”的操作(1s除外)。
将1添加到最小的非1数字:对于1s,由于乘法是无用的操作,我们最好添加。这里我们再次展示了添加结果的完整排序。为了修辞,请再次考虑集合{2,3,4}
。我们注意到:2*3*(4+1) <= 2*(3+1)*4 <= (2+1)*3*4
。换句话说,我们通过将它添加到集合中最小的现有非1元素,从1获得最多“里程”。给定排序集,可以在O(N^2logN)
。
这是伪代码的样子:
S = input set of integers;
S.absolute();
S.sort();
//delete all the 0 elements
S.removeZeros();
//remove all 1 elements from the sorted list, and store them
ones = S.removeOnes();
//now S contains only integers > 1, in ascending order S[0] ... S[end]
for each 1 in ones:
S[0] = S[0] + 1;
S.sort();
end
max_result = Product(S);
答案 6 :(得分:0)
我知道我迟到了,但我接受了这个对自己的挑战。这是我提出的解决方案。
type Operation =
| Add
| Sub
| Mult
type 'a Expr =
| Op of 'a Expr * Operation * 'a Expr
| Value of 'a
let rec eval = function
| Op (a, Add, b) -> (eval a) + (eval b)
| Op (a, Sub, b) -> (eval a) - (eval b)
| Op (a, Mult, b) -> (eval a) * (eval b)
| Value x -> x
let rec toString : int Expr -> string = function
| Op (a, Add, b) -> (toString a) + " + " + (toString b)
| Op (a, Sub, b) -> (toString a) + " - " + (toString b)
| Op (a, Mult, b) -> (toString a) + " * " + (toString b)
| Value x -> string x
let appendExpr (a:'a Expr) (o:Operation) (v:'a) =
match o, a with
| Mult, Op(x, o2, y) -> Op(x, o2, Op(y, o, Value v))
| _ -> Op(a, o, Value v)
let genExprs (xs:'a list) : 'a Expr seq =
let rec permute xs e =
match xs with
| x::xs ->
[Add; Sub; Mult]
|> Seq.map (fun o -> appendExpr e o x)
|> Seq.map (permute xs)
|> Seq.concat
| [] -> seq [e]
match xs with
| x::xs -> permute xs (Value x)
| [] -> Seq.empty
let findBest xs =
let best,result =
genExprs xs
|> Seq.map (fun e -> e,eval e)
|> Seq.maxBy snd
toString best + " = " + string result
findBest [-3; -4; 5]
返回"-3 * -4 * 5 = 60"
findBest [0; 10; -4; 0; 52; -2; -40]
返回"0 - 10 * -4 + 0 + 52 * -2 * -40 = 4200"
它应该适用于任何支持比较的类型和基本的数学运算符,但FSI会将其限制为整数。