Сгенерировать уравнение с результатом, ближайшим к запрашиваемому, иметь проблемы со скоростью

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

Данные:

  • Список из 6 номеров для использования, например 4, 8, 6, 2, 15, 50.
  • Целевое значение, где 0 < значение < 1000, например 590.
  • Доступными операциями являются деление, сложение, умножение и деление.
  • Можно использовать скобки.

Создать математическое выражение, оценка которого равна или как можно ближе к целевому значению. Например, для приведенных выше чисел выражение может быть: (6 + 4) * 50 + 15 * (8 - 2) = 590

Мой алгоритм выглядит следующим образом:

  • Сгенерировать все перестановки всех подмножеств заданных чисел из (1) выше
  • Для каждой перестановки генерируются все скобки и комбинации операторов
  • Отслеживать ближайшее значение по мере запуска алгоритма

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

Код, написанный сегодня для решения этой проблемы (соответствующий материал, извлеченный из проекта):

from operator import add, sub, mul, div
import itertools


ops = ['+', '-', '/', '*']
op_map = {'+': add, '-': sub, '/': div, '*': mul}

# iterate over 1 permutation and generates parentheses and operator combinations
def iter_combinations(seq):
    if len(seq) == 1:
        yield seq[0], str(seq[0])
    else:
        for i in range(len(seq)):
            left, right = seq[:i], seq[i:]  # split input list at i`th place
            # generate cartesian product
            for l, l_str in iter_combinations(left):
                for r, r_str in iter_combinations(right):
                    for op in ops:
                        if op_map[op] is div and r == 0:  # cant divide by zero
                            continue
                        else:
                            yield op_map[op](float(l), r), \
                                  ('(' + l_str + op + r_str + ')')

numbers = [4, 8, 6, 2, 15, 50]
target = best_value = 590
best_item = None

for i in range(len(numbers)):
    for current in itertools.permutations(numbers, i+1): # generate perms
        for value, item in iter_combinations(list(current)):
            if value < 0:
                continue

            if abs(target - value) < best_value:
                best_value = abs(target - value)
                best_item = item

print best_item

Он печатает: ((((4 * 6) +50) * 8) -2). Протестировал его немного с разными значениями и, похоже, работает правильно. Также у меня есть функция для удаления ненужных круглых скобок, но это не относится к вопросу, поэтому он не отправлен.

Проблема в том, что это происходит очень медленно из-за всех этих перестановок, комбинаций и оценок. На моем mac book air это работает несколько минут для примера. Я хотел бы запустить его за несколько секунд на одной машине, потому что многие экземпляры викторины будут запускаться одновременно на сервере. Итак, вопросы:

  • Можно ли каким-то образом (по порядку) ускорить текущий алгоритм?
  • Я пропустил какой-то другой алгоритм для этой проблемы, который будет работать намного быстрее?

Ответ 1

Вы можете построить все возможные деревья выражений с заданными числами и оценить их. Вам не нужно хранить их в памяти, просто распечатайте их, когда найденный целевой номер:

Сначала нам нужен класс для хранения выражения. Лучше спроектировать его, чтобы он был неизменным, поэтому его значение можно предварительно вычислить. Что-то вроде этого:

class Expr:
    '''An Expr can be built with two different calls:
           -Expr(number) to build a literal expression
           -Expr(a, op, b) to build a complex expression. 
            There a and b will be of type Expr,
            and op will be one of ('+','-', '*', '/').
    '''
    def __init__(self, *args):
        if len(args) == 1:
            self.left = self.right = self.op = None
            self.value = args[0]
        else:
            self.left = args[0]
            self.right = args[2]
            self.op = args[1]
            if self.op == '+':
                self.value = self.left.value + self.right.value
            elif self.op == '-':
                self.value = self.left.value - self.right.value
            elif self.op == '*':
                self.value = self.left.value * self.right.value
            elif self.op == '/':
                self.value = self.left.value // self.right.value

    def __str__(self):
        '''It can be done smarter not to print redundant parentheses,
           but that is out of the scope of this problem.
        '''
        if self.op:
            return "({0}{1}{2})".format(self.left, self.op, self.right)
        else:
            return "{0}".format(self.value)

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

Мы можем использовать itertools.combinations() или itertools.permutations(), разница в порядке. Некоторые из наших операций являются коммутативными, а некоторые - нет, поэтому мы можем использовать permutations() и предположить, что мы получим много очень симпатичных решений. Или мы можем использовать combinations() и вручную изменить порядок значений, когда операция не является коммутативной.

import itertools
OPS = ('+', '-', '*', '/')
def SearchTrees(current, target):
    ''' current is the current set of expressions.
        target is the target number.
    '''
    for a,b in itertools.combinations(current, 2):
        current.remove(a)
        current.remove(b)
        for o in OPS:
            # This checks whether this operation is commutative
            if o == '-' or o == '/':
                conmut = ((a,b), (b,a))
            else:
                conmut = ((a,b),)

            for aa, bb in conmut:
                # You do not specify what to do with the division.
                # I'm assuming that only integer divisions are allowed.
                if o == '/' and (bb.value == 0 or aa.value % bb.value != 0):
                    continue
                e = Expr(aa, o, bb)
                # If a solution is found, print it
                if e.value == target:
                    print(e.value, '=', e)
                current.add(e)
                # Recursive call!
                SearchTrees(current, target)
                # Do not forget to leave the set as it were before
                current.remove(e)
        # Ditto
        current.add(b)
        current.add(a)

И затем главный вызов:

NUMBERS = [4, 8, 6, 2, 15, 50]
TARGET = 590

initial = set(map(Expr, NUMBERS))
SearchTrees(initial, TARGET)

И сделано! С этими данными я получаю 719 различных решений всего за 21 секунду! Конечно, многие из них являются тривиальными вариациями одного и того же выражения.

Ответ 2

24 игра состоит из 4 чисел для цели 24, ваша игра - 6 чисел для цели x (0 < x < 1000).

Это очень похоже.

Вот быстрое решение, получите все результаты и напечатайте только одно в моем rMBP примерно в 1-3s, я думаю, что одно решение печатает в этой игре нормально:), я объясню это позже:

def mrange(mask):
    #twice faster from Evgeny Kluev
    x = 0
    while x != mask:
        x = (x - mask) & mask
        yield x 

def f( i ) :
    global s
    if s[i] :
        #get cached group
        return s[i]
    for x in mrange(i & (i - 1)) :
        #when x & i == x
        #x is a child group in group i
        #i-x is also a child group in group i
        fk = fork( f(x), f(i-x) )
        s[i] = merge( s[i], fk )
    return s[i] 

def merge( s1, s2 ) :
    if not s1 :
        return s2
    if not s2 :
        return s1
    for i in s2 :
        #print just one way quickly
        s1[i] = s2[i]
        #combine all ways, slowly
        # if i in s1 :
        #   s1[i].update(s2[i])
        # else :
        #   s1[i] = s2[i]
    return s1   

def fork( s1, s2 ) :
    d = {}
    #fork s1 s2
    for i in s1 :
        for j in s2 :
            if not i + j in d :
                d[i + j] = getExp( s1[i], s2[j], "+" )
            if not i - j in d :
                d[i - j] = getExp( s1[i], s2[j], "-" )
            if not j - i in d :
                d[j - i] = getExp( s2[j], s1[i], "-" )
            if not i * j in d :
                d[i * j] = getExp( s1[i], s2[j], "*" )
            if j != 0 and not i / j in d :
                d[i / j] = getExp( s1[i], s2[j], "/" )
            if i != 0 and not j / i in d :
                d[j / i] = getExp( s2[j], s1[i], "/" )
    return d    

def getExp( s1, s2, op ) :
    exp = {}
    for i in s1 :
        for j in s2 :
            exp['('+i+op+j+')'] = 1
            #just print one way
            break
        #just print one way
        break
    return exp  

def check( s ) :
    num = 0
    for i in xrange(target,0,-1):
        if i in s :
            if i == target :
                print numbers, target, "\nFind ", len(s[i]), 'ways'
                for exp in s[i]:
                    print exp, ' = ', i
            else :
                print numbers, target, "\nFind nearest ", i, 'in', len(s[i]), 'ways'
                for exp in s[i]:
                    print exp, ' = ', i
            break
    print '\n'  

def game( numbers, target ) :
    global s
    s = [None]*(2**len(numbers))
    for i in xrange(0,len(numbers)) :
        numbers[i] = float(numbers[i])
    n = len(numbers)
    for i in xrange(0,n) :
        s[2**i] = { numbers[i]: {str(numbers[i]):1} }   

    for i in xrange(1,2**n) :
        #we will get the f(numbers) in s[2**n-1]
        s[i] = f(i) 

    check(s[2**n-1])    



numbers = [4, 8, 6, 2, 2, 5]
s = [None]*(2**len(numbers))    

target = 590
game( numbers, target ) 

numbers = [1,2,3,4,5,6]
target = 590
game( numbers, target )

Предположим, что A - ваш список из 6 номеров.

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

Мы можем разделить A на две реальные дочерние группы: A1 и A-A1 (A1 не является пустым и не равно A), который разрешает проблему от f(A) до f(A1) и f(A-A1). Потому что мы знаем f(A) = Union( a+b, a-b, b-a, a*b, a/b(b!=0), b/a(a!=0) ), что a в A, b in A-A1.

Мы используем fork f(A) = Union( fork(A1,A-A1) ) означает такой процесс. Мы можем удалить все повторяющиеся значения в fork(), чтобы мы могли сократить диапазон и ускорить выполнение программы.

Итак, если A = [1,2,3,4,5,6], то f(A) = fork( f([1]),f([2,3,4,5,6]) ) U ... U fork( f([1,2,3]), f([4,5,6]) ) U ... U означает Union.

Мы увидим f ([2,3,4,5,6]) = fork (f ([2,3]), f ([4,5,6])) U..., f ( [3,4,5,6]) = fork (f ([3]), f ([4,5,6])) U..., f ([4,5,6]), используемые в обоих.

Итак, если мы можем кэшировать каждый f ([...]), программа может быть быстрее.

Мы можем получить 2^len(A) - 2 (A1, A-A1) в A. Мы можем использовать для этого двоичные значения.

Например: A = [1,2,3,4,5,6], A1 = [1,2,3], тогда двоичный 000111 (7) означает A1. A2 = [1,3,5], двоичный 010101 (21) означает A2. A3 = [1], то двоичный 000001 (1) означает A3...

Итак, мы получаем способ для всех групп в A, мы можем их кэшировать и сделать все быстрее!

Ответ 3

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

http://en.wikipedia.org/wiki/Abstract_syntax_tree

1) Создать дерево с N узлами
(N = количество чисел, которое у вас есть).
Я читал до того, как многие из вас имеют, их размер серьезен по мере роста N.

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

2) Теперь просто начните изменять операции
в нелистовых узлах и продолжать оценивать результат.

Но это снова отступает и слишком большая степень свободы.
Это сложная задача, которую вы делаете. Я верю, что если вы задайте вопрос так же, как и вы: "позвольте генерировать число K на выходе
такой, что | K-V | минимально "(здесь V - заданный желаемый результат,
т.е. 590 в вашем примере), то я предполагаю, что эта проблема даже NP-полная.

Кто-нибудь, пожалуйста, поправьте меня, если моя интуиция лжет мне.

Поэтому я думаю, что даже генерация всех возможных АСТ (при условии, что только 1 операция разрешено) является NP полным, поскольку их счет не является полиномиальным. Не говорить больше здесь допускается не более 1 операции, а не говорить о минимальном требовании разности (между результатом и желаемым результатом).

Ответ 4

Все комбинации для шести чисел, четырех операций и скобок - до 5 * 9! как минимум. Поэтому я думаю, что вы должны использовать алгоритм AI. Использование генетического программирования или оптимизации, кажется, путь к следующему.

В книге "Коллективный интеллект программирования" в главе 11 Развивающийся интеллект вы найдете именно то, что хотите, и многое другое Больше. В этой главе объясняется, как найти математическую функцию, объединяющую операции и числа (как вы хотите), чтобы соответствовать результату. Вы будете удивлены, насколько легко такая задача.

PD: примеры написаны с использованием Python.

Ответ 5

Это похоже на игру 24. Вы можете попробовать один из методов, используемых в задаче Rosetta Code: 24 игрового игрока.

Ответ 6

1. Быстрый полностью онлайн-алгоритм

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

Чтобы объединить два дерева выражений вместе, мы могли бы спуститься из корня одного дерева на "целевой" лист, для каждого node на этом пути инвертировать соответствующую операцию ('*' to '/', '/' to '*' or '/', '+' to '-', '-' to '+' or '-') и переместить "перевернутый" root node к другому дереву (также как root node).

Этот алгоритм быстрее и проще реализовать, когда все операции обратимы. Поэтому лучше всего использовать с делением с плавающей запятой (как в моей реализации), либо с рациональное разделение. Усечение целочисленного деления является наиболее сложным делом, поскольку он дает одинаковый результат для разных входных данных (42/25 = 1 и 25/25 также равно 1). При использовании целочисленного деления с нулевым остатком этот алгоритм дает результат почти мгновенно, когда имеется точный результат, но нуждается в некоторых модификациях для правильной работы, когда необходим приблизительный результат.

См. реализацию в Ideone.


2. Даже более быстрый подход с автономной предварительной обработкой

Как заметил @WolframH, не так много возможных комбинаций входных чисел. Только 3 * 3 * (4 9 + 4-1) = 4455, если возможны повторения. Или 3 * 3 * (4 9) = 1134 без дубликатов. Это позволяет нам предварительно обрабатывать все возможные входы в автономном режиме, сохранение результатов в компактной форме, и когда какой-то конкретный результат необходимо быстро распаковать одно из предварительно обработанных значений.

Программа предварительной обработки должна принимать массив из 6 чисел и генерировать значения для всех возможных выражения. Затем он должен отказаться от значений вне диапазона и найти ближайший результат для всех случаев где нет точного совпадения. Все это может быть выполнено алгоритмом, предложенным @Tim. Его код нуждается в минимальных модификациях. Также это самая быстрая альтернатива (пока). Поскольку предварительная обработка в автономном режиме, мы можем использовать что-то лучше, чем интерпретировать Python. Одним из вариантов является PyPy, другой - использование некоторого быстро интерпретируемого языка. Предварительная обработка все возможные входы не должны занимать больше нескольких минут.

Говоря о памяти, необходимой для хранения всех предварительно обработанных значений, единственной проблемой является полученных выражений. Если они хранятся в строковой форме, они будут занимать до 4455 * 999 * 30 байт или 120 МБ. Но каждое выражение может быть сжато. Он может быть представлен в постфиксной нотации следующим образом: arg1 arg2 + arg3 arg4 + *. Чтобы сохранить это, нам нужно 10 бит, чтобы сохранить все перестановки аргументов, 10 бит для хранения 5 операций и 8 бит, чтобы указать, как аргументы и операции чередование (6 аргументов + 5 операций - 3 предопределенные позиции: первые два всегда аргументы, последний всегда работает). 28 бит на дерево или 4 байта, что означает, что это только 20Mb для всего набора данных с дубликатами или 5Mb без них.


3. Медленный полностью онлайн-алгоритм

Есть несколько способов ускорить алгоритм в OP:

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

После усиления подхода OP с этими идеями достигается примерно 30-кратное ускорение:

from itertools import combinations

numbers = [4, 8, 6, 2, 15, 50]
target = best_value = 590
best_item = None
subsets = {}


def get_best(value, item):
    global best_value, target, best_item

    if value >= 0 and abs(target - value) < best_value:
        best_value = abs(target - value)
        best_item = item

    return value, item


def compare_one(value, op, left, right):
    item = ('(' + left + op + right + ')')
    return get_best(value, item)


def apply_one(left, right):
    yield compare_one(left[0] + right[0], '+', left[1], right[1])
    yield compare_one(left[0] * right[0], '*', left[1], right[1])
    yield compare_one(left[0] - right[0], '-', left[1], right[1])
    yield compare_one(right[0] - left[0], '-', right[1], left[1])

    if right[0] != 0 and left[0] >= right[0]:
        yield compare_one(left[0] / right[0], '/', left[1], right[1])

    if left[0] != 0 and right[0] >= left[0]:
        yield compare_one(right[0] / left[0], '/', right[1], left[1])


def memorize(seq):
    fs = frozenset(seq)

    if fs in subsets:
        for x in subsets[fs].items():
            yield x
    else:
        subsets[fs] = {}
        for value, item in try_all(seq):
            subsets[fs][value] = item
            yield value, item


def apply_all(left, right):
    for l in memorize(left):
        for r in memorize(right):
            for x in apply_one(l, r):
                yield x;


def try_all(seq):
    if len(seq) == 1:
        yield get_best(numbers[seq[0]], str(numbers[seq[0]]))

    for length in range(1, len(seq)):
        for x in combinations(seq[1:], length):
            for value, item in apply_all(list(x), list(set(seq) - set(x))):
                yield value, item


for x, y in try_all([0, 1, 2, 3, 4, 5]): pass

print best_item

Дополнительные улучшения скорости возможны, если вы добавите некоторые проблемы в проблему:

  • Если целочисленное деление возможно только тогда, когда остаток равен нулю.
  • Если все промежуточные результаты должны быть неотрицательными и/или ниже 1000.

Ответ 7

Хорошо, я не сдаюсь. Следуя строчке всех ответов на ваш вопрос, я придумал еще один алгоритм. Этот алгоритм дает решение со средним временем 3 миллисекунды.

#! -*- coding: utf-8 -*-
import copy

numbers = [4, 8, 6, 2, 15, 50]
target = 590

operations  = {
    '+': lambda x, y: x + y, 
    '-': lambda x, y: x - y,
    '*': lambda x, y: x * y,
    '/': lambda x, y: y == 0 and 1e30 or x / y   # Handle zero division
}

def chain_op(target, numbers, result=None, expression=""):
    if len(numbers) == 0:
        return (expression, result)
    else:
        for choosen_number in numbers:
            remaining_numbers = copy.copy(numbers)
            remaining_numbers.remove(choosen_number)
            if result is None:
                return chain_op(target, remaining_numbers, choosen_number, str(choosen_number))
            else:
                incomming_results = []
                for key, op in operations.items():
                    new_result = op(result, choosen_number)
                    new_expression = "%s%s%d" % (expression, key, choosen_number)
                    incomming_results.append(chain_op(target, remaining_numbers, new_result, new_expression))
                diff = 1e30
                selected = None
                for exp_result in incomming_results:
                    exp, res = exp_result
                    if abs(res - target) < diff:
                        diff = abs(res - target)
                        selected = exp_result
                    if diff == 0:
                        break
                return selected

if __name__ == '__main__':
    print chain_op(target, numbers)

Исправление: Этот алгоритм не включает решения, содержащие скобки. Он всегда попадает в цель или ближайший результат, мой плохой. Все еще довольно быстро. Он может быть адаптирован для поддержки скобок без большой работы.

Ответ 8

На самом деле есть две вещи, которые вы можете сделать, чтобы ускорить время до миллисекунд.

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

Еще один способ - предварительный расчет. Решите 100 квитов, используйте их как встроенные в свое приложение и создайте новый "на лету", попробуйте сохранить стек викторины на уровне 100, а также попытайтесь дать пользователю только новые викторины. У меня была такая же проблема в библейских играх, и я использовал этот метод для ускорения работы. Вместо 10 секунд для вопроса он принимает миллисекунды, поскольку я создаю новый вопрос в фоновом режиме и всегда сохраняю свой стек до 100.

Ответ 9

Как насчет динамического программирования, потому что вам нужны одинаковые результаты для вычисления других параметров?