19

I have tried to ask this question before, but have never been able to word it correctly. I hope I have it right this time:

I have a list of unique elements. I want to shuffle this list to produce a new list. However, I would like to constrain the shuffle, such that each element's new position is at most d away from its original position in the list.

So for example:

L = [1,2,3,4]
d = 2
answer = magicFunction(L, d)

Now, one possible outcome could be:

>>> print(answer)
[3,1,2,4]

Notice that 3 has moved two indices, 1 and 2 have moved one index, and 4 has not moved at all. Thus, this is a valid shuffle, per my previous definition. The following snippet of code can be used to validate this:

old = {e:i for i,e in enumerate(L)}
new = {e:i for i,e in enumerate(answer)}
valid = all(abs(i-new[e])<=d for e,i in old.items())

Now, I could easily just generate all possible permutations of L, filter for the valid ones, and pick one at random. But that doesn't seem very elegant. Does anyone have any other ideas about how to accomplish this?

Community
  • 1
  • 1
inspectorG4dget
  • 110,290
  • 27
  • 149
  • 241
  • How is `[3, 1, 2, 4]` not valid? And what distribution over possible outputs do you want to produce? – user2357112 Jun 10 '15 at 05:26
  • 1
    @user2357112: It /is/ valid, based on what I've said in my post – inspectorG4dget Jun 10 '15 at 05:27
  • 1
    @user2357112 He said `[3,1,2,4]` is valid. – Loocid Jun 10 '15 at 05:28
  • Could've sworn that sentence had a "not" in it. What distribution do you want, then? Uniform over all possible outputs? Or should it model the results of some particular process that would motivate a different distribution? – user2357112 Jun 10 '15 at 05:29
  • I'm looking for a uniform distribution, but I would prefer an answer that takes the distribution as a parameter – inspectorG4dget Jun 10 '15 at 05:47
  • The combinatorics of this problem are pretty interesting. I have some ideas for how to use dynamic programming to count the number of possible results after each intermediate choice and weight the choices appropriately, and some ideas about how to reduce the memory required for such a process. It'd be best to come up with a closed form for how many outcomes are possible after each step, though; my current ideas make the runtime too dependent on d. – user2357112 Jun 10 '15 at 05:57
  • Even that seems better than my exhaustive search. I'd be interested to see what you come up with – inspectorG4dget Jun 10 '15 at 05:59
  • Add `algorithm` tag to your question. – f43d65 Jun 10 '15 at 06:11
  • you might want to add additional requirements to `"each element's new position is at most d away from its original position"` - wouldn't a valid solution be simply swapping a couple of adjacent elements? – גלעד ברקן Jun 10 '15 at 10:40
  • yes, that /would/ be a valid solution. I would expect that a proposed solution to this problem consider it as well – inspectorG4dget Jun 10 '15 at 10:43
  • so if simply swapping a couple of adjacent elements is all you need, what's the problem? Or do you have additional requirements? – גלעד ברקן Jun 10 '15 at 10:47
  • I don't want a deterministic shuffle. I want a random, feasible shuffle. Notice that `random.shuffle` doesn't return the same output for a given input, two different times. Similarly, I would like a potential solution to have that element of randomness about it, so that I can't necessarily predict the output, given the input – inspectorG4dget Jun 10 '15 at 10:50
  • Hint: Try to count f(n, d) the number of permutations (of n items) that meet the 'moved at most d' constraint. – Colonel Panic Jun 10 '15 at 15:48
  • @colonel: how would you count it? Would you have a formula? – inspectorG4dget Jun 10 '15 at 16:02
  • @ColonelPanic: Do you know an efficient way to compute f, particularly for large d? My efforts to find a closed form haven't gotten anywhere, and my dynamic programming method requires a table that grows exponentially with d. – user2357112 Jun 10 '15 at 21:39
  • 1
    There's a thesis about counting such permutations: http://www.ma.utexas.edu/users/olenab/olena_thesis.pdf . It doesn't seem to say much about generating them. To generate them with a uniform distribution I would use "early rejection": generate a random permutation and reject it as soon as possible during the generation process if it violates the restriction. Early rejection works very well for generating random derangements, for example. – Edward Doolittle Jun 11 '15 at 06:59
  • @EdwardDoolittle: I've been looking into rejection-based methods. Unfortunately, unlike with derangements, the rejection probability is really high; it takes a lot of attempts to generate one sample. – user2357112 Jun 11 '15 at 07:12
  • I can imagine. OK, I've continued searching the literature and turned up this interesting book: https://books.google.ca/books?id=6MR0CQAAQBAJ&pg=PA282&lpg=PA282&dq=permutation+distance+restriction&source=bl&ots=YSH9huNdNr&sig=MlaNPeUSFbOIwm_vlfLrEQXLUxo&hl=en&sa=X&ved=0CDEQ6AEwBGoVChMI0-3314WHxgIVE3-SCh32zgBy#v=onepage&q=permutation%20distance%20restriction&f=false – Edward Doolittle Jun 11 '15 at 09:56
  • Here's another idea: generate all permutations which shuffle items at most one place in either direction. Then chain together d of those permutations, randomly chosen. – Edward Doolittle Jun 11 '15 at 10:04
  • @edward: do you think you could write up that solution. It definitely sounds interesting, but I wonder about the runtime – inspectorG4dget Jun 11 '15 at 10:14
  • I wrote it up below. Runtime is O(nd). I don't know whether my algorithm finds the complete set of such permutations (but I think it does) nor whether they are uniformly distributed (probably not, but close). – Edward Doolittle Jun 11 '15 at 12:03
  • I've been thinking about some ideas based on using a transition matrix between the empty/full spot patterns and solving the recurrence to find explicit formulas for how many permutations there are, but it seems like we still need to remember information about too many degrees of freedom. Random walk methods seem like they might work; there's apparently a proof that a certain type of random walk mixes quickly for d>=n/2, but for d that close to n, I think rejection sampling would also be practical. – user2357112 Jun 11 '15 at 21:43
  • The similar problem with one-directional distance restrictions is so much easier. Placing each element starting from the front just works in that case. – user2357112 Jun 11 '15 at 22:22
  • @user2357112: what is a one-directional distance restriction? Is the movement simply not restricted on the other side? The assumption is that the distance metric does not consider the list to be torroidal, i.e. the last element cannot move to the head of the list (even when d=1 and the size of the list is greater than 2). Please explain your comment further, as I can see your idea yielding only the identity permucation – inspectorG4dget Jun 12 '15 at 07:17
  • @inspectorG4dget: A one-directional distance restriction would be where each element can only move d places forward, but they can move any distance backward (or vice versa). – user2357112 Jun 12 '15 at 07:38
  • @user2357112 I cannot find a significant difference in distribution between my non-swap function to yours for: `print(collections.Counter(tuple(_distance_limited_shuffle([0, 1, 2, 3], 2,_markov_chain(4,2))) for i in xrange(1000)))`, running the counter a few times in a row. – גלעד ברקן Jun 13 '15 at 05:49
  • @גלעדברקן: I find that `magicFunction` is much less likely than `_distance_limited_shuffle` to produce the outputs `(0, 1, 2, 3)` or `(0, 2, 1, 3)`. There are enough permutations in the sample space that 1000 runs isn't enough to see it; with 1000000 runs, the difference is clear. – user2357112 Jun 13 '15 at 06:12
  • @user2357112 yes, I see, thanks. Seems it might be a similar take on what happens with n=3 and the identity permutation. Interesting. – גלעד ברקן Jun 13 '15 at 06:34
  • I'm very pleasantly surprised at the number and breadth of answers. I'm going to leave this question open for a bit long, just to see what other responses it garners – inspectorG4dget Jun 13 '15 at 09:20
  • 1
    @user2357112 I'm not sure if it's still of interest but the first 10 pages of this Master's thesis explain a fairly straightforward way of counting the restricted permutations using rook polynomials. It seems like it may be especially straightforward for d>=n/2 because of the ease in creating disjoint B's (see the paper). http://people.rit.edu/hxssma/Ben-thesis.pdf and here's an online demonstration: https://www.bluffton.edu/~nesterd/java/rookpolynomials.html – גלעד ברקן Jun 13 '15 at 20:36
  • Can anyone find "Applications of Random Walks for Restricted Permutations", by Diaconis, Graham, and Holmes, in 1999? I could only find "Statistical Problems Involving Permutations With Restricted Positions", by the same authors in the same year. The paper I found says that the result I'm looking for was proved in the paper I'm looking for. – user2357112 Jun 14 '15 at 01:10
  • After looking some more, it seems that the paper I'm looking for may not exist, and the result I'm looking for may not actually have been proven. I hope it exists; if it doesn't, the best bound I have on the runtime of a random walk-based method is quartic in `len(L)` and unknown in `d`. – user2357112 Jun 14 '15 at 02:09
  • @user2357112 revisiting your question above about "an efficient way to compute f, particularly for large d," I coded the rook polynomial method here and it seems very fast for large d: http://ideone.com/Vnfw1D (still have to figure what to do when the B's overlap, i.e., when d < n/2...) – גלעד ברקן Jun 17 '15 at 20:20
  • @גלעדברקן: Yeah, there doesn't seem to be a good way to adapt it to d – user2357112 Jun 19 '15 at 00:58
  • @user2357112 I think this paper shows how to count the restricted permutations for any `d` in O(log2*N) - did I understand this correctly? The math might be too advanced for me. Is this kind of thing reasonably code-able? http://www.doiserbia.nb.rs/img/doi/1452-8630/2010/1452-86301000008B.pdf – גלעד ברקן Jun 23 '15 at 22:55
  • @user2357112 reading that method more carefully, it actually does not seem too complicated. I'm not sure why the author sets the complexity at O(log2N) when you also have to generate (k+r) choose k equations (in our case, 2*d choose d). But for reasonably sized d, it might work well. Cool method! I might try coding. – גלעד ברקן Jun 24 '15 at 01:11
  • @גלעדברקן: I haven't read it thoroughly yet, but unfortunately, it looks like the transition matrix thing I was considering earlier. The recurrences and matrices they arrive at for k=r=2 look like the same ones I got. The O(log(n)) complexity they give is misleading; it's the number of matrix multiplications, not the actual time complexity, and the matrices become huge for moderately large k and r. – user2357112 Jun 24 '15 at 01:39
  • (Also, the integers in the matrices become huge too, so we can't assume arithmetic is constant time. This is also an issue for my solution, since it uses integer arithmetic where it should use floating-point. Of course, the paper can't use floating-point, since they want exact counts, but for our purposes, a deviation of a few quadrillionths from exact uniformity is fine.) – user2357112 Jun 24 '15 at 01:43

7 Answers7

6

This is going to be long and dry.

I have a solution that produces a uniform distribution. It requires O(len(L) * d**d) time and space for precomputation, then performs shuffles in O(len(L)*d) time1. If a uniform distribution is not required, the precomputation is unnecessary, and the shuffle time can be reduced to O(len(L)) due to faster random choices; I have not implemented the non-uniform distribution. Both steps of this algorithm are substantially faster than brute force, but they're still not as good as I'd like them to be. Also, while the concept should work, I have not tested my implementation as thoroughly as I'd like.

Suppose we iterate over L from the front, choosing a position for each element as we come to it. Define the lag as the distance between the next element to place and the first unfilled position. Every time we place an element, the lag grows by at most one, since the index of the next element is now one higher, but the index of the first unfilled position cannot become lower.

Whenever the lag is d, we are forced to place the next element in the first unfilled position, even though there may be other empty spots within a distance of d. If we do so, the lag cannot grow beyond d, we will always have a spot to put each element, and we will generate a valid shuffle of the list. Thus, we have a general idea of how to generate shuffles; however, if we make our choices uniformly at random, the overall distribution will not be uniform. For example, with len(L) == 3 and d == 1, there are 3 possible shuffles (one for each position of the middle element), but if we choose the position of the first element uniformly, one shuffle becomes twice as likely as either of the others.

If we want a uniform distribution over valid shuffles, we need to make a weighted random choice for the position of each element, where the weight of a position is based on the number of possible shuffles if we choose that position. Done naively, this would require us to generate all possible shuffles to count them, which would take O(d**len(L)) time. However, the number of possible shuffles remaining after any step of the algorithm depends only on which spots we've filled, not what order they were filled in. For any pattern of filled or unfilled spots, the number of possible shuffles is the sum of the number of possible shuffles for each possible placement of the next element. At any step, there are at most d possible positions to place the next element, and there are O(d**d) possible patterns of unfilled spots (since any spot further than d behind the current element must be full, and any spot d or further ahead must be empty). We can use this to generate a Markov chain of size O(len(L) * d**d), taking O(len(L) * d**d) time to do so, and then use this Markov chain to perform shuffles in O(len(L)*d) time.

Example code (currently not quite O(len(L)*d) due to inefficient Markov chain representation):

import random

# states are (k, filled_spots) tuples, where k is the index of the next
# element to place, and filled_spots is a tuple of booleans
# of length 2*d, representing whether each index from k-d to
# k+d-1 has an element in it. We pretend indices outside the array are
# full, for ease of representation.

def _successors(n, d, state):
    '''Yield all legal next filled_spots and the move that takes you there.

    Doesn't handle k=n.'''
    k, filled_spots = state
    next_k = k+1

    # If k+d is a valid index, this represents the empty spot there.
    possible_next_spot = (False,) if k + d < n else (True,)

    if not filled_spots[0]:
        # Must use that position.
        yield k-d, filled_spots[1:] + possible_next_spot
    else:
        # Can fill any empty spot within a distance d.
        shifted_filled_spots = list(filled_spots[1:] + possible_next_spot)
        for i, filled in enumerate(shifted_filled_spots):
            if not filled:
                successor_state = shifted_filled_spots[:]
                successor_state[i] = True
                yield next_k-d+i, tuple(successor_state)
                # next_k instead of k in that index computation, because
                # i is indexing relative to shifted_filled_spots instead
                # of filled_spots


def _markov_chain(n, d):
    '''Precompute a table of weights for generating shuffles.

    _markov_chain(n, d) produces a table that can be fed to
    _distance_limited_shuffle to permute lists of length n in such a way that
    no list element moves a distance of more than d from its initial spot,
    and all permutations satisfying this condition are equally likely.

    This is expensive.

    '''

    if d >= n - 1:
        # We don't need the table, and generating a table for d >= n
        # complicates the indexing a bit. It's too complicated already.
        return None

    table = {}
    termination_state = (n, (d*2 * (True,)))
    table[termination_state] = 1

    def possible_shuffles(state):
        try:
            return table[state]
        except KeyError:
            k, _ = state
            count = table[state] = sum(
                    possible_shuffles((k+1, next_filled_spots))
                    for (_, next_filled_spots) in _successors(n, d, state)
            )
            return count

    initial_state = (0, (d*(True,) + d*(False,)))
    possible_shuffles(initial_state)
    return table

def _distance_limited_shuffle(l, d, table):
    # Generate an index into the set of all permutations, then use the
    # markov chain to efficiently find which permutation we picked.

    n = len(l)

    if d >= n - 1:
        random.shuffle(l)
        return

    permutation = [None]*n
    state = (0, (d*(True,) + d*(False,)))
    permutations_to_skip = random.randrange(table[state])

    for i, item in enumerate(l):
        for placement_index, new_filled_spots in _successors(n, d, state):
            new_state = (i+1, new_filled_spots)
            if table[new_state] <= permutations_to_skip:
                permutations_to_skip -= table[new_state]
            else:
                state = new_state
                permutation[placement_index] = item
                break
    return permutation

class Shuffler(object):
    def __init__(self, n, d):
        self.n = n
        self.d = d
        self.table = _markov_chain(n, d)
    def shuffled(self, l):
        if len(l) != self.n:
            raise ValueError('Wrong input size')
        return _distance_limited_shuffle(l, self.d, self.table)
    __call__ = shuffled

1We could use a tree-based weighted random choice algorithm to improve the shuffle time to O(len(L)*log(d)), but since the table becomes so huge for even moderately large d, this doesn't seem worthwhile. Also, the factors of d**d in the bounds are overestimates, but the actual factors are still at least exponential in d.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • what makes you think my non-swap based solution would not present a uniform distribution? oh, actually, what do you mean by "uniform distribution"? is it that every index moves somewhere or that any valid permutation is equally likely? – גלעד ברקן Jun 10 '15 at 21:46
  • @גלעדברקן: Try it and see: `collections.Counter(tuple(magicFunction([0, 1, 2], 1)) for i in xrange(1000))`. You'll find that `(1, 0, 2)` comes up far more often than it should. – user2357112 Jun 10 '15 at 21:48
  • @גלעדברקן: By "uniform distribution", I mean that every valid permutation is equally likely. – user2357112 Jun 10 '15 at 21:49
  • not sure if this may be relevant to you - http://www.ma.utexas.edu/users/olenab/olena_thesis.pdf – גלעד ברקן Jun 11 '15 at 05:56
  • @גלעדברקן: Thanks for pointing me to that thesis. It seems pretty helpful. – user2357112 Jun 11 '15 at 07:04
  • Is this algorithm based on a particular paper or book? Most publications seem focused solely on *counting* the restricted permutations, but don't provide a method to actually obtain them. It seems that your `table[initial_state]` turns out to be equal to that count; but why is that? It means that `permutations_to_skip` can indeed be used to index the set of all permutations with this restriction. I would also like to better understand how your construction of an explicit permutation that respects the distance constraint works based on this simple index. – wea0 Feb 21 '23 at 21:57
  • @wea0: I just derived it from first principles. There's probably stuff in the literature about this, but I'm not familiar with the literature. – user2357112 Feb 21 '23 at 22:24
  • The `table` entry for a given state is the number of valid permutations that can be formed continuing from that state, so `table[initial_state]` is the number of valid permutations that can be formed starting from the initial state. – user2357112 Feb 21 '23 at 22:33
  • The algorithm for converting a permutation index to a permutation is basically the algorithm for finding the Nth leaf in a tree where nodes store the number of leaves of the subtree rooted at that node. Here, the tree is the tree of all possible element placement decisions we could make while constructing a permutation, and we use some tricks to avoid having to store the whole tree explicitly. – user2357112 Feb 21 '23 at 22:59
5

In short, the list that should be shuffled gets ordered by the sum of index and a random number.

import random
xs = range(20) # list that should be shuffled
d = 5          # distance
[x for i,x in sorted(enumerate(xs), key= lambda (i,x): i+(d+1)*random.random())]

Out:

[1, 4, 3, 0, 2, 6, 7, 5, 8, 9, 10, 11, 12, 14, 13, 15, 19, 16, 18, 17]

Thats basically it. But this looks a little bit overwhelming, therefore...

The algorithm in more detail

To understand this better, consider this alternative implementation of an ordinary, random shuffle:

import random
sorted(range(10), key = lambda x: random.random())

Out:

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

In order to constrain the distance, we have to implement a alternative sort key function that depends on the index of an element. The function sort_criterion is responsible for that.

import random

def exclusive_uniform(a, b):
    "returns a random value in the interval  [a, b)"
    return a+(b-a)*random.random()

def distance_constrained_shuffle(sequence, distance,
                                 randmoveforward = exclusive_uniform):
    def sort_criterion(enumerate_tuple):
        """
        returns the index plus a random offset,
        such that the result can overtake at most 'distance' elements
        """
        indx, value = enumerate_tuple
        return indx + randmoveforward(0, distance+1)

    # get enumerated, shuffled list
    enumerated_result = sorted(enumerate(sequence), key = sort_criterion)
    # remove enumeration
    result = [x for i, x in enumerated_result]
    return result

With the argument randmoveforward you can pass a random number generator with a different probability density function (pdf) to modify the distance distribution.

The remainder is testing and evaluation of the distance distribution.


Test function

Here is an implementation of the test function. The validatefunction is actually taken from the OP, but I removed the creation of one of the dictionaries for performance reasons.

def test(num_cases = 10, distance = 3, sequence = range(1000)):
    def validate(d, lst, answer):
        #old = {e:i for i,e in enumerate(lst)}
        new = {e:i for i,e in enumerate(answer)}
        return all(abs(i-new[e])<=d for i,e in enumerate(lst))
        #return all(abs(i-new[e])<=d for e,i in old.iteritems())


    for _ in range(num_cases):
        result = distance_constrained_shuffle(sequence, distance)
        if not validate(distance, sequence, result):
            print "Constraint violated. ", result
            break
    else:
        print "No constraint violations"


test()

Out:

No constraint violations

Distance distribution

I am not sure whether there is a way to make the distance uniform distributed, but here is a function to validate the distribution.

def distance_distribution(maxdistance = 3, sequence = range(3000)):
    from collections import Counter

    def count_distances(lst, answer):
        new = {e:i for i,e in enumerate(answer)}
        return Counter(i-new[e] for i,e in enumerate(lst))    

    answer = distance_constrained_shuffle(sequence, maxdistance)
    counter = count_distances(sequence, answer)

    sequence_length = float(len(sequence))

    distances = range(-maxdistance, maxdistance+1)
    return distances, [counter[d]/sequence_length for d in distances]

distance_distribution()

Out:

([-3, -2, -1, 0, 1, 2, 3],
 [0.01,
  0.076,
  0.22166666666666668,
  0.379,
  0.22933333333333333,
  0.07766666666666666,
  0.006333333333333333])

Distance distribution/pdf for d=3

Or for a case with greater maximum distance:

distance_distribution(maxdistance=9, sequence=range(100*1000))

Distance distribution for d=9

koffein
  • 1,792
  • 13
  • 21
  • 1
    I see your tests, and they do show that the algorithm works. However, I'm curious as to whether there is a proof that the algorithm respects the distance constraint. It seems to me that this algorithm might at some point allow an element to move exactly d+1 from it's original position, just because of the way `randmoveforward` works – inspectorG4dget Jun 11 '15 at 16:32
  • 1
    This would be the case, if e.g. the 0th element moved d+1 and the (d+1)th element moved 0 forward. `random.uniform` contains the upper and the lower bound, so with this function it could actually happen, you are right. (Though I think `sorted` keeps order, when two keys are equal...) `random.random` is defined as `*->[0,1)`, so using this function would work. Thanks for that hint, @inspectorG4dget . I am going to define an exclusive uniform function for that... – koffein Jun 11 '15 at 16:50
4

This is a very difficult problem, but it turns out there is a solution in the academic literature, in an influential paper by Mark Jerrum, Alistair Sinclair, and Eric Vigoda, A Polynomial-Time Approximation Algorithm for the Permanent of a Matrix with Nonnegative Entries, Journal of the ACM, Vol. 51, No. 4, July 2004, pp. 671–697. http://www.cc.gatech.edu/~vigoda/Permanent.pdf.

Here is the general idea: first write down two copies of the numbers in the array that you want to permute. Say

1   1
2   2
3   3
4   4

Now connect a node on the left to a node on the right if mapping from the number on the left to the position on the right is allowed by the restrictions in place. So if d=1 then 1 on the left connects to 1 and 2 on the right, 2 on the left connects to 1, 2, 3 on the right, 3 on the left connects to 2, 3, 4 on the right, and 4 on the left connects to 3, 4 on the right.

1 - 1
  X 
2 - 2
  X
3 - 3
  X
4 - 4

The resulting graph is bipartite. A valid permutation corresponds a perfect matching in the bipartite graph. A perfect matching, if it exists, can be found in O(VE) time (or somewhat better, for more advanced algorithms).

Now the problem becomes one of generating a uniformly distributed random perfect matching. I believe that can be done, approximately anyway. Uniformity of the distribution is the really hard part.

What does this have to do with permanents? Consider a matrix representation of our bipartite graph, where a 1 means an edge and a 0 means no edge:

1 1 0 0
1 1 1 0
0 1 1 1
0 0 1 1

The permanent of the matrix is like the determinant, except there are no negative signs in the definition. So we take exactly one element from each row and column, multiply them together, and add up over all choices of row and column. The terms of the permanent correspond to permutations; the term is 0 if any factor is 0, in other words if the permutation is not valid according to the matrix/bipartite graph representation; the term is 1 if all factors are 1, in other words if the permutation is valid according to the restrictions. In summary, the permanent of the matrix counts all permutations satisfying the restriction represented by the matrix/bipartite graph.

It turns out that unlike calculating determinants, which can be accomplished in O(n^3) time, calculating permanents is #P-complete so finding an exact answer is not feasible in general. However, if we can estimate the number of valid permutations, we can estimate the permanent. Jerrum et. al. approached the problem of counting valid permutations by generating valid permutations uniformly (within a certain error, which can be controlled); an estimate of the value of the permanent can be obtained by a fairly elaborate procedure (section 5 of the paper referenced) but we don't need that to answer the question at hand.

The running time of Jerrum's algorithm to calculate the permanent is O(n^11) (ignoring logarithmic factors). I can't immediately tell from the paper the running time of the part of the algorithm that uniformly generates bipartite matchings, but it appears to be over O(n^9). However, another paper reduces the running time for the permanent to O(n^7): http://www.cc.gatech.edu/fac/vigoda/FasterPermanent_SODA.pdf; in that paper they claim that it is now possible to get a good estimate of a permanent of a 100x100 0-1 matrix. So it should be possible to (almost) uniformly generate restricted permutations for lists of 100 elements.

There may be further improvements, but I got tired of looking.

If you want an implementation, I would start with the O(n^11) version in Jerrum's paper, and then take a look at the improvements if the original algorithm is not fast enough.

There is pseudo-code in Jerrum's paper, but I haven't tried it so I can't say how far the pseudo-code is from an actual implementation. My feeling is it isn't too far. Maybe I'll give it a try if there's interest.

Edward Doolittle
  • 4,002
  • 2
  • 14
  • 27
1

I am not sure how good it is, but maybe something like:

  • create a list of same length than initial list L; each element of this list should be a list of indices of allowed initial indices to be moved here; for instance [[0,1,2],[0,1,2,3],[0,1,2,3],[1,2,3]] if I understand correctly your example;
  • take the smallest sublist (or any of the smallest sublists if several lists share the same length);
  • pick a random element in it with random.choice, this element is the index of the element in the initial list to be mapped to the current location (use another list for building your new list);
  • remove the randomly chosen element from all sublists

For instance:

L = [ "A", "B", "C", "D" ]
i = [[0,1,2],[0,1,2,3],[0,1,2,3],[1,2,3]]
# I take [0,1,2] and pick randomly 1 inside
# I remove the value '1' from all sublists and since
# the first sublist has already been handled I set it to None
# (and my result will look as [ "B", None, None, None ]
i = [None,[0,2,3],[0,2,3],[2,3]]
# I take the last sublist and pick randomly 3 inside
# result will be ["B", None, None, "D" ]
i = [None,[0,2], [0,2], None]
etc.

I haven't tried it however. Regards.

Thomas Baruchel
  • 7,236
  • 2
  • 27
  • 46
  • 1
    This doesn't work. It's possible to end up with elements that have nowhere left to go, or spots that have no elements left to fill them. – user2357112 Jun 10 '15 at 07:13
1

Here are two sketches in Python; one swap-based, the other non-swap-based. In the first, the idea is to keep track of where the indexes have moved and test if the next swap would be valid. An additional variable is added for the number of swaps to make.

from random import randint

def swap(a,b,L):
  L[a], L[b] = L[b], L[a]

def magicFunction(L,d,numSwaps):
  n = len(L)
  new = list(range(0,n))
  for i in xrange(0,numSwaps):
    x = randint(0,n-1)
    y = randint(max(0,x - d),min(n - 1,x + d))
    while abs(new[x] - y) > d or abs(new[y] - x) > d:
      y = randint(max(0,x - d),min(n - 1,x + d))
    swap(x,y,new)
    swap(x,y,L)
  return L

print(magicFunction([1,2,3,4],2,3)) # [2, 1, 4, 3]
print(magicFunction([1,2,3,4,5,6,7,8,9],2,4)) # [2, 3, 1, 5, 4, 6, 8, 7, 9]

Using print(collections.Counter(tuple(magicFunction([0, 1, 2], 1, 1)) for i in xrange(1000))) we find that the identity permutation comes up heavy with this code (the reason why is left as an exercise for the reader).


Alternatively, we can think about it as looking for a permutation matrix with interval restrictions, where abs(i - j) <= d where M(i,j) would equal 1. We can construct a one-off random path by picking a random j for each row from those still available. x's in the following example represent matrix cells that would invalidate the solution (northwest to southeast diagonal would represent the identity permutation), restrictions represent how many is are still available for each j. (Adapted from my previous version to choose both the next i and the next j randomly, inspired by user2357112's answer):

n = 5, d = 2

Start:

0 0 0 x x
0 0 0 0 x
0 0 0 0 0
x 0 0 0 0
x x 0 0 0

restrictions = [3,4,5,4,3]  # how many i's are still available for each j

1.

0 0 1 x x  # random choice
0 0 0 0 x
0 0 0 0 0
x 0 0 0 0
x x 0 0 0

restrictions = [2,3,0,4,3] # update restrictions in the neighborhood of (i ± d)

2.

0 0 1 x x 
0 0 0 0 x  
0 0 0 0 0
x 0 0 0 0
x x 0 1 0  # random choice

restrictions = [2,3,0,0,2] # update restrictions in the neighborhood of (i ± d)

3.

0 0 1 x x
0 0 0 0 x
0 1 0 0 0  # random choice
x 0 0 0 0
x x 0 1 0

restrictions = [1,0,0,0,2] # update restrictions in the neighborhood of (i ± d)

only one choice for j = 0 so it must be chosen

4.

0 0 1 x x  
1 0 0 0 x  # dictated choice
0 1 0 0 0
x 0 0 0 0
x x 0 1 0

restrictions = [0,0,0,0,2] # update restrictions in the neighborhood of (i ± d)

Solution:

0 0 1 x x
1 0 0 0 x
0 1 0 0 0
x 0 0 0 1  # dictated choice
x x 0 1 0

[2,0,1,4,3]

Python code (adapted from my previous version to choose both the next i and the next j randomly, inspired by user2357112's answer):

from random import randint,choice
import collections

def magicFunction(L,d):
  n = len(L)
  restrictions = [None] * n
  restrict = -1
  solution = [None] * n
  for i in xrange(0,n):
    restrictions[i] = abs(max(0,i - d) - min(n - 1,i + d)) + 1
  while True:
    availableIs = filter(lambda x: solution[x] == None,[i for i in xrange(n)]) if restrict == -1 else filter(lambda x: solution[x] == None,[j for j in xrange(max(0,restrict - d),min(n,restrict + d + 1))])
    if not availableIs:
      L = [L[i] for i in solution]
      return L
    i = choice(availableIs)
    availableJs = filter(lambda x: restrictions[x] <> 0,[j for j in xrange(max(0,i - d),min(n,i + d + 1))])
    nextJ = restrict if restrict != -1 else choice(availableJs)
    restrict = -1
    solution[i] = nextJ
    restrictions[ nextJ ] = 0
    for j in xrange(max(0,i - d),min(n,i + d + 1)):
      if j == nextJ or restrictions[j] == 0:
        continue
      restrictions[j] = restrictions[j] - 1
      if restrictions[j] == 1:
        restrict = j

print(collections.Counter(tuple(magicFunction([0, 1, 2], 1)) for i in xrange(1000)))

Using print(collections.Counter(tuple(magicFunction([0, 1, 2], 1)) for i in xrange(1000))) we find that the identity permutation comes up light with this code (why is left as an exercise for the reader).

גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
1

My idea is to generate permutations by moving at most d steps by generating d random permutations which move at most 1 step and chaining them together.

We can generate permutations which move at most 1 step quickly by the following recursive procedure: consider a permutation of {1,2,3,...,n}. The last item, n, can move either 0 or 1 place. If it moves 0 places, n is fixed, and we have reduced the problem to generating a permutation of {1,2,...,n-1} in which every item moves at most one place.

On the other hand, if n moves 1 place, it must occupy position n-1. Then n-1 must occupy position n (if any smaller number occupies position n, it will have moved by more than 1 place). In other words, we must have a swap of n and n-1, and after swapping we have reduced the problem to finding such a permutation of the remainder of the array {1,...,n-2}.

Such permutations can be constructed in O(n) time, clearly.

Those two choices should be selected with weighted probabilities. Since I don't know the weights (though I have a theory, see below) maybe the choice should be 50-50 ... but see below.

A more accurate estimate of the weights might be as follows: note that the number of such permutations follows a recursion that is the same as the Fibonacci sequence: f(n) = f(n-1) + f(n-2). We have f(1) = 1 and f(2) = 2 ({1,2} goes to {1,2} or {2,1}), so the numbers really are the Fibonacci numbers. So my guess for the probability of choosing n fixed vs. swapping n and n-1 would be f(n-1)/f(n) vs. f(n-2)/f(n). Since the ratio of consecutive Fibonacci numbers quickly approaches the Golden Ratio, a reasonable approximation to the probabilities is to leave n fixed 61% of the time and swap n and n-1 39% of the time.

To construct permutations where items move at most d places, we just repeat the process d times. The running time is O(nd).

Here is an outline of an algorithm.

arr = {1,2,...,n};
for (i = 0; i < d; i++) {
  j = n-1;
  while (j > 0) {
    u = random uniform in interval (0,1)
    if (u < 0.61) { // related to golden ratio phi; more decimals may help
      j -= 1;
    } else {
      swap items at positions j and j-1 of arr // 0-based indexing
      j -= 2;
    }
  }
}

Since each pass moves items at most 1 place from their start, d passes will move items at most d places. The only question is the uniform distribution of the permutations. It would probably be a long proof, if it's even true, so I suggest assembling empirical evidence for various n's and d's. Probably to prove the statement, we would have to switch from using the golden ratio approximation to f(n-1)/f(n-2) in place of 0.61.

There might even be some weird reason why some permutations might be missed by this procedure, but I'm pretty sure that doesn't happen. Just in case, though, it would be helpful to have a complete inventory of such permutations for some values of n and d to check the correctness of my proposed algorithm.

Update

I found an off-by-one error in my "pseudocode", and I corrected it. Then I implemented in Java to get a sense of the distribution. Code is below. The distribution is far from uniform, I think because there are many ways of getting restricted permutations with short max distances (move forward, move back vs. move back, move forward, for example) but few ways of getting long distances (move forward, move forward). I can't think of a way to fix the uniformity issue with this method.

import java.util.Random;
import java.util.Map;
import java.util.TreeMap;

class RestrictedPermutations {
  private static Random rng = new Random();

  public static void rPermute(Integer[] a, int d) {
    for (int i = 0; i < d; i++) {
      int j = a.length-1;
      while (j > 0) {
        double u = rng.nextDouble();
        if (u < 0.61) { // related to golden ratio phi; more decimals may help
          j -= 1;
        } else {
          int t = a[j];
          a[j] = a[j-1];
          a[j-1] = t;
          j -= 2;
        }
      }
    }
  }

  public static void main(String[] args) {
    int numTests = Integer.parseInt(args[0]);
    int d = 2;
    Map<String,Integer> count = new TreeMap<String,Integer>();
    for (int t = 0; t < numTests; t++) {
      Integer[] a = {1,2,3,4,5};
      rPermute(a,d);
      // convert a to String for storage in Map
      String s = "(";
      for (int i = 0; i < a.length-1; i++) {
        s += a[i] + ",";
      }
      s += a[a.length-1] + ")";
      int c = count.containsKey(s) ? count.get(s) : 0;
      count.put(s,c+1);
    }
    for (String k : count.keySet()) {
      System.out.println(k + ": " + count.get(k));
    }
  }

}
Edward Doolittle
  • 4,002
  • 2
  • 14
  • 27
  • I don't have a valid proof, but this makes sense to me. I think it is correct – inspectorG4dget Jun 11 '15 at 14:48
  • I reversed the inequality sign in the probability test. – Edward Doolittle Jun 11 '15 at 16:18
  • Unfortunately, this doesn't produce all permutations. Take n=10, d=5, and consider the permutation where every element ends up 5 spots away from its starting point. If this is to be produced by chaining 5 permutations with d=1, then at every step, every element must move toward its final position. However, if the first 5 elements all move toward the end of the array, the back 5 elements get squished; they can't move far enough to fill the hole at the front of the array. – user2357112 Jun 11 '15 at 21:50
  • Can you give me an example of a permutation where every element ends up 5 spots away from its starting point? – Edward Doolittle Jun 11 '15 at 22:06
  • `[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -> [5, 6, 7, 8, 9, 0, 1, 2, 3, 4]` – user2357112 Jun 11 '15 at 22:17
  • Yup, you're right. There are also a lot of other restricted permutations missing, now that I've had a chance to test it more thoroughly. Back to the drawing board. – Edward Doolittle Jun 12 '15 at 05:02
0

Here's an adaptation of @גלעד ברקן's code that takes only one pass through the list (in random order) and swaps only once (using a random choice of possible positions):

from random import choice, shuffle

def magicFunction(L, d):
  n = len(L)
  swapped = [0] * n     # 0: position not swapped, 1: position was swapped
  positions = list(xrange(0,n))         # list of positions: 0..n-1
  shuffle(positions)                    # randomize positions
  for x in positions:
    if swapped[x]:      # only swap an item once
      continue
    # find all possible positions to swap
    possible = [i for i in xrange(max(0, x - d), min(n, x + d)) if not swapped[i]]
    if not possible:
      continue
    y = choice(possible)        # choose another possible position at random
    if x != y:
      L[y], L[x] = L[x], L[y]   # swap with that position
      swapped[x] = swapped[y] = 1       # mark both positions as swapped
  return L

Here is a refinement of the above code that simply finds all possible adjacent positions and chooses one:

from random import choice

def magicFunction(L, d):
  n = len(L)
  positions = list(xrange(0, n))        # list of positions: 0..n-1
  for x in xrange(0, n):
    # find all possible positions to swap
    possible = [i for i in xrange(max(0, x - d), min(n, x + d)) if abs(positions[i] - x) <= d]
    if not possible:
      continue
    y = choice(possible)        # choose another possible position at random
    if x != y:
      L[y], L[x] = L[x], L[y]   # swap with that position
      positions[x] = y
      positions[y] = x
  return L
Brent Washburne
  • 12,904
  • 4
  • 60
  • 82