Пары элементов из двух списков на условии без повторного использования элементов


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

Пример:

# inputs
list1 = [{'amount': 124, 'name': 'john'},
         {'amount': 456, 'name': 'jack'},
         {'amount': 456, 'name': 'jill'},
         {'amount': 666, 'name': 'manuel'}]
list2 = [{'amount': 124, 'color': 'red'},
         {'amount': 456, 'color': 'yellow'},
         {'amount': 456, 'color': 'on fire'},
         {'amount': 666, 'color': 'purple'}]
keyfunc = lambda e: e['amount']

# expected result
[({'amount': 124, 'name': 'john'}, {'amount': 124, 'color': 'red'}),
 ({'amount': 456, 'name': 'jack'}, {'amount': 456, 'color': 'yellow'}),
 ({'amount': 456, 'name': 'jill'}, {'amount': 456, 'color': 'on fire'}),
 ({'amount': 666, 'name': 'manuel'}, {'amount': 666, 'color': 'purple'})]

Я написал в Python, но это кажется неуклюжим, непонятным и неэффективным:

def match(al, bl, key):
    bl = list(bl)
    for a in al:
        found = False
        for i, b in enumerate(bl):
            if key(a) == key(b):
                found = True
                yield (a, b)
                del bl[i]
                break
        if not found:
            raise Exception("Not found: %s" % a)

result = list(match(list1, list2, key=keyfunc)


6581
8
задан 13 февраля 2018 в 09:02 Источник Поделиться
Комментарии
3 ответа

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

from collections import defaultdict

def match(al, bl, key):
table = defaultdict(list)
for b in bl:
table[key(b)].append(b)
return [(a, table[key(a)].pop(0)) for a in al]

Функция будет вызвана IndexError исключение в том случае, если ключ не ссылаться на соответствующий элемент в bl.

2
ответ дан 13 февраля 2018 в 07:02 Источник Поделиться

Теория

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

enter image description here

Вы заинтересованы в поиске максимального соответствия , и если он идеально подходит.

NetworkX - это большая библиотека Python для графики, и maximum_matching функция уже реализована. Он использует Хопкрофт-Карп алгоритм и работает в \$О(N^{2.5})\$ где \$п$ - число узлов.

Вы только для предварительной обработки списков в диаграммы и пусть networkx делать свою работу.

Код

Вот слегка измененная версия предыдущего ответа на StackOverflow:

import networkx as nx
import matplotlib.pyplot as plt

def has_a_perfect_match(list1, list2):
if len(list1) != len(list2):
return False

g = nx.Graph()

l = [('l', d['name'], d['amount']) for d in list1]
r = [('r', d['color'], d['amount']) for d in list2]

g.add_nodes_from(l, bipartite=0)
g.add_nodes_from(r, bipartite=1)

edges = [(a,b) for a in l for b in r if a[2] == b[2]]
g.add_edges_from(edges)

pos = {}
pos.update((node, (1, index)) for index, node in enumerate(l))
pos.update((node, (2, index)) for index, node in enumerate(r))

m = nx.bipartite.maximum_matching(g, l)
colors = ['blue' if m.get(a) == b else 'gray' for a,b in edges]

nx.draw_networkx(g,
pos=pos,
arrows=False,
labels = {n:"%s\n%d" % (n[1], n[2]) for n in g.nodes()},
edge_color=colors)
plt.axis('off')
plt.show()

return len(m) // 2 == len(list1)

В качестве бонуса, он отображает диаграмму с графиком и максимальное соответствие:

list1 = [{'amount': 124, 'name': 'john'},
{'amount': 456, 'name': 'jack'},
{'amount': 456, 'name': 'jill'},
{'amount': 666, 'name': 'manuel'}]
list2 = [{'amount': 124, 'color': 'red'},
{'amount': 456, 'color': 'yellow'},
{'amount': 456, 'color': 'on fire'},
{'amount': 666, 'color': 'purple'}]

print(has_a_perfect_match(list1, list2))
# True

enter image description here

list1 = [{'amount': 124, 'name': 'john'},
{'amount': 456, 'name': 'jack'},
{'amount': 457, 'name': 'jill'},
{'amount': 666, 'name': 'manuel'}]
list2 = [{'amount': 124, 'color': 'red'},
{'amount': 458, 'color': 'yellow'},
{'amount': 456, 'color': 'on fire'},
{'amount': 666, 'color': 'purple'}]

print(has_a_perfect_match(list1, list2))
# False

enter image description here

Примечания

Нужные согласования в m и имеет немного другой формат, чем то, что вы упомянули:

{('l', 'jack', 456): ('r', 'yellow', 456), ('l', 'jill', 456): ('r', 'on fire', 456), ('l', 'john', 124): ('r', 'red', 124), ('l', 'manuel', 666): ('r', 'purple', 666), ('r', 'red', 124): ('l', 'john', 124), ('r', 'yellow', 456): ('l', 'jack', 456), ('r', 'purple', 666): ('l', 'manuel', 666), ('r', 'on fire', 456): ('l', 'jill', 456)}

Хотя, это действительно достаточно информации.

Обратите внимание, что пограничным поколения не является оптимальным (Это \$О(N^{2})\$ и может быть \$О(П)\$ с dicts), но это лаконичные и еще быстрее, чем соответствующий алгоритм. Не стесняйтесь, чтобы изменить его!

Оптимизация

@Peilonrayz' ответ имеет лучшую производительность, потому что ваши проблемы-это легче, чем в общей комбинационной проблемы : нет соединения между узлами с различными идентификаторами, так жадный алгоритм работает отлично.

На самом деле, это можно проверить в 2 линии, если списки совпадают. С Counterвы просто должны проверить, если распределение (напр. Counter({124: 1, 456: 2, 666: 1})) является одинаковым для обоих списков:

from collections import Counter
Counter(map(key, list1)) == Counter(map(key, list2))
# True

12
ответ дан 13 февраля 2018 в 12:02 Источник Поделиться

Я думаю, вы должны разделить ваш код в две функции.


  1. Вы должны добавить некоторый код, чтобы группировать списки объектов вместе. Я бы назвал это group_by.

  2. Возьмите первую из каждого матча, и ошибка в match функция.

Для выполнения первой функции, я использую collections.defaultdictТак что вы только генерировать ключи, которые вам нужны.
Это также имеет преимущество наличия \$о(КН)\$ производительность, нежели \$o(п^к)\$.
Где \n $\$ количество объектов в каждом списке, и $\к\$ количество списков.

После этого вы можете проверить, если есть достаточно элементов, чтобы правильно доходности. Это две проверки, 1, что есть элементы в первой группе. Тогда вы убедитесь, что есть такое же количество элементов в обеих группах. После этого, вы можете использовать zip чтобы сгруппировать элементы в парах.

import collections

def group_by(lists, key):
amount = range(len(lists))
d = collections.defaultdict(lambda:tuple([[] for _ in amount]))
for i, objects in enumerate(lists):
for obj in objects:
d[key(obj)][i].append(obj)
return d

def match(al, bl, key):
for key, groups in group_by((al, bl), key).items():
if groups[0] and len(groups[0]) != len(groups[1]):
raise ValueError("Missing value for {!r}".format(key))
yield from zip(*groups)

Тип исключения, который должен быть поднят спорно, однако оно не должно быть Exception.


  • Если вы считаете, что ошибки в основном из-за отсутствия ценностей, то, используя ValueError наверное, лучше всего.

  • Если вы считаете, что ошибка происходит потому, что она требует наличия общей ключа, затем KeyError или LookupError может быть лучше.

  • Поочередно, если вы планируете на всю рационального библиотека алгебра питона, создавать свои собственные исключения.

8
ответ дан 13 февраля 2018 в 10:02 Источник Поделиться