Построение выражения с максимальным значением - PullRequest
4 голосов
/ 23 сентября 2010

Учитывая n целых чисел, существует ли алгоритм O(n) или O(n log n), который может вычислить максимальное значение математического выражения, которое можно получить, вставив операторы -, +, * и круглые скобки между указанными числами? Допустим только двоичные варианты операторов, поэтому унарный минус отсутствует, кроме, если необходимо, первого элемента.

Например, учитывая -3 -4 5, мы можем построить выражение (-3) * (-4) * 5, значение которого 60 и максимально возможное.

Справка:

Я наткнулся на эту проблему некоторое время назад при изучении генетических алгоритмов и узнал, что ее можно решить довольно просто с помощью классического генетического алгоритма. Однако это происходит медленно и теоретически просто, поскольку на практике код становится довольно уродливым (оцените выражение, проверьте правильность размещения скобок и т. Д.). Более того, мы также не можем найти абсолютный максимум.

Все эти недостатки генетических алгоритмов заставили меня задуматься: поскольку нам не нужно беспокоиться о делении, есть ли способ сделать это эффективно с помощью более классического подхода, такого как динамическое программирование или жадная стратегия?

Обновление:

Вот программа на F #, которая реализует решение DP, предложенное @Keith Randall, вместе с моим улучшением, которое я написал в комментарии к его сообщению. Это очень неэффективно, но я утверждаю, что это полиномиально и имеет кубическую сложность. Он запускается за несколько секунд для ~ 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 ]

4 голосов
/ 24 сентября 2010

Вот предлагаемое решение:

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

Я не доказал правильность, но я проверил его результаты по решению грубой силы с тысячами небольших случайно сгенерированных входных данных.

Он обрабатывает возможность начального унарного минуса, вставляя ноль в начале последовательности.

EDIT

Я немного больше думал об этой проблеме, и я считаю, что ее можно свести к более простой задаче, в которой все значения (строго) положительны и допускаются только операторы * и +.

Просто удалите все нули из последовательности и замените отрицательные числа их абсолютными значениями.

Кроме того, если в результирующей последовательности нет ни одного, результатом будет просто произведение всех чисел.

После этого сокращения будет работать простой алгоритм динамического программирования.

РЕДАКТИРОВАТЬ 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 отрицательный, вычтите все положительные члены и добавьте все отрицательные.

Это означает, что знак каждого члена не имеет значения, мы можем рассмотреть абсолютное значение каждого числа и использовать только оператор + внутри факторов. Теперь давайте рассмотрим все числа положительные.

Важнейший шаг, который приводит к алгоритму O(n), состоит в том, чтобы доказать, что максимальный результат всегда может быть достигнут с факторами, которые имеют не более 3 членов.

Предположим, что у нас есть коэффициент более 3-х терминов, по Лемма 1 мы можем разбить его на два меньших фактора по 2 или более слагаемых каждый (следовательно, каждый добавляет до 2 или более), без уменьшение общего результата. Мы можем неоднократно разбивать его до тех пор, пока не останется больше трех членов.

Это завершает аргумент. Я до сих пор не нашел полного обоснования первоначального предположения. Но я протестировал свой код с миллионами случайно сгенерированных случаев и не смог его сломать.

3 голосов
/ 24 сентября 2010

Достаточно большое значение можно найти в 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 & times; -x с Продукт & минус; х + 1 .
    • Если первый размер значительно больше, уменьшите B на 1 и умножьте Product на - x , затем удалите x .
    • Если последний больше, ничего не делать.
  10. Установите Результат в 0. Если Продукт ≠ 1, добавьте его в Результат .
  11. Добавить D к Результат , представляющий сложение D"1" с.
  12. Добавить B к Результат , представляющий вычитание B"-1" с.
  13. Если установлено x , вычтите 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 & times; -три раза; -4 = 60
  8. -
  9. -
  10. Продукт ≠ 1, поэтому Результат = 60.
  11. -
  12. -
  13. -

5 раз; -три раза; -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 & times; -5 раз; -3 = 90.
  9. x установлено и B отлично от нуля. Сравните Продукт и время; -х = 180 и Продукт & минус; x + 1 = 93. Поскольку первый размер больше, мы сбрасываем B на 1, Product на 180 и удаляем x .
  10. Результат = 180.
  11. Результат = 180 + 1 = 181
  12. Результат = 181 + 1 = 182
  13. -

6 раз; -5 раз; -три раза; -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

2 & минус; (-1) & минус; (-2) = 5

2 голосов
/ 23 сентября 2010

Вы должны быть в состоянии сделать это с помощью динамического программирования.Пусть 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))

edit:

Я думаю, вам нужно вычислить максимальное и минимальное вычисляемое значение, используя каждую подпоследовательность.Итак

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)...
1 голос
/ 23 сентября 2010

Работайте в обратном порядке - таким образом вам не придется иметь дело с круглыми скобками. Затем поставьте перед каждым -ве число - (тем самым сделав его положительным). Наконец, умножьте их все вместе. Не уверен насчет сложности, вероятно, о O(N).

РЕДАКТИРОВАТЬ: забыл о 0. Если это происходит в вашем наборе ввода, добавьте его к результату.

0 голосов
/ 17 декабря 2010

Я знаю, что опаздываю на вечеринку, но я воспринял это как вызов самому себе. Вот решение, которое я придумал.

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 ограничит его целыми числами.

0 голосов
/ 01 октября 2010

Это мой первый пост на stackoverflow, поэтому я заранее прошу прощения за отсутствие какого-либо предварительного этикета. Кроме того, в интересах полного раскрытия информации, Дэйв обратил мое внимание на эту проблему.

Вот решение O(N^2logN), в основном из-за повторяющегося этапа сортировки в цикле for.

  1. Абсолютные значения: Удалить нулевые элементы и отсортировать по абсолютному значению. Поскольку вам разрешено ставить отрицательный знак перед вашим окончательным результатом, не имеет значения, является ли ваш ответ отрицательным или положительным. Только абсолютные значения всех чисел в наборе имеют значение.

  2. Умножение только для чисел> 1: Мы отмечаем, что для любого набора натуральных чисел больше 1 (например, {2,3,4}) самый большой результат получается при умножении. Это может быть показано перечислительным методом или аргументом противоречия над разрешенными операциями + и -. например (2+3)*4 = 2*4 + 3*4 < 3*4 + 3*4 = 2*(3*4). Другими словами, умножение является наиболее «мощной» операцией (кроме 1).

  3. Добавление 1 к наименьшим не-1 числам: Для 1, поскольку умножение является бесполезной операцией, лучше добавлять. Здесь снова мы показываем полное упорядочение по результату сложения. Для риторики рассмотрим снова набор {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);
0 голосов
/ 24 сентября 2010

Мне кажется, что NP Complete, хотя я еще не понял, как сделать сокращение.Если я прав, то могу сказать:

  • Никто в мире не знает, существует ли какой-либо полиномиальный алгоритм, не говоря уже о O(n log n), но большинство компьютерных ученых подозревают, что его нет.
  • Существуют многовременные алгоритмы для оценки ответа, такие как генетический алгоритм, который вы описываете.
  • На самом деле, я думаю, что вопрос, который вы хотите задать, таков: "Есть ли достаточно разумный O(n) или O(n log n) алгоритм оценки максимального значения? "
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...