构建具有最大值的表达式

时间:2010-09-23 19:43:49

标签: algorithm optimization f#

给定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

最后一个很可能是由于溢出造成的错误,我只是想表明一个相对较大的测试运行速度太快而无法指数化。

7 个答案:

答案 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,yx,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. 查找所有正数≥2。将结果存储为 A
  2. 统计所有“-1”。将结果存储为 B
  3. 查找所有负数≤-2。将结果存储为 C
  4. 统计所有“1”。将结果存储为 D
  5. 产品初始化为1。
  6. 如果 A 不为空,请将产品乘以 A 的乘积。
  7. 如果 C 不为空且偶数,请将产品乘以 C 的乘积。
  8. 如果 C 具有奇数,则取 C 的最小数量(将其存储为 x ),并乘以< em>产品由 C 的其余部分的产品。
  9. 如果 x 已设置且 B 非零,则将 Product×-x Product - x + 1 。
    • 如果前者严格较大,则将 B 减少1并将 Product 乘以 - x ,然后删除 x
    • 如果后者较大,则什么也不做。
  10. 结果设置为0.如果产品≠1,请将其添加到结果
  11. D 添加到结果,表示添加 D “1”。
  12. B 添加到结果,表示减去 B “ - 1”。
  13. 如果设置了 x ,请从结果中减去 x
  14. 时间的复杂性是:

    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
    
    1. A = [5]
    2. B = 0
    3. C = [ - 3,-4]
    4. D = 1
    5. 产品 = 1
    6. A 不为空,因此产品 = 5。
    7. C 是偶数,所以产品 = 5×-3×-4 = 60
    8. -
    9. -
    10. 产品≠1,所以结果 = 60。
    11. -
    12. -
    13. -
    14.   

      5×-3×-4 = 60

      -5 -3 -2 0 1 -1 -1 6 
      
      1. A = [6]
      2. B = 2
      3. C = [ - 5,-3,-2]
      4. D = 1
      5. 产品 = 1
      6. A 不为空,因此产品 = 6
      7. -
      8. C 是奇数,所以 x = -2,产品 = 6×-5×-3 = 90。
      9. x 已设置且 B 非零。比较产品×-x = 180和产品 - x + 1 = 93.由于前者较大,我们将 B 重置为1,< em>产品为180并删除 x
      10. 结果 = 180。
      11. 结果 = 180 + 1 = 181
      12. 结果 = 181 + 1 = 182
      13. -
      14.   

        6×-5×-3×-2×-1 + 1 - (-1)+ 0 = 182

        2 -2 -1
        
        1. A = [2]
        2. B = 1
        3. C = [ - 2]
        4. D = 0
        5. 产品 = 1
        6. 产品 = 2
        7. -
        8. x = -2,产品未更改。
        9. B 非零。比较产品×-x = 4和产品 - x + 1 = 5.由于后者较大,我们什么都不做。
        10. 结果 = 2
        11. -
        12. 结果 = 2 + 1 = 3
        13. 结果 = 3 - ( - 2)= 5.
        14.   

          2 - (-1) - (-2)= 5.

答案 2 :(得分:2)

您应该可以通过动态编程来完成此操作。让x_i成为您的输入数字。然后,让M(a,b)成为子序列x_ax_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循环中重复的排序步骤。

  1. 绝对值:删除零元素并按绝对值排序。由于您被允许在最终结果前面放置一个负号,因此您的答案是否为正或负是无关紧要的。只有集合中所有数字的绝对值才重要。

  2. 仅针对数字的乘法&gt; 1:我们观察到,对于任何大于1的正整数集(例如{2,3,4}),最大的结果来自乘法。这可以通过枚举技术或允许操作+和 - 的矛盾论证来显示。例如(2+3)*4 = 2*4 + 3*4 < 3*4 + 3*4 = 2*(3*4)。换句话说,乘法是最“强大”的操作(1s除外)。

  3. 将1添加到最小的非1数字:对于1s,由于乘法是无用的操作,我们最好添加。这里我们再次展示了添加结果的完整排序。为了修辞,请再次考虑集合{2,3,4}。我们注意到:2*3*(4+1) <= 2*(3+1)*4 <= (2+1)*3*4。换句话说,我们通过将它添加到集合中最小的现有非1元素,从1获得最多“里程”。给定排序集,可以在O(N^2logN)

  4. 中完成

    这是伪代码的样子:

    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会将其限制为整数。