【问题标题】:Building an expression with maximum value构建具有最大值的表达式
【发布时间】:2010-09-23 19:43:49
【问题描述】:

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

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

【问题讨论】:

  • 最初的想法 - 加法/减法似乎很少用于解决方案 - 乘法将是主要使用的运算符,+ / - 用于处理输入集中的 1(如 * 1 是没用的!),并应对您有奇数个负值的情况 - 简单的所有值相乘将导致 -ve。

标签: algorithm optimization f#


【解决方案1】:

这是一个建议的解决方案:

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 &gt;= x+y 对于所有 x,y 使得 x,y &gt;= 2

引理 2:abs(x_1) + ... + abs(x_n) &gt;= abs(x_1 +/- ... +/- x_n)

来了。

每个因子的符号无关紧要,因为您始终可以通过使用前导一元减号使乘积为正。因此,为了最大化产品,我们需要最大化每个因素的绝对值。

撇开所有数字都为零的琐碎情况不谈,在最优解中,任何因子都不会仅由零组成。因此,由于零在每个项的总和内没有影响,并且每个因子将至少有一个非零数,我们可以删除所有零。从现在开始,让我们假设没有零。

让我们分别关注每个项的总和:

(x_1 +/- x_2 +/- ... +/- x_n)

根据引理2,每个因子所能达到的最大绝对值是每个项的绝对值之和。这可以通过以下方式实现:

如果 x_1 为正,则添加所有正项并减去所有负项。如果 x_1 为负数,则减去所有正项并添加所有负项。

这意味着每个项的符号无关紧要,我们可以考虑每个数字的绝对值并且只使用运算符+内部因素。从现在开始,让我们认为所有数字都是正数。

导致O(n) 算法的关键步骤是证明使用最多具有 3 个项的因子始终可以实现最大结果。

假设我们有超过 3 个项的因子,通过 引理 1 我们可以将其分解为两个较小的因子,每个因子包含 2 个或更多项(因此,每个加起来等于 2 个或更多),在不减少总结果的情况下。我们可以反复分解,直到没有超过 3 个项的因子。

这样就完成了论证。我仍然没有找到最初假设的完整理由。但我用数百万个随机生成的案例测试了我的代码,却无法破解它。

【讨论】:

  • 你能解释一下线性解决方案背后的逻辑吗?我也看到了 Nabb 的帖子,正要问他同样的事情,但显然他删除了它。与此同时,我会尝试自己运行一些测试。
  • 关键见解是您永远不需要添加超过 3 个连续数字的组。这就是为什么内部 while 循环最多执行 3 次的原因。实际上,您只需要在至少涉及第一名时添加组。稍后我会尝试写一个更长的解释。
  • @Sheldon - 你没有考虑一元减号吗?为什么你只使用每个数字的绝对值?
  • @IVlad:因为您始终可以通过使用前导一元减号使组的乘积为正。因此,您可以通过考虑绝对值并仅使用运算符 * 和 + 来减少问题。
  • @Sheldon L. Cooper 从来没有超过 3 个数字?错误的 !见 4 1 1 1 1 4 最大值为 4*4*4 = 64,你需要将 1....相加。
【解决方案2】:

在 O(N) 中可以找到一个合理的大值。认为这是一个贪心算法。

  1. 找出所有≥2的正数。将结果存储为A。
  2. 计算所有“-1”。将结果存储为 B。
  3. 查找所有小于等于 -2 的负数。将结果存储为 C。
  4. 计数所有“1”。将结果存储为 D。
  5. 将产品初始化为1。
  6. 如果 A 不为空,则将 Product 乘以 A 的乘积。
  7. 如果 C 不为空且计数为偶数,则将 Product 乘以 C 的乘积。
  8. 如果C是奇数,取C量级最小的数(存为x),乘以Product由其余C的乘积。
  9. 如果 x 已设置且 B 非零,则将 Product × -x 与 Product - x + 1 进行比较>。
    • 如果前者严格较大,则将B减1,然后将Product乘以-x,然后删除x em>。
    • 如果后者较大,则什么也不做。
  10. 将Result设置为0。如果Product≠1,则将其添加到Result中。
  11. 将D添加到Result中,表示添加D“1”。
  12. Result加上B,代表B“-1”的减法。
  13. 如果设置了 x,则从 Result 中减去 x。

时间复杂度为:

1。 O(N), 2. O(N), 3. O(N), 4. O(N), 5. O(1), 6. O(N), 7. O(N), 8. O (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 不为空,所以 Product = 5。
  7. C 是偶数,所以 Product = 5 × -3 × -4 = 60
  8. -
  9. -
  10. 产品 ≠ 1,所以 结果 = 60。
  11. -
  12. -
  13. -

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 不为空,所以 Product = 6
  7. -
  8. C 是奇数,所以 x = -2,Product = 6 × -5 × -3 = 90。
  9. x 已设置且 B 为非零。比较 Product × -x = 180 和 Product - x + 1 = 93。由于前者较大,我们将 B 重置为 1,Product 到 180 并删除 x。
  10. 结果 = 180。
  11. 结果 = 180 + 1 = 181
  12. 结果 = 181 + 1 = 182
  13. -

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 非零。比较 Product × -x = 4 和 Product - x + 1 = 5。由于后者更大,我们什么都不做。
  10. 结果 = 2
  11. -
  12. 结果 = 2 + 1 = 3
  13. 结果 = 3 - (-2) = 5。

2 - (-1) - (-2) = 5。

【讨论】:

  • +1,这很好。但是,您的最后两个示例也可以作为反例。最后我们可以得到6:-2 * (-2 + (-1))。另外,对于-5 -3 -2 0 1 -1 -1 6,我们可以使用-5 * (-3 + -2 + 0 - 1 + -1 + -1) * 6 = -30 * -8 = 240。
  • @IVlad:哈。 ±1s的加法/减法应该在最大整数而不是最终乘积上进行。明天我会尝试改进算法。
  • 显然我们可以在第二个例子中得到540。它的工作原理是将前三个数字相乘,将所有这些数字相加,直到 6 并与前 3 相乘,然后与 6 相乘。
【解决方案3】:

您应该能够使用动态编程来做到这一点。让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)...

【讨论】:

  • 事实上这是立方的。抱歉,我错过了您要求的时间限制。
  • 我认为这行不通,因为问题没有最优的子结构。考虑输入 [-10, 10, -10],最大值为 1000,但它不是来自子序列的最大值。
  • 实际上是O(3^n),而不是O(n^3)。由于您有 n - 1 变量和每个变量的 3 个值,因此您有 n^3 - 1 可能的解决方案,这种方法会检查其中的每一个。
  • @Sheldon - 是的,但它应该可以通过添加第三个维度来修复:M(a,b,c) = maximum value obtainable with the subsequence x_a through x_b, and the last operation is c。 @Dave - 这将以与最佳矩阵乘法相同的方式实现。是立方的。 en.wikipedia.org/wiki/Matrix_chain_multiplication - 虽然我承认我不再 100% 确定它的正确性。我仍然认为有一个多项式解决方案。
  • @Sheldon,我对您可以用括号括起来的矩阵方式的增长进行了纠正。很好的收获。
【解决方案4】:

以反向波兰语工作 - 这样您就不必处理括号。接下来在每个 -ve 数字前面放一个 - (从而使其成为正数)。最后将它们全部相乘。不确定复杂性,大概是O(N)。

编辑:忘记了 0。如果它出现在您的输入集中,请将其添加到结果中。

【讨论】:

  • 请注意|x| &lt; 2 的数字。它们需要相加而不是相乘。
  • @Kenny -- 当你指出我的错误时,我只是在编辑问题。谢谢。
  • 我希望问题不允许在每个负值前面放置一个 - 否则它会变成一个微不足道的问题,而不是从一开始就看起来那么复杂。
  • 我可以看到反向抛光有助于遗传算法方法,但我看不出它对确定性方法有何帮助。此外,您不会将一个全为 1 的字符串相乘,因此在它们为正数后将它们全部相乘是行不通的。
  • @Will A - 是的。让我们假设没有一元减号,除了第一个元素。我会编辑这个。
【解决方案5】:

这对我来说感觉 NP Complete,尽管我还没有弄清楚如何进行归约。如果我是对的,那我可以说

  • 世界上没有人知道是否存在任何多项式算法,更不用说 O(n log n),但大多数计算机科学家怀疑不存在。
  • 有多种时间算法来估计答案,例如您描述的遗传算法。
  • 实际上,我认为您要问的问题是,“是否有合理有用的O(n) 或O(n log n) 算法来估计最大值?”

【讨论】:

  • 无意冒犯或任何事情,我知道您正在尝试提供帮助,但是当@Keith Randall 不久前发布多项式解决方案时,您怎么能这么说?对于这个问题,肯定存在给出准确答案的多项式算法,我只是在寻找有效的方法。
  • 嘿@IVlad,我相信您对@Keith 解决方案的运行时分析是不正确的。它是指数的,而不是多项式的。检查我在他的 cmets 中的逻辑,看看我是否犯了错误。
【解决方案6】:

这是我在 stackoverflow 上的第一篇文章,因此对于错过任何初步礼仪,我提前道歉。此外,为了充分披露,戴夫将这个问题提请我注意。

这是一个O(N^2logN) 解决方案,主要是因为for循环中重复的排序步骤。

  1. 绝对值:删除零个元素并按绝对值排序。由于您可以在最终结果前放置负号,因此您的答案是否定还是肯定都没有关系。只有集合中所有数字的绝对值才重要。

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

  3. 将 1 与最小的非 1 数相加: 对于 1,由于乘法是无用的运算,我们最好加法。在这里,我们再次展示了加法结果的完整排序。为了修辞,再次考虑集合{2,3,4}。我们注意到:2*3*(4+1) &lt;= 2*(3+1)*4 &lt;= (2+1)*3*4。换句话说,我们通过将 1 添加到集合中最小的现有非 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);

【讨论】:

    【解决方案7】:

    我知道我参加聚会迟到了,但我把这当作是对自己的挑战。这是我想出的解决方案。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-06-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多