Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Building an expression with maximum value

Given n integers, is there an O(n) or O(n log n) algorithm that can compute the maximum value of a mathematical expression that can be obtained by inserting the operators -, +, * and parentheses between the given numbers? Assume only binary variants of the operators, so no unary minus, except before the first element if needed.

For example, given -3 -4 5, we can build the expression (-3) * (-4) * 5, whose value is 60, and maximum possible.

Background:

I stumbled upon this problem some time ago when studying genetic algorithms, and learned that it can be solved pretty simply with a classical genetic algorithm. This runs slowly however, and it's only simple in theory, as the code gets rather ugly in practice (evaluate the expression, check for correct placement of brackets etc.). What's more, we're not guaranteed to find the absolute maximum either.

All these shortcomings of genetic algorithms got me wondering: since we can don't have to worry about division, is there a way to do this efficiently with a more classic approach, such as dynamic programming or a greedy strategy?

Update:

Here's an F# program that implements the DP solution proposed by @Keith Randall together with my improvement, which I wrote in a comment to his post. This is very inefficient, but I maintain that it's polynomial and has cubic complexity. It runs in a few seconds for ~50 element arrays. It would probably be faster if written in a fully imperative manner, as a lot of time is probably wasted on building and traversing lists.

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;|]

Results:

1000
6
540
2147376354

The last one is most likely an error due to overflow, I'm just trying to show that a relatively big test runs too fast for this to be exponential.

like image 286
IVlad Avatar asked Sep 23 '10 19:09

IVlad


2 Answers

Here's a proposed solution:

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 is the main function, and min_and_max is auxiliary. The latter returns the minimum and maximum results that can be achieved by sub-sequence a[i..j].

It assumes that maximum and minimum results of sequences are composed by maximum and minimum results of sub-sequences. Under this assumption, the problem has optimal substructure and can be solved with dynamic programming (or memoization). Run time is O(n^3).

I haven't proved correctness, but I have verified its output against a brute force solution with thousands of small randomly generated inputs.

It handles the possibility of a leading unary minus by inserting a zero at the beginning of the sequence.

EDIT

Been thinking a bit more about this problem, and I believe it can be reduced to a simpler problem in which all values are (strictly) positive and only operators * and + are allowed.

Just remove all zeroes from the sequence and replace negative numbers by their absolute value.

Furthermore, if there are no ones in the resulting sequence, the result is simply the product of all numbers.

After this reduction, the simple dynamic programming algorithm would work.

EDIT 2

Based on the previous insights I think I found a linear solution:

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] is the maximum result that can be achieved by sub-sequence b[0..(i-1)].

EDIT 3

Here's an argument in favor of the O(n) algorithm based on the following assumption:

You can always achieve the maximum result with an expression of the form

+/- (a_1 +/- ... +/- a_i) * ... * (a_j +/- ... +/- a_n)

That is: a product of factors composed of an algebraic sum of terms (including the case of only one factor).

I will also use the following lemmas which are easy to prove:

Lemma 1: x*y >= x+y for all x,y such that x,y >= 2

Lemma 2: abs(x_1) + ... + abs(x_n) >= abs(x_1 +/- ... +/- x_n)

Here it goes.

The sign of each factor doesn't matter, since you can always make the product positive by using the leading unary minus. Hence, to maximize the product we need to maximize the absolute value of each factor.

Setting aside the trivial case in which all numbers are zeroes, in an optimal solution no factor will be composed only of zeroes. Therefore, since zeroes have no effect inside each sum of terms, and each factor will have at least one non-zero number, we can remove all zeroes. From now on, let's assume there are no zeroes.

Let's concentrate in each sum of terms separately:

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

By Lemma 2, the maximum absolute value each factor can achieve is the sum of the absolute values of each term. This can be achieved in the following way:

If x_1 is positive, add all positive terms and subtract all negative terms. If x_1 is negative, subtract all positive terms and add all negative terms.

This implies that the sign of each term does not matter, we can consider the absolute value of each number and only use operator + inside factors. From now on, let's consider all numbers are positive.

The crucial step, that leads to an O(n) algorithm, is to prove that the maximum result can always be achieved with factors that have at most 3 terms.

Suppose we have a factor of more than 3 terms, by Lemma 1 we can break it into two smaller factors of 2 or more terms each (hence, each add up to 2 or more), without reducing the total result. We can break it down repeatedly until no factors of more than 3 terms are left.

That completes the argument. I still haven't found a complete justification of the initial assumption. But I tested my code with millions of randomly generated cases and couldn't break it.

like image 86
Sheldon L. Cooper Avatar answered Oct 20 '22 09:10

Sheldon L. Cooper


A reasonable big value can be found in O(N). Consider this a greedy algorithm.

  1. Find all positive numbers ≥ 2. Store the result as A.
  2. Count all "-1"s . Store the result as B.
  3. Find all negative numbers ≤ -2. Store the result as C.
  4. Count all "1"s. Store the result as D.
  5. Initialize Product to 1.
  6. If A is not empty, multiply Product by the product of A.
  7. If C is not empty and has even count, multiply Product by the product of C.
  8. If C is has odd count, take the smallest number in magnitude of C away (store it as x), and multiply Product by the product of the rest of C.
  9. If x is set and B is nonzero, compare Product × -x with Product − x + 1.
    • If the former is strictly larger, decrease B by 1 and multiply Product by -x, then remove x.
    • If the latter is larger, do nothing.
  10. Set Result to 0. If Product ≠ 1, add it to Result.
  11. Add D to Result, representing addition of D "1"s.
  12. Add B to Result, representing subtraction of B "-1"s.
  13. If x is set, substract x from Result.

The time complexities are:

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),

so the whole algorithm runs in O(N) time.


An example session:

-3 -4 5
  1. A = [5]
  2. B = 0
  3. C = [-3, -4]
  4. D = 1
  5. Product = 1
  6. A is not empty, so Product = 5.
  7. C is even, so Product = 5 × -3 × -4 = 60
  8. -
  9. -
  10. Product ≠ 1, so Result = 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. Product = 1
  6. A is not empty, so Product = 6
  7. -
  8. C is odd, so x = -2, and Product = 6 × -5 × -3 = 90.
  9. x is set and B is nonzero. Compare Product × -x = 180 and Product − x + 1 = 93. Since the former is larger, we reset B to 1, Product to 180 and remove x.
  10. Result = 180.
  11. Result = 180 + 1 = 181
  12. Result = 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. Product = 1
  6. Product = 2
  7. -
  8. x = -2, Product is unchanged.
  9. B is nonzero. Compare Product × -x = 4 and Product − x + 1 = 5. Since the latter is larger, we do nothing.
  10. Result = 2
  11. -
  12. Result = 2 + 1 = 3
  13. Result = 3 − (-2) = 5.

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

like image 42
kennytm Avatar answered Oct 20 '22 09:10

kennytm