Search code examples
pythoncode-golfshuffle

oneliner scramble program


It's that time of year again that programmers want to shuffle a list such that no element resides on its original position (at least in the Netherlands, we celebrate Sinterklaas and pick straws for deciding who writes who a poem). Does anyone have a nice Python single statement for that?

So, input example: range(10)

Output example: [2,8,4,1,3,7,5,9,6,0]

Wrong output would be [2,8,4,1,3,5,7,9,6,0] because the 5 is at its original position. This would mean that person 5 must write a poem to himself and that is less fun.

edit Many people repeat the assignment just as long as needed to get lucky and find that in fact the solution is satisfactory. This is a bad approach as in theory this can take infinitely long. The better approach is indeed suggested by Bart, but I can't get that into a oneliner for one reason or another...

edit By oneliner, I mean single statement. As it appears, Python is also able to compress multiple statements on a single line. I didn't know that. There are currently very nice solutions only using the semicolon to mimic multiline behaviour on a single line. Hence: "can you do it in a single statement?"


Solution

  • I found shuffle can be abused into solving this

    from random import shuffle
    L = ["Anne", "Beth", "Cath", "Dave", "Emma"]
    shuffle(L, int=lambda n: int(n - 1))
    print L
    

    The distribution is not uniform however this was not a requirement.

    #For 100,000 samples
    
    (('Beth', 'Cath', 'Dave', 'Emma', 'Anne'), 13417)
    (('Beth', 'Cath', 'Emma', 'Anne', 'Dave'), 6572)
    (('Beth', 'Dave', 'Anne', 'Emma', 'Cath'), 3417)
    (('Beth', 'Dave', 'Emma', 'Cath', 'Anne'), 6581)
    (('Beth', 'Emma', 'Anne', 'Cath', 'Dave'), 3364)
    (('Beth', 'Emma', 'Dave', 'Anne', 'Cath'), 6635)
    (('Cath', 'Anne', 'Dave', 'Emma', 'Beth'), 1703)
    (('Cath', 'Anne', 'Emma', 'Beth', 'Dave'), 1705)
    (('Cath', 'Dave', 'Beth', 'Emma', 'Anne'), 6583)
    (('Cath', 'Dave', 'Emma', 'Anne', 'Beth'), 3286)
    (('Cath', 'Emma', 'Beth', 'Anne', 'Dave'), 3325)
    (('Cath', 'Emma', 'Dave', 'Beth', 'Anne'), 3421)
    (('Dave', 'Anne', 'Beth', 'Emma', 'Cath'), 1653)
    (('Dave', 'Anne', 'Emma', 'Cath', 'Beth'), 1664)
    (('Dave', 'Cath', 'Anne', 'Emma', 'Beth'), 3349)
    (('Dave', 'Cath', 'Emma', 'Beth', 'Anne'), 6727)
    (('Dave', 'Emma', 'Anne', 'Beth', 'Cath'), 3319)
    (('Dave', 'Emma', 'Beth', 'Cath', 'Anne'), 3323)
    (('Emma', 'Anne', 'Beth', 'Cath', 'Dave'), 1682)
    (('Emma', 'Anne', 'Dave', 'Beth', 'Cath'), 1656)
    (('Emma', 'Cath', 'Anne', 'Beth', 'Dave'), 3276)
    (('Emma', 'Cath', 'Dave', 'Anne', 'Beth'), 6638)
    (('Emma', 'Dave', 'Anne', 'Cath', 'Beth'), 3358)
    (('Emma', 'Dave', 'Beth', 'Anne', 'Cath'), 3346)
    

    For a uniform distribution, this (longer) version can be used

    from random import shuffle,randint
    L=["Anne", "Beth", "Cath", "Dave", "Emma"]
    shuffle(L, random=lambda: 1, int=lambda n: randint(0, n - 2))
    print L
    
    # For 100,000 samples
    
    (('Beth', 'Cath', 'Dave', 'Emma', 'Anne'), 4157)
    (('Beth', 'Cath', 'Emma', 'Anne', 'Dave'), 4155)
    (('Beth', 'Dave', 'Anne', 'Emma', 'Cath'), 4099)
    (('Beth', 'Dave', 'Emma', 'Cath', 'Anne'), 4141)
    (('Beth', 'Emma', 'Anne', 'Cath', 'Dave'), 4243)
    (('Beth', 'Emma', 'Dave', 'Anne', 'Cath'), 4208)
    (('Cath', 'Anne', 'Dave', 'Emma', 'Beth'), 4219)
    (('Cath', 'Anne', 'Emma', 'Beth', 'Dave'), 4087)
    (('Cath', 'Dave', 'Beth', 'Emma', 'Anne'), 4117)
    (('Cath', 'Dave', 'Emma', 'Anne', 'Beth'), 4127)
    (('Cath', 'Emma', 'Beth', 'Anne', 'Dave'), 4198)
    (('Cath', 'Emma', 'Dave', 'Beth', 'Anne'), 4210)
    (('Dave', 'Anne', 'Beth', 'Emma', 'Cath'), 4179)
    (('Dave', 'Anne', 'Emma', 'Cath', 'Beth'), 4119)
    (('Dave', 'Cath', 'Anne', 'Emma', 'Beth'), 4143)
    (('Dave', 'Cath', 'Emma', 'Beth', 'Anne'), 4203)
    (('Dave', 'Emma', 'Anne', 'Beth', 'Cath'), 4252)
    (('Dave', 'Emma', 'Beth', 'Cath', 'Anne'), 4159)
    (('Emma', 'Anne', 'Beth', 'Cath', 'Dave'), 4193)
    (('Emma', 'Anne', 'Dave', 'Beth', 'Cath'), 4177)
    (('Emma', 'Cath', 'Anne', 'Beth', 'Dave'), 4087)
    (('Emma', 'Cath', 'Dave', 'Anne', 'Beth'), 4150)
    (('Emma', 'Dave', 'Anne', 'Cath', 'Beth'), 4268)
    (('Emma', 'Dave', 'Beth', 'Anne', 'Cath'), 4109)
    

    How it works

    Here is the code for random.shuffle()

    def shuffle(self, x, random=None, int=int):
        """x, random=random.random -> shuffle list x in place; return None.
    
        Optional arg random is a 0-argument function returning a random
        float in [0.0, 1.0); by default, the standard random.random.
        """
    
        if random is None:
            random = self.random
        for i in reversed(xrange(1, len(x))):
            # pick an element in x[:i+1] with which to exchange x[i]
            j = int(random() * (i+1))
            x[i], x[j] = x[j], x[i]
    

    Both solutions work by targeting the line j = int(random() * (i+1))

    The first(non uniform) effectively makes the line work like this

    j = int(random() * (i + 1) - 1)
    

    So instead of a range of (1..i) we obtain (0..i-1)

    The second solution replaces random() with a function that always returns 1, and uses randint instead of int. So the line now works like this

    j = randint(0, i - 1)