7

I am looking for a Pythonic method to generate all pairwise-unique unique pairings (where a pairing is a system consisting of pairs, and pairwise-unique indicates that (a,b) ≠ (b,a)) for a set containing even number n items.

I like the code from here:

for perm in itertools.permutations(range(n)):
    print zip(perm[::2], perm[1::2])

except that it generates all order-unique, pairwise-unique pairings, or (n/2)! times more pairings than I want (redundancy), which, although I can filter out, really bog down my program at large n.

That is, for n = 4, I am looking for the following output (12 unique pairings):

[(0, 1), (2, 3)]
[(0, 1), (3, 2)]
[(1, 0), (2, 3)]
[(1, 0), (3, 2)]
[(1, 2), (0, 3)]
[(1, 2), (3, 0)]
[(1, 3), (0, 2)]
[(2, 0), (1, 3)]
[(2, 0), (3, 1)]
[(3, 1), (0, 2)]
[(0, 3), (2, 1)]
[(3, 0), (2, 1)]

Note that (a,b) ≠ (b,a).

Is this possible? I am also okay with a function that generates the 3 non–pairwise-unique pairings for n = 4 where (a,b) = (b,a), as it is straightforward to permute what I need from there. My main goal is to avoid the superfluous permutations on the order of the pairs in the pairing.

Thanks in advance for your help and suggestions—I really appreciate it.

Community
  • 1
  • 1
nimble agar
  • 417
  • 8
  • 15
  • Please explain the "uniqueness" of pairings. Why [(0, 2), (3, 1)] isn't and [(0, 3), (2, 1)] is in your list? – Robert Lujo May 12 '13 at 21:01
  • @RobertLujo Equality of the pairs depends on the order of the paired items, but equality of the pairings does not depends on the order of the pairs within that pairing. That is, `(a,b) ≠ (b,a)` but `[(a,b),(c,d)] = [(c,d),(a,b)]`. For the specific cases that you cite, `[(0, 2), (3, 1)]` is represented by `[(3, 1), (0, 2)]`. On the other hand, `[(0, 3), (2, 1)]` is the only representation of itself. – nimble agar May 12 '13 at 21:36
  • @Arman: By those definitions of pair and pairing equality, the "12 unique pairings" you list in your question aren't the only possible 12 because many have equivalents that differ only in the order of the pairs. If that's correct, then calling them "unique" is somewhat misleading in my opinion. Those defs also doesn't explain why "`[(0, 3), (2, 1)]` is the only representation of itself", because isn't `[(2, 1), (0, 3)]` another, equally valid one? – martineau May 13 '13 at 17:32
  • @martineau By my definitions, `[(0, 3), (2, 1)]` and `[(2, 1), (0, 3)]` are equivalent by `[(a,b),(c,d)] = [(c,d),(a,b)]`. I apologize for not being very clear—the terminology is kind of hairy. Obviously there will be different levels of "unique", depending on the context of the pairs. In the simplest scenario, say pairs of students doing an activity, neither pairs nor the pairing are ordered. However, in a chess tournament for example, whether a player is assigned to white or black makes a difference. But I cannot think of any case for which the order of the pairs in the pairing matters. – nimble agar May 13 '13 at 20:40
  • @Arman: My main point was that since `[(0, 3), (2, 1)]` and `[(2, 1), (0, 3)]` _are_ equivalent by your definition, then the latter could have just as well have been included in your list of "unique pairings" instead of the former...which makes calling them "unique" -- which means being the only one of its kind -- misleading IMHO. – martineau May 14 '13 at 00:22
  • @martineau: OK, that makes sense, and I agree. I know that the lack of straightforward terminology certainly made it difficult for me to search for existing answers to my problem. Any thoughts on a better word choice in this case? – nimble agar May 14 '13 at 00:32
  • @Arman: Perhaps by adding an adjective specifying in what sense they're unique...something like "pairwise unique"? – martineau May 14 '13 at 00:49

3 Answers3

3

I think this gives you the fundamental pairings that you need: 1 when N=2; 3 when N=4; 15 when N=6; 105 when n=8, etc.

import sys

def pairings(remainder, partial = None):
    partial = partial or []

    if len(remainder) == 0:
        yield partial

    else:
        for i in xrange(1, len(remainder)):
            pair = [[remainder[0], remainder[i]]]
            r1   = remainder[1:i]
            r2   = remainder[i+1:]
            for p in pairings(r1 + r2, partial + pair):
                yield p

def main():
    n = int(sys.argv[1])
    items = list(range(n))
    for p in pairings(items):
        print p

main()
FMc
  • 41,963
  • 13
  • 79
  • 132
1

In the linked question "Generating all unique pair permutations", (here), an algorithm is given to generate a round-robin schedule for any given n. That is, each possible set of matchups/pairings for n teams.

So for n = 4 (assuming exclusive), that would be:

[0, 3], [1, 2]
[0, 2], [3, 1]
[0, 1], [2, 3]

Now we've got each of these partitions, we just need to find their permutations in order to get the full list of pairings. i.e [0, 3], [1, 2] is a member of a group of four: [0, 3], [1, 2] (itself) and [3, 0], [1, 2] and [0, 3], [2, 1] and [3, 0], [2, 1].

To get all the members of a group from one member, you take the permutation where each pair can be either flipped or not flipped (if they were, for example, n-tuples instead of pairs, then there would be n! options for each one). So because you have two pairs and options, each partition yields 2 ^ 2 pairings. So you have 12 altogether.

Code to do this, where round_robin(n) returns a list of lists of pairs. So round_robin(4) --> [[[0, 3], [1, 2]], [[0, 2], [3, 1]], [[0, 1], [2, 3]]].

def pairs(n):
    for elem in round_robin(n):
        for first in [elem[0], elem[0][::-1]]:
            for second in [elem[1], elem[1][::-1]]:
                print (first, second)

This method generates less than you want and then goes up instead of generating more than you want and getting rid of a bunch, so it should be more efficient. ([::-1] is voodoo for reversing a list immutably).

And here's the round-robin algorithm from the other posting (written by Theodros Zelleke)

from collections import deque

def round_robin_even(d, n):
    for i in range(n - 1):
        yield [[d[j], d[-j-1]] for j in range(n/2)]
        d[0], d[-1] = d[-1], d[0]
        d.rotate()

def round_robin_odd(d, n):
    for i in range(n):
        yield [[d[j], d[-j-1]] for j in range(n/2)]
        d.rotate()

def round_robin(n):
    d = deque(range(n))
    if n % 2 == 0:
        return list(round_robin_even(d, n))
    else:
        return list(round_robin_odd(d, n))
Community
  • 1
  • 1
Eli Rose
  • 6,788
  • 8
  • 35
  • 55
  • Thanks Eli—this was actually my original implementation (Zelleke's algorithm in combination with permutations on the "flip" status of each pair in a pairing), however after hitting some bugs I realized that, as far as I understand it, Zelleke's round-robin algorithm does not actually address my problem. Note that the expected number of unique pairings for even `n` when order does not matter, ever, is `n!/(2^(n/2) * (n/2)!)` (although correct me if mistaken). If you run Zelleke's code you will see that this does not hold true. – nimble agar May 12 '13 at 21:49
  • Zelleke's algorithm is designed so that a specific pair `(a,b)` appears only once. That is, you will never see `[(a,b),(c,d),(e,f)]` and `[(a,b),(c,f),(d,e)]` from his code; however, I consider these to be unique pairings, although not every pair is unique across the system. – nimble agar May 12 '13 at 21:52
0

I'm not sure if I understood the problem well, anyway here is my solution:

import itertools
n = 4
out = set()
for perm in itertools.permutations(range(n)):
    pairs = tuple(sorted(zip(perm[::2], perm[1::2])))
    out.add(pairs)

for i, p in enumerate(sorted(out)):
    print i,p

it returns 12 unique pairs for n=4, and 120 for n=6.

Robert Lujo
  • 15,383
  • 5
  • 56
  • 73
  • Thanks, Robert—this definitely addresses my problem, but still generates the redundant pairings and filters them out via the set, correct? I've been looking/hoping for a way to directly generate the exact set of pairings I want, without having to over-permute and then filter out the redundancies. – nimble agar May 12 '13 at 23:06