I'm trying to create an algorithm that will match one set of objects with another. Each of the objects from one group can be matched with some of the objects from another group. Once an item has been matched, it cannot be matched to another item. My current implementation iterates over each of the objects and attempts to find a solution where each object has a match. I've done this recursively, but this method quickly becomes unusable as the processing time increases exponentially. Here is a trivial example of what I've written:
def demo_match():
possible_matches = {
"x": {1, 2},
"y": {2, 3},
"z": {1, 2}
}
used_numbers = set()
matched = {}
def match_items(remaining_items_to_match):
for letter in remaining_items_to_match:
possible_numbers = possible_matches[letter]
available_numbers = possible_numbers.difference(used_numbers)
print(f"can match '{letter}' with {available_numbers}")
if not available_numbers:
raise ValueError(f"there are no possible matches for {letter}")
for possible_match in available_numbers:
try:
used_numbers.add(possible_match)
matched[letter] = possible_match
print(f"'{letter}' has been matched with {possible_match}")
if len(remaining_items_to_match) is 1:
return matched
# remove this item and process the remaining
next_remaining_items = remaining_items_to_match[1:]
return match_items(next_remaining_items)
except ValueError:
print(f"***** found a dead end, unmatching '{letter}' and {possible_match} *****")
used_numbers.remove(possible_match)
del matched[letter]
raise ValueError("ran out of available numbers to match")
print(f"\nMatch Solution: {match_items(list(possible_matches.keys()))}")
The actual implementation assigns weights to the possible matches, meaning that matching 1 item would be preferred, but if it results in a dead end it tries the next match. I feel like there is a better implementation of this out there, maybe involving sets to find where items overlap and determining the critical path. I just haven't been able to come up with a decent alternative other than this exhaustive search.
------- Update ------
After researching the algorithms suggested in the answers I decided to go with the munkres algorithm. It allows weighting of items in the vertex.
It accepts a matrix of items, where the rows represent 1 item, the columns another, and where they intersect is the cost when those items are paired. The common example uses people and jobs as the items. This algorithm will find the pair of items with the least cost. To prevent an item from being paired, set the cost in the matrix to float('inf')
eg:
Janitor Teacher Doctor
Bob 9 5 1
Dick 1 4 6
Sally 5 3 7
would be represented by:
[
[9,5,1],
[1,4,6],
[5,3,7]
]
and would return [0,1,2], [2,0,1] where the first list represents the row indices and the second list represents the index of columns that would be matched with the rows I ended up converting the two lists into a dictionary of people to their corresponding job via
>>> def match():
from munkres import Munkres
people = ["bob", "dick", "sally"]
jobs = ["janitor", "teacher", "doctor"]
matrix = [
[9,5,1],
[1,4,6],
[5,3,7]
]
m = Munkres()
indexes = m.compute(matrix)
matches = {}
for p, j in indexes:
person = people[p]
job = jobs[j]
matches[person] = job
return matches
>>> match()
{'bob': 'doctor', 'dick': 'janitor', 'sally': 'teacher'}
matches
will be a dictionary of people to their matched job