Скрипт Python для расчета добавленных комбинаций из словаря - PullRequest
4 голосов
/ 25 апреля 2010

Я пытаюсь написать скрипт, который будет принимать словарь элементов, каждый из которых содержит свойства значений от 0 до 10, и добавлять различные элементы, чтобы выбрать, какая комбинация элементов достигает желаемых итогов. Мне также нужен скрипт для этого, использующий только те элементы, которые имеют один и тот же общий «слот».

Например:

item_list = {
 'item_1': {'slot': 'top', 'prop_a': 2, 'prop_b': 0, 'prop_c': 2, 'prop_d': 1 },
 'item_2': {'slot': 'top', 'prop_a': 5, 'prop_b': 0, 'prop_c': 1, 'prop_d':-1 },
 'item_3': {'slot': 'top', 'prop_a': 2, 'prop_b': 5, 'prop_c': 2, 'prop_d':-2 },
 'item_4': {'slot': 'mid', 'prop_a': 5, 'prop_b': 5, 'prop_c':-5, 'prop_d': 0 },
 'item_5': {'slot': 'mid', 'prop_a':10, 'prop_b': 0, 'prop_c':-5, 'prop_d': 0 },
 'item_6': {'slot': 'mid', 'prop_a':-5, 'prop_b': 2, 'prop_c': 3, 'prop_d': 5 },
 'item_7': {'slot': 'bot', 'prop_a': 1, 'prop_b': 3, 'prop_c':-4, 'prop_d': 4 },
 'item_8': {'slot': 'bot', 'prop_a': 2, 'prop_b': 2, 'prop_c': 0, 'prop_d': 0 },
 'item_9': {'slot': 'bot', 'prop_a': 3, 'prop_b': 1, 'prop_c': 4, 'prop_d':-4 },
}

Затем сценарию нужно будет выбрать, какие комбинации из «item_list» диктуют, что при использовании 1 предмета на «слот» будет достигнут желаемый результат при добавлении.

Например, если желаемый результат был: 'prop_a': 3, 'prop_b': 3, 'prop_c': 8, 'prop_d': 0, скрипт выберет 'item_2', 'item_6' и ' item_9 ', наряду с любой другой комбинацией, которая сработала.

'item_2': {'slot': 'top', 'prop_a': 5, 'prop_b': 0, 'prop_c': 1, 'prop_d':-1 }
'item_6': {'slot': 'mid', 'prop_a':-5, 'prop_b': 2, 'prop_c': 3, 'prop_d': 5 }
'item_9': {'slot': 'bot', 'prop_a': 3, 'prop_b': 1, 'prop_c': 4, 'prop_d':-4 }
 'total':                 'prop_a': 3, 'prop_b': 3, 'prop_c': 8, 'prop_d': 0

Есть идеи, как этого добиться? Это не обязательно должен быть написан на python или даже полном сценарии, но мне достаточно было бы просто объяснить, как это сделать в теории. Я пытался прорабатывать циклы через каждую комбинацию, но это, кажется, очень быстро выходит из-под контроля и неуправляемо. Фактический сценарий должен будет сделать это примерно для 1000 элементов, используя 20 различных «слотов», каждый с 8 свойствами.

Спасибо за помощь!

Ответы [ 4 ]

7 голосов
/ 25 апреля 2010

Поскольку свойства могут иметь как положительные, так и отрицательные значения, и вам нужно все удовлетворительные комбинации, я считаю, что «существенная» оптимизация невозможна, то есть решение за полиномиальное время (в предположении P ! = NP ... ;-). Все решения сводятся к перечислению всех комбинаций «один на слот» и проверке окончательных результатов с минимальными возможными изменениями, которые могут сэкономить вам несколько процентов усилий здесь или там, но ничего особенного.

Если у вас есть 1000 предметов в 20 возможных слотах, скажем, одинаково распределенных примерно по 50 предметам на слот, то в целом существует около 50**20 возможностей, т. Е. 9536743164062500000000000000000000 - около 10**34 (бесчисленные миллиарды миллиардов миллиарды ...). В общем, вы не можете "обрезать" любое поддерево из "поиска по всем решениям", потому что независимо от значения проп, когда у вас есть гипотетический выбор для первых 20-p слотов, все равно может быть выбор из оставшихся p слотов, которые могли бы удовлетворить ограничение (или более одного).

Если бы вы могли найти точное решение за полиномиальное время для этой задачи, NP-полной задачи, вы бы в корне изменили современную математику и информатику - призы Тьюринга и полевые медали были бы только началом последующих похвал. Это не очень вероятно.

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

Как только вы это сделаете, некоторые небольшие оптимизации могут иметь смысл - например, начать с суммирования констант (равных на единицу больше, чем наименьшее отрицательное значение каждого свойства) для всех значений свойств и целей, так что каждое значение проп > 0 - теперь вы можете сортировать слоты по (например) значению для какого-либо свойства или сумме всех свойств и выполнять некоторое сокращение, основываясь на знании того, что добавление еще одного слота к частичному гипотетическому решению будет увеличивать каждое совокупное значение пропа как минимум на X, а итог - как минимум на Y (так что вы можете сократить эту ветвь, если какое-либо условие приводит к тому, что промежуточные итоги превышают целевой показатель). Этот вид эвристической аппроксимации не должен вообще улучшать поведение big-O, но он может уменьшить ожидаемое значение множителя на достаточное количество, чтобы приблизить проблему к вычислительно выполнимой.

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

4 голосов
/ 26 апреля 2010

Эта проблема, по сути, является обобщением проблемы суммы подмножеств (которая является NP-полной, да) для нескольких измерений. Чтобы переформулировать проблему (чтобы убедиться, что мы решаем ту же проблему): у вас есть 1000 предметов, разделенных на 20 классов (которые вы называете слоты). Каждый элемент имеет целочисленное значение в [-10,10] для каждого из 8 свойств; таким образом, каждый элемент может рассматриваться как имеющий значение, которое является 8-мерным вектором. Вы хотите выбрать один элемент из каждого слота, чтобы общее значение (с добавлением этих 8-мерных векторов) было заданным вектором.

В приведенном вами примере у вас есть 4 измерения, а 9 элементов в 3 классах имеют значения (2,0,2,1), (5,0,1, -1), ... и т. Д., И вы хочу выбрать один предмет из каждого класса, чтобы составить сумму (3,3,8,0). Правильно?

перебор

Во-первых, это перебор, который перечисляет все возможности. Предполагая, что ваши 1000 предметов разделены поровну на 20 классов (то есть, у вас есть 50 в каждом), у вас есть 50 вариантов для каждого класса, что означает, что вам нужно проверить 50 20 = 9536743164062500000000000000000000 вариантов (и для к каждому из них нужно сложить 20 элементов вдоль каждой из 8 координат и проверить, чтобы время выполнения составило ∝ 50 20 & middot; 20 & middot; 8): это неосуществимо.

Динамическое программирование, одиночный выстрел

Тогда есть решение для динамического программирования, которое отличается и на практике часто работает там, где грубая сила невозможна, но в этом случае, к сожалению, также кажется невозможной. (Вы бы улучшили его в геометрической прогрессии, если бы получили более точные границы для своих «значений свойств».) Идея здесь состоит в том, чтобы отслеживать один из способов достижения каждого возможного sum . Сумма из 20 чисел из [-10,10] лежит в [-200,200], поэтому для вашего 8-мерного вектора есть «только» 400 8 = 655360000000000000000 возможных сумм. (Это небольшая часть другого пространства поиска, но это вас не утешает. Для каждого «свойства» вы также можете взять разницу между суммами [самый большой элемент в каждом классе] и [наименьший элемент в каждом классе ] заменить 400 на меньшее число.) Идея алгоритма динамического программирования заключается в следующем.

  • Пусть last [(a, b, c, d, e, f, g, h)] [k] обозначает один предмет, который можно взять из k-го класса (вместе с одним предметом из первого k -1 классы), чтобы сделать сумму точно (a, b, c, d, e, f, g, h). Тогда псевдокод:

    for k=1 to 20:
        for each item i in class k:
            for each vector v for which last[v][k-1] is not null:
                last[v + value(i)][k] = i
    

Затем, если желаемая конечная сумма равна s, вы выбираете элемент last [s] [k] из k-го класса, элемент last [s-value (i)] [k-1] из (k-1) й класс и тд. В худшем случае это занимает время & 20 - середина; 50 - середина; 400 8 - середина 8 (только свободная верхняя граница, а не жесткий анализ).

Динамическое программирование, отдельно

Так много для "идеальных" решений. Однако, если вы разрешите эвристические решения и те, которые «скорее всего, будут работать на практике», вы можете добиться большего успеха (даже для точного решения проблемы). Например, вы можете решить проблему отдельно для каждого из 8 измерений. Это еще проще реализовать, и в худшем случае это займет всего лишь время ∝ 20 & middot; 50 & middot; 400 & middot; 8 = 3200000, и вы можете сделать это довольно легко. Если вы сохраните последний [] [] как список, а не как отдельный элемент, то в конце вы получите (эффективно) список подмножеств, которые достигают заданной суммы для этой координаты (в форме продукта) «). На практике, не так много подмножеств могут составить именно ту сумму, которую вы хотите, поэтому вы можете начать с координаты, для которой количество подмножеств наименьшее, а затем попробовать каждое из этих подмножеств для остальных 7 координат. Сложность этого шага зависит от данных в задаче, но я подозреваю (или можно надеяться), что либо (1) будет очень мало наборов с равными суммами, и в этом случае это пересечение сократит количество наборов до проверьте, или (2) будет много наборов с заданной суммой, и в этом случае вы найдете один довольно рано.

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

Приближенные алгоритмы

Если вам не нужны суммы, равные точно , и вы будете принимать суммы, которые находятся в пределах определенного фактора от вашей требуемой суммы, есть известная идея, используемая для получения FPTAS (полностью схема аппроксимации полиномиального времени) для задачи о сумме подмножеств, которая выполняется во временном полиноме по (количеству элементов и т. д.) и 1 / ε. Я исчерпал свое время, чтобы объяснить это, но вы можете посмотреть его - в основном, он просто заменяет пространство 400 8 на меньшее, например, округление чисел до ближайшего кратного 5 или любого другого значения.

3 голосов
/ 25 апреля 2010

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

Но вы, вероятно, могли бы написать довольно простое (но более медленное) решение, используя рекурсию:

def GetItemsForSlot(item_list, slot):
  return [ (k,v) for (k,v) in item_list.items() if v['slot'] == slot]

def SubtractWeights(current_weights, item_weights):
  remaining_weights = {}
  for (k,v) in current_weights.items():
      remaining_weights[k] = current_weights[k] - item_weights[k]
  return remaining_weights

def AllWeightsAreZero(remaining_weights):
  return not [v for v in remaining_weights.values() if v != 0]

def choose_items(item_list, remaining_weights, available_slots,
                 accumulated_items=[ ]):
    print "choose_items: ", remaining_weights, available_slots, \
      accumulated_items
    # Base case: we have no more available slots.
    if not available_slots:
      if AllWeightsAreZero(remaining_weights):
          # This is a solution.
          print "SOLUTION FOUND: ", accumulated_items
          return
      else:
          # This had remaining weight, not a solution.
          return

    # Pick the next available_slot
    slot = available_slots[0]
    # Iterate over each item for this slot, checking to see if they're in a
    # solution.
    for name, properties in GetItemsForSlot(item_list, slot):
        choose_items(item_list, # pass the items recursively
                     SubtractWeights(remaining_weights, properties),
                     available_slots[1:], # pass remaining slots
                     accumulated_items + [name]) # Add this item




if __name__ == "__main__":
    total_weights = {
       'prop_a': 3,
       'prop_b': 3,
       'prop_c': 8,
       'prop_d': 0
    }

    choose_items(item_list, total_weights, ["top", "mid", "bot"])

Это было проверено и, похоже, работает. Но никаких обещаний:)

Сохранение slot & prop_a как свойств одного и того же объекта сделало его немного сложнее в работе. Я бы предложил использовать классы вместо словаря, чтобы облегчить понимание кода.

1 голос
/ 25 апреля 2010

Я пытался пройтись по каждой комбинации, но это, кажется, очень быстро берет нас в руки и неуправляемо. Фактический сценарий должен будет сделать это примерно для 1000 элементов, используя 20 различных «слотов», каждый с 8 свойствами.

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

Пример:

class Items(dict):
    def find(self, **clauses):
        # TODO!

class Slots(dict):
    # TODO!

items = Items()
for item, slots in item_list.items():
    items[item] = Slots(slots)
    # consider abstracting out slot based on location (top, mid, bot) too

print items.find(prop_a=3, prop_b=3, prop_c=8, prop_d=0)
...