Быстрый алгоритм для оптимизации последовательности арифметического выражения

EDIT: уточненное описание проблемы

Существует ли быстрый алгоритм, решая следующую задачу? И, также для расширенной версии этой проблемы заменяя натуральные числа на Z/(2 ^ n Z)? (эта проблема была слишком сложной, чтобы добавить больше вопросов в одном месте, IMO.)

Проблема:

Для данного набора натуральных чисел, таких как {7, 20, 17, 100}, требуется алгоритм возвращает кратчайшую последовательность дополнений, вычислений и вычислений мощности все заданные числа. Каждый элемент последовательности (правильное) уравнение соответствует следующему шаблону:

<number> = <number> <op> <number>

где < число > представляет собой число natual, <op> является одним из {+, *, ^}.

В последовательности каждый операнд <op> должен быть одним из

  • 1
  • числа, которые уже появились в левой части равенства.

Пример:

Input: {7, 20, 17, 100}
Output:
2 = 1 + 1
3 = 1 + 2
6 = 2 * 3
7 = 1 + 6
10 = 3 + 7
17 = 7 + 10
20 = 2 * 10
100 = 10 ^ 2

Я написал алгоритм обратного слежения в Haskell. он работает для небольшого ввода, например, выше, но мой реальный запрос случайно распределенных ~ 30 номеров в [0,255]. для реального запроса следующий код занимает от 2 до 10 минут на моем ПК.

(Фактический код,  очень простой тест)

Мой текущий (псевдо) код:

-- generate set of sets required to compute n.
-- operater (+) on set is set union.
requiredNumbers 0 = { {} }
requiredNumbers 1 = { {} }
requiredNumbers n =
      { {j, k} | j^k == n, j >= 2, k >= 2 }
    + { {j, k} | j*k == n, j >= 2, k >= 2 }
    + { {j, k} | j+k == n, j >= 1, k >= 1 }

-- remember the smallest set of "computed" number
bestSet := {i | 1 <= i <= largeNumber}

-- backtracking algorithm
-- from: input
-- to:   accumulator of "already computed" number
closure from to =
    if (from is empty)
        if (|bestSet| > |to|)
            bestSet := to
            return
    else if (|from| + |to| >= |bestSet|)
        -- cut branch
        return
    else
        m := min(from)
        from' := deleteMin(from)
        foreach (req in (requiredNumbers m))
            closure (from' + (req - to)) (to + {m}) 

-- recoverEquation is a function converts set of number to set of equation.
-- it can be done easily.
output = recoverEquation (closure input {})

Дополнительное примечание:

Ответы как

  • Существует не быстрый алгоритм, потому что...
  • Существует эвристический алгоритм, это...

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

Ответ # 1 можно использовать как эвристику, я думаю.

Ответ 1

Что делать, если вы работали в обратном порядке с самого большого числа в отсортированном входе, проверяя, использовать ли/как использовать меньшие числа (и числа, которые вводятся) в его конструкции?

Например, хотя это может не гарантировать кратчайшую последовательность...

input: {7, 20, 17, 100}

(100) = (20) * 5 => 
(7) = 5 + 2      => 
(17) = 10 + (7)  =>
(20) = 10 * 2    =>
10 = 5 * 2       =>
5 = 3 + 2        =>
3 = 2 + 1        =>
2 = 1 + 1

Ответ 2

Я рекомендую преобразовать его в какой-то алгоритм кратчайшего пути графика.

  • Для каждого номера вы вычисляете (и сохраняете) кратчайший путь операций. Технически достаточно одного шага: для каждого номера вы можете сохранить операцию и два операнда (влево и вправо, так как работа питания не является коммутативной), а также вес ("узлы" )
  • Сначала вы регистрируете 1 с весом нуля
  • Каждый раз, когда вы регистрируете новый номер, вы должны генерировать все вычисления с этим номером (все добавления, умножения, полномочия) со всеми уже зарегистрированными номерами. ("края" )
    • Фильтр для вычислений: результат вычисления уже зарегистрирован, вы не должны его хранить, потому что есть более простой способ добраться до этого числа
    • Сохранять только 1 операцию для коммутативных (1 + 2 = 2 + 1)
    • Предварительно очистите питание, потому что это может даже вызвать переполнение
  • Вам необходимо заказать этот список для кратчайшего пути (веса края). Вес = (вес операнда1) + (вес операнда2) + (1, что является весом операции)
    • Вы можете исключить все результирующие числа, которые больше максимального числа, которое мы должны найти (например, если мы уже нашли 100, что-то большее, что можно исключить 20) - это можно уточнить, чтобы вы могли проверить членов также операции.
  • Если вы нажмете на один из своих целевых номеров, то вы нашли кратчайший способ вычисления одного из ваших целевых номеров, вы должны перезапустить поколения:
    • Пересчитать максимальное количество целевых номеров
    • Вернитесь на пути найденного в данный момент числа, установите их вес на 0 (они будут даны с этого момента, поскольку их стоимость уже оплачена)
    • Пересчитать вес для операций в списке генерации, так как вес исходного операнда может быть изменен (это приводит к переупорядочению в конце) - здесь вы можете исключить те, где либо операнд больше, чем новый максимум
  • Если все числа попадают, поиск продолжается

Вы можете создать свое выражение, используя "обратные ссылки" (операции, левый и правый операнды) для каждого из ваших целевых номеров.

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

Теоретически этот алгоритм обрабатывает (регистрирует) каждый номер только один раз. Применение соответствующих фильтров разрезает ненужные ветки, поэтому ничто не вычисляется дважды (кроме весов элементов в очереди)