Randomly assign a pair to each item in a list without repetitions

Viewed 585

So I have a list and I would like to "assign" the values to a different random value.

For eg.

list = ["dog", "cat", "rat", "bird", "monkey"]

I would like an output like

{"dog": "bird", "cat": "monkey", "rat": "dog", "bird": "rat", "monkey": "cat"}

What I would like is:

  • A value can't be assigned to itself e.g not {"cat": "cat"}
  • A value can only be assigned once e.g not {"cat": "dog", "rat": "dog"}
  • Values can't be assigned to each other e.g not {"cat": "dog", "dog", "cat"}

I tried this code:

def shuffle_recur(_list):
    final = {}
    not_done = copy.deepcopy(_list)
    for value in _list:
        without_list = not_done.copy()
        if value in without_list :
            without_list.remove(value)
        if value in final.values():
            for final_key, final_value in final.items():
                if final_value == value:
                    print(final_value, '    ', final_key)
                    if final_key in without_list :
                        without_list.remove(final_key)
        if len(without_list) < 1:
            print('less')
            return shuffle_recur(_list)
        target = random.choice(without_list)
        not_done.remove(target)
        final[value] = target
        print('{} >> {}'.format(value, target))
    return final

But it is very messy and I don't think it the the best way. What would be a better way to do this?

5 Answers

You may just build a random-ordered list of your items, then pair them as key-value

From one hand you'll take the list, in the other hand the same list rotated from on item values[1:] + [values[0]], and you zip both to pair 2-by-2 pairs, and build a dict from these pairs

values = ["dog", "cat", "rat", "bird", "monkey"]
shuffle(values)
result = dict(zip(values, values[1:] + [values[0]]))

Example

  • shuffling gives ['bird', 'dog', 'rat', 'monkey', 'cat']

  • rotating gives ['dog', 'rat', 'monkey', 'cat', 'bird']

  • zipping gives [('bird', 'dog'), ('dog', 'rat'), ('rat', 'monkey'), ('monkey', 'cat'), ('cat', 'bird')]

  • then each pair becomes a mapping

print(values)  # ['bird', 'dog', 'rat', 'monkey', 'cat']
print(result)  # {'bird': 'dog', 'dog': 'rat', 'rat': 'monkey', 'monkey': 'cat', 'cat': 'bird'}

If you don't the mapping to be following each other, just shuffle a second time

mappings = list(zip(values, values[1:] + [values[0]]))
shuffle(mappings)
result = dict(mappings)

You can shuffle the data and then randomly generate cycle lengths to connect elements until your list is exhausted. For lists with 5 or less elements a cycle of full length needs to be created in order to satisfy all the requirements (it can't be split into 4+1 because the 1 element doesn't have a partner and it can't be split into 3+2 because the 2 elements would need to map to themselves and hence infringe requirement #3). For lists with length >= 6 we can choose sub-cycles of minimum length 3 randomly.

import random

def random_mapping(data):
    data = data.copy()
    random.shuffle(data)
    result = {}
    while len(data) >= 5:
        index = random.randint(3, len(data)-2)  # length of the (sub-)cycle
        if index == len(data)-2:  # this means a full cycle is generated
            index = len(data)
        cycle, data = data[:index], data[index:]
        result.update(zip(cycle, cycle[1:]))
        result[cycle[-1]] = cycle[0]
    return result

You can use the following:

ll = ["dog", "cat", "rat", "bird", "monkey"]

res = list(zip(ll, ll[1:] + ll[:1]))
print(dict(res)) 
# {'dog': 'cat', 'cat': 'rat', 'rat': 'bird', 'bird': 'monkey', 'monkey': 'dog'}

zip function can be used to extract pairs over the list and slicing can be used to successively pair the current element with the next one for the efficient pairing.

# used for shuffling
import random

def shuffle_recur(list1):
    # the final dictionary
    final = {}
    # needed for values as list1 are the keys
    list2 = copy.deepcopy(list1)
    random.shuffle(list2)
    # loop trough and assign values
    for i in range(len(list1)):
        final[list1[i]] = list2[i]
    return final

You can do this with simple index arithmetic:

>>> li=["dog", "cat", "rat", "bird", "monkey"]
>>> dict((li[i],li[(i+1)%len(li)]) for i in range(len(li)))
{'dog': 'cat', 'cat': 'rat', 'rat': 'bird', 'bird': 'monkey', 'monkey': 'dog'}

Or, a dict comprehension with same arithmetic:

{li[i]:li[(i+1)%len(li)] for i in range(len(li))}
# same result

You may need to make li a set to deduplicate and you can use shuffle if you want a random order.

Further explanation:

  1. dict creates a dictionary, in this case from a generator creating tuples;
  2. (li[i],li[(i+1)%len(li)]) take two elements of the list and create a tuple. The %len(li) wraps around to 0 at the end of the list so you have ('dog','cat'),('cat','rat')...
  3. The last tuple is (li[last_element], li[first_element]) because of the li[(i+1)%len(li)] arithmetic.
Related