Создайте случайный порядок (x, y) пар, не повторяя/последующие x

Скажем, у меня есть список допустимых X = [1, 2, 3, 4, 5] и список допустимых Y = [1, 2, 3, 4, 5].

Мне нужно сгенерировать все комбинации каждого элемента в X и каждый элемент из Y (в данном случае, 25) и получить эти комбинации в случайном порядке.

Это само по себе было бы простым, но есть дополнительное требование: в этом случайном порядке не может быть повторения одного и того же X подряд. Например, это нормально:

[1, 3]
[2, 5]
[1, 2]
...
[1, 4]

Это не:

[1, 3]
[1, 2]  <== the "1" cannot repeat, because there was already one before
[2, 5]
...
[1, 4]

Теперь наименее эффективной идеей было бы просто рандомизировать полный набор, если повторений больше нет. Мой подход был несколько иным, неоднократно создавая перетасованный вариант X и список всех Y * X, а затем выбирал из него случайный следующий. До сих пор я придумал это:

import random

output = []
num_x  = 5
num_y  = 5

all_ys = list(xrange(1, num_y + 1)) * num_x

while True:
    # end if no more are available
    if len(output) == num_x * num_y:
        break

    xs = list(xrange(1, num_x + 1))
    while len(xs):
        next_x = random.choice(xs)
        next_y = random.choice(all_ys)

        if [next_x, next_y] not in output:
            xs.remove(next_x)
            all_ys.remove(next_y)
            output.append([next_x, next_y])

print(sorted(output))

Но я уверен, что это можно сделать еще более эффективно или более лаконично?

Кроме того, мое решение сначала проходит через все значения X, прежде чем продолжить полный набор, что не является совершенно случайным. Я могу жить с этим для моего конкретного случая приложения.

Ответ 1

Интересный вопрос! Вот мое решение. Он обладает следующими свойствами:

  • Если нет правильного решения, оно должно обнаружить это и сообщить вам
  • Итерация гарантированно завершается, поэтому она никогда не должна застревать в бесконечном цикле
  • Любое возможное решение доступно с ненулевой вероятностью

Я не знаю распределения вывода по всем возможным решениям, но я думаю, что он должен быть однородным, потому что нет очевидной асимметрии, присущей алгоритму. Я был бы удивлен и рад, если бы был показан иначе!

import random

def random_without_repeats(xs, ys):
    pairs = [[x,y] for x in xs for y in ys]
    output = [[object()], [object()]]
    seen = set()
    while pairs:
        # choose a random pair from the ones left
        indices = list(set(xrange(len(pairs))) - seen)
        try:
            index = random.choice(indices)
        except IndexError:
            raise Exception('No valid solution exists!')
        # the first element of our randomly chosen pair
        x = pairs[index][0]
        # search for a valid place in output where we slot it in
        for i in xrange(len(output) - 1):
            left, right = output[i], output[i+1]
            if x != left[0] and x != right[0]:
                output.insert(i+1, pairs.pop(index))
                seen = set()
                break
        else:
            # make sure we don't randomly choose a bad pair like that again
            seen |= {i for i in indices if pairs[i][0] == x}
    # trim off the sentinels
    output = output[1:-1]
    assert len(output) == len(xs) * len(ys)
    assert not any(L==R for L,R in zip(output[:-1], output[1:]))
    return output


nx, ny = 5, 5       # OP example
# nx, ny = 2, 10      # output must alternate in 1st index
# nx, ny = 4, 13      # shuffle 'deck of cards' with no repeating suit
# nx, ny = 1, 5       # should raise 'No valid solution exists!' exception

xs = range(1, nx+1)
ys = range(1, ny+1)

for pair in random_without_repeats(xs, ys):
    print pair

Ответ 2

Вот мое решение. Сначала кортежи выбираются среди тех, у кого другое значение х из предыдущего выбранного кортежа. Но я заметил, что вы должны подготовить окончательный трюк для случая, когда у вас есть только кортежи с плохими значениями, которые можно разместить в конце.

import random

num_x = 5
num_y = 5

all_ys = range(1,num_y+1)*num_x
all_xs = sorted(range(1,num_x+1)*num_y)

output = []

last_x = -1

for i in range(0,num_x*num_y):

    #get list of possible tuple to place    
    all_ind    = range(0,len(all_xs))
    all_ind_ok = [k for k in all_ind if all_xs[k]!=last_x]

    ind = random.choice(all_ind_ok)

    last_x = all_xs[ind]
    output.append([all_xs.pop(ind),all_ys.pop(ind)])


    if(all_xs.count(last_x)==len(all_xs)):#if only last_x tuples,
        break  

if len(all_xs)>0: # if there are still tuples they are randomly placed
    nb_to_place = len(all_xs)
    while(len(all_xs)>0):
        place = random.randint(0,len(output)-1)
        if output[place]==last_x:
            continue
        if place>0:
            if output[place-1]==last_x:
                continue
        output.insert(place,[all_xs.pop(),all_ys.pop()])

print output

Ответ 3

Простое решение для обеспечения усреднения O(N*M):

def pseudorandom(M,N):
    l=[(x+1,y+1) for x in range(N) for y in range(M)]
    random.shuffle(l)
    for i in range(M*N-1):
            for j in range (i+1,M*N): # find a compatible ...
                if l[i][0] != l[j][0]:
                    l[i+1],l[j] = l[j],l[i+1]
                    break  
            else:   # or insert otherwise.
                while True:
                    l[i],l[i-1] = l[i-1],l[i]
                    i-=1
                    if l[i][0] != l[i-1][0]: break  
    return l

Некоторые тесты:

In [354]: print(pseudorandom(5,5))
[(2, 2), (3, 1), (5, 1), (1, 1), (3, 2), (1, 2), (3, 5), (1, 5), (5, 4),\
(1, 3), (5, 2), (3, 4), (5, 3), (4, 5), (5, 5), (1, 4), (2, 5), (4, 4), (2, 4),\ 
(4, 2), (2, 1), (4, 3), (2, 3), (4, 1), (3, 3)]

In [355]: %timeit pseudorandom(100,100)
10 loops, best of 3: 41.3 ms per loop

Ответ 4

Здесь решение с использованием NumPy

def generate_pairs(xs, ys):
    n = len(xs)
    m = len(ys)
    indices = np.arange(n)

    array = np.tile(ys, (n, 1))
    [np.random.shuffle(array[i]) for i in range(n)]

    counts = np.full_like(xs, m)
    i = -1

    for _ in range(n * m):
        weights = np.array(counts, dtype=float)
        if i != -1:
            weights[i] = 0
        weights /= np.sum(weights)

        i = np.random.choice(indices, p=weights)
        counts[i] -= 1
        pair = xs[i], array[i, counts[i]]
        yield pair

Здесь блокнот Jupyter, в котором объясняется, как он работает

Внутри цикла нам нужно скопировать весы, добавить их и выбрать случайный индекс, используя веса. Все они линейны в n. Таким образом, общая сложность генерации всех пар O(n^2 m)

Но время исполнения детерминировано, а накладные расходы низки. И я уверен, что он генерирует все правовые последовательности с равной вероятностью.

Ответ 5

Это должно делать то, что вы хотите.

rando никогда не будет генерировать один и тот же X два раза подряд, но я понял, что это возможно (хотя кажется маловероятным, поскольку я никогда не замечал, что это произошло в 10 или около того раз, когда я бежал без дополнительной проверки), что из-за потенциального выброса повторяющихся пар это может произойти при предыдущем X. О! Но я думаю, что понял... обновит свой ответ через минуту.

import random

X = [1,2,3,4,5]
Y = [1,2,3,4,5]


def rando(choice_one, choice_two):
    last_x = random.choice(choice_one)
    while True:
        yield last_x, random.choice(choice_two)
        possible_x = choice_one[:]
        possible_x.remove(last_x)
        last_x = random.choice(possible_x)


all_pairs = set(itertools.product(X, Y))
result = []
r = rando(X, Y)
while set(result) != all_pairs:
    pair = next(r)
    if pair not in result:
        if result and result[-1][0] == pair[0]:
            continue
        result.append(pair)

import pprint
pprint.pprint(result)

Ответ 6

Равномерно распределите значения x (5 раз каждое значение) по вашему результату:

import random

def random_combo_without_x_repeats(xvals, yvals):
    # produce all valid combinations, but group by `x` and shuffle the `y`s
    grouped = [[x, random.sample(yvals, len(yvals))] for x in xvals]
    last_x = object()  # sentinel not equal to anything
    while grouped[0][1]:  # still `y`s left
        for _ in range(len(xvals)):
            # shuffle the `x`s, but skip any ordering that would
            # produce consecutive `x`s.
            random.shuffle(grouped)
            if grouped[0][0] != last_x:
                break
        else:
            # we tried to reshuffle N times, but ended up with the same `x` value
            # in the first position each time. This is pretty unlikely, but
            # if this happens we bail out and just reverse the order. That is
            # more than good enough.
            grouped = grouped[::-1]
        # yield a set of (x, y) pairs for each unique x
        # Pick one y (from the pre-shuffled groups per x
        for x, ys in grouped:
            yield x, ys.pop()
        last_x = x

Сначала сначала перемещается значения y на x, затем вы получаете комбинацию x, y для каждого x. Порядок, в котором даны x, перетасовывается на каждую итерацию, где вы проверяете ограничение.

Это случайное значение, но вы получите все числа от 1 до 5 в позиции x, прежде чем снова увидите тот же номер:

>>> list(random_combo_without_x_repeats(range(1, 6), range(1, 6)))
[(2, 1), (3, 2), (1, 5), (5, 1), (4, 1),
 (2, 4), (3, 1), (4, 3), (5, 5), (1, 4),
 (5, 2), (1, 1), (3, 3), (4, 4), (2, 5),
 (3, 5), (2, 3), (4, 2), (1, 2), (5, 4),
 (2, 2), (3, 4), (1, 3), (4, 5), (5, 3)]

(Я вручную сгруппировал это в набор из 5). В целом, это приводит к довольно хорошему случайному перетасовке фиксированного набора входных данных с вашим ограничением.

Это тоже эффективно; потому что есть только шанс 1-в-N, что вам нужно повторно перетасовать порядок x, вы должны увидеть только одну перестановку в среднем во время полного запуска алгоритма. Весь алгоритм остается в пределах O (N * M) границ, поэтому он идеально подходит для чего-то, производящего N раз M элементов вывода. Поскольку мы ограничиваем перестановку максимум N раз, прежде чем вернуться к простому обратному пути, мы избегаем (крайне маловероятной) возможности бесконечной перестановки.

Единственным недостатком тогда является то, что он должен создать N копий значений M y спереди.

Ответ 7

Для полноты, я думаю, я брошу супер-наивный "просто держись, пока не получишь одно" решение. Это не гарантирует даже прекращение, но если это произойдет, у него будет хорошая степень случайности, и вы сказали, что одно из желаемых качеств было лаконичным, и это обязательно кратким:

import itertools
import random

x = range(5)  # this is a list in Python 2
y = range(5)
all_pairs = list(itertools.product(x, y))

s = list(all_pairs)  # make a working copy
while any(s[i][0] == s[i + 1][0] for i in range(len(s) - 1)):
    random.shuffle(s)
print s

Как отмечалось, для небольших значений x и y (особенно y!) это на самом деле достаточно быстрое решение. Ваш пример из 5 для каждого заканчивается в среднем времени "сразу". Пример колоды карт (4 и 13) может занять гораздо больше времени, потому что обычно требуется сотни тысяч перетасовки. (И снова, не гарантируется прекращение вообще.)

Ответ 8

Вот эволюционный алгоритм. Сначала он развивает список, в котором элементы X повторяются len(Y) раз, а затем он случайным образом заполняет каждый элемент Y len (X) раз. Полученные порядки кажутся довольно случайными:

import random

#the following fitness function measures
#the number of times in which
#consecutive elements in a list
#are equal

def numRepeats(x):
    n = len(x)
    if n < 2: return 0
    repeats = 0
    for i in range(n-1):
        if x[i] == x[i+1]: repeats += 1
    return repeats

def mutate(xs):
    #swaps random pairs of elements
    #returns a new list
    #one of the two indices is chosen so that
    #it is in a repeated pair
    #and swapped element is different

    n = len(xs)
    repeats = [i for i in range(n) if (i > 0 and xs[i] == xs[i-1]) or (i < n-1 and xs[i] == xs[i+1])]
    i = random.choice(repeats)
    j = random.randint(0,n-1)
    while xs[j] == xs[i]: j = random.randint(0,n-1)
    ys = xs[:]
    ys[i], ys[j] = ys[j], ys[i]
    return ys

def evolveShuffle(xs, popSize = 100, numGens = 100):
    #tries to evolve a shuffle of xs so that consecutive
    #elements are different
    #takes the best 10% of each generation and mutates each 9
    #times. Stops when a perfect solution is found
    #popsize assumed to be a multiple of 10

    population = []

    for i in range(popSize):
        deck = xs[:]
        random.shuffle(deck)
        fitness = numRepeats(deck)
        if fitness == 0: return deck
        population.append((fitness,deck))

    for i in range(numGens):
        population.sort(key = (lambda p: p[0]))
        newPop = []
        for i in range(popSize//10):
            fit,deck = population[i]
            newPop.append((fit,deck))
            for j in range(9):
                newDeck = mutate(deck)
                fitness = numRepeats(newDeck)
                if fitness == 0: return newDeck
                newPop.append((fitness,newDeck))
        population = newPop
    #if you get here :
    return [] #no special shuffle found

#the following function takes a list x
#with n distinct elements (n>1) and an integer k
#and returns a random list of length nk
#where consecutive elements are not the same

def specialShuffle(x,k):
    n = len(x)
    if n == 2:
        if random.random() < 0.5:
            a,b = x
        else:
            b,a = x
        return [a,b]*k
    else:
        deck = x*k
        return evolveShuffle(deck)

def randOrder(x,y):
    xs = specialShuffle(x,len(y))
    d = {}
    for i in x:
        ys = y[:]
        random.shuffle(ys)
        d[i] = iter(ys)

    pairs = []
    for i in xs:
        pairs.append((i,next(d[i])))
    return pairs

например:

>>> randOrder([1,2,3,4,5],[1,2,3,4,5])
[(1, 4), (3, 1), (4, 5), (2, 2), (4, 3), (5, 3), (2, 1), (3, 3), (1, 1), (5, 2), (1, 3), (2, 5), (1, 5), (3, 5), (5, 5), (4, 4), (2, 3), (3, 2), (5, 4), (2, 4), (4, 2), (1, 2), (5, 1), (4, 1), (3, 4)]

При увеличении len(X) и len(Y) это затрудняет поиск решения (и предназначено для возврата пустого списка в этом случае), и в этом случае параметры popSize и numGens могут быть увеличены. Как и в случае, он может быстро найти решения 20х20. Требуется около минуты, когда X и Y имеют размер 100, но даже тогда могут найти решение (в то время, когда я его запустил).

Ответ 9

Интересное ограничение! Я, вероятно, задумался над этим, решая более общую проблему: перетасовывание произвольного списка последовательностей, для которых (если возможно) никакие две соседние последовательности не разделяют первый элемент.

from itertools import product
from random import choice, randrange, shuffle

def combine(*sequences):
    return playlist(product(*sequences))

def playlist(sequence):
    r'''Shuffle a set of sequences, avoiding repeated first elements.
    '''#"""#'''
    result = list(sequence)
    length = len(result)
    if length < 2:
        # No rearrangement is possible.
        return result
    def swap(a, b):
        if a != b:
            result[a], result[b] = result[b], result[a]
    swap(0, randrange(length))
    for n in range(1, length):
        previous = result[n-1][0]
        choices = [x for x in range(n, length) if result[x][0] != previous]
        if not choices:
            # Trapped in a corner: Too many of the same item are left.
            # Backtrack as far as necessary to interleave other items.
            minor = 0
            major = length - n
            while n > 0:
                n -= 1
                if result[n][0] == previous:
                    major += 1
                else:
                    minor += 1
                if minor == major - 1:
                    if n == 0 or result[n-1][0] != previous:
                        break
            else:
                # The requirement can't be fulfilled,
                # because there are too many of a single item.
                shuffle(result)
                break

            # Interleave the majority item with the other items.
            major = [item for item in result[n:] if item[0] == previous]
            minor = [item for item in result[n:] if item[0] != previous]
            shuffle(major)
            shuffle(minor)
            result[n] = major.pop(0)
            n += 1
            while n < length:
                result[n] = minor.pop(0)
                n += 1
                result[n] = major.pop(0)
                n += 1
            break
        swap(n, choice(choices))
    return result

Это начинается просто, но когда он обнаруживает, что он не может найти элемент с другим первым элементом, он выясняет, как далеко назад ему нужно идти, чтобы чередовать этот элемент с чем-то другим. Поэтому основной цикл пересекает массив не более трех раз (один раз назад), но обычно один раз. Конечно, каждая итерация первого прямого прохода проверяет каждый оставшийся элемент в массиве, а сам массив содержит каждую пару, поэтому общее время выполнения O((NM)**2).

Для вашей конкретной проблемы:

>>> X = Y = [1, 2, 3, 4, 5]
>>> combine(X, Y)
[(3, 5), (1, 1), (4, 4), (1, 2), (3, 4),
 (2, 3), (5, 4), (1, 5), (2, 4), (5, 5),
 (4, 1), (2, 2), (1, 4), (4, 2), (5, 2),
 (2, 1), (3, 3), (2, 5), (3, 2), (1, 3),
 (4, 3), (5, 3), (4, 5), (5, 1), (3, 1)]

Кстати, это сравнивает значения x по равенству, а не по положению в массиве X, что может иметь значение, если массив может содержать дубликаты. Фактически, повторяющиеся значения могут инициировать резервный случай перетасовки всех пар вместе, если более половины значений X одинаковы.