【问题标题】:Create a random order of (x, y) pairs, without repeating/subsequent x's创建 (x, y) 对的随机顺序,不重复/后续 x
【发布时间】:2016-04-06 15:05:59
【问题描述】:

假设我有一个有效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 值,然后再次继续完整集,这不是完全随机的。对于我的特定应用案例,我可以忍受。

【问题讨论】:

  • 为什么是list(xrange ... 而不是range,还有为什么不是while len(output)&lt;num_xy
  • 我现在明白了——您最初的问题对此并不十分清楚,尽管编辑说明了这一点。您是否希望所有此类排序以相同的概率出现?
  • len(X) 和 len(Y) 通常有多大?如果规模小,不要立即放弃简单、明显、可读和低效的解决方案。
  • @JohnColeman Aww,没有 BOGO 排序?这是我的答案。
  • 只是想感谢您提出这个问题。对于过渡到编程的工程师,到目前为止,我已经花了 2 个小时进行了 6 次尝试,试图通过 if 检查来绕过蛮力方面,几乎可以肯定是以一种被误导的方式,但我已经了解了更多关于编程的知识这个问题比其他任何问题都好:)

标签: python


【解决方案1】:

确保平均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

【讨论】:

  • 您的解决方案不是 O(N*M)。您有一个 M*N 迭代循环,其中是另一个循环最多迭代 M*N 次,然后是另一个 M*N 最坏情况 while 循环。当然,您通常会跳出那个内部循环,但最坏的情况仍然是 O(NM**2)。
  • 我平均提到过。根据我的测试,每次大约进行 2M*N 次交换。
【解决方案2】:

这是我的解决方案。首先,元组是在与先前选择的元组具有不同 x 值的元组中选择的。但是我注意到你必须为最后只有坏值元组放置的情况准备最后的技巧。

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

【讨论】:

  • 似乎工作,第一次发帖做得很好,欢迎堆栈溢出:)
  • 请注意,这仅适用于 Python 2,除非您将 list() 调用添加到 range() 调用。
  • 这里有一些令人担忧的性能缺陷。 sorted() 不需要;使用 [x for x in range(1, num_x + 1) for y in range(num_y)] 代替产生相同的输出。 all_xs.count() 必须遍历所有 all_xs 每次迭代;每 N 次 M 次迭代有 1/2 N 次循环。这使得这是一个 O(N**2)(二次)问题!
  • 啊,不管怎样,你也循环了所有的all_ind,所以即使没有list.count(),这也是一种二次方法。
【解决方案3】:

这是一个使用 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 notebook that explains how it works

在循环中,我们必须复制权重,将它们相加,然后使用权重选择一个随机索引。这些在n 中都是线性的。所以生成所有对的总体复杂度是O(n^2 m)

但运行时是确定性的,开销很低。而且我相当肯定它会以相同的概率生成所有合法序列。

【讨论】:

    【解决方案4】:

    一个有趣的问题!这是我的解决方案。它具有以下属性:

    • 如果没有有效的解决方案,它应该检测到这一点并通知您
    • 保证迭代终止,因此它永远不会陷入无限循环
    • 任何可能的解决方案都是以非零概率达到的

    我不知道输出在所有可能解决方案中的分布,但我认为它应该是均匀的,因为算法中没有明显的固有不对称性。不过,我会感到惊讶和高兴!

    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
    

    【讨论】:

    • 除非len(X) &lt; 2 总是有一个有效的解决方案,因为您可以简单地在第一个坐标中循环。如果您的算法陷入无限循环,也许可以使用某种回溯来跳出循环。
    • 如果存在有效的解决方案,我的算法是否可能陷入无限循环?我一眼看不到任何方式,但正如我所提到的,我也不是 100% 确定
    • 是的,理论上如果你运气不好,你可能会被卡住。你总是随机选择下一对,来地狱或高水位。因此,例如,如果机会之神决定总是“随机”选择具有相同 x 值的对,那么您只能插入第一对,然后您就被卡住了。显然,这不太可能,但在数学上是可能的。 (如果您使用依赖于this randomization function 的 Python 自定义构建,发生这种情况的可能性会更高。)
    • 算法的效率确实会随着xsys 的大小而变化。当 xs 只有 2 个元素并且 ys 非常大时,您的算法会花费大量时间“退回”它无法插入的对(并且有更多机会陷入无限循环,尽管这总是在现实世界中的机会微不足道)。
    • 哦,我根本不担心这种情况,因为它的概率等于 0。这对我来说就像“不可能”一样好。我更担心存在解决方案的可能性,该解决方案需要重新排序已添加到输出中的项目。如果在输出中的现有元素 [之后] 之前将一个元素添加到输出中,则它将永远保持在该顺序中的 [之后] 之前 - 即使其他元素被插入其中。我不相信这会破坏算法,但我无法证明这一点。
    【解决方案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)
    

    【讨论】:

    • 它可以在输出中产生重复对,这个想法是在这里准确地产生 25 个项目,没有重复和重复。
    • 这也可能产生无限循环,因为您会一遍又一遍地随机产生一个已经产生的对。
    【解决方案6】:

    为了完整起见,我想我会提出超级幼稚的“一直洗牌,直到你得到一个”的解决方案。它甚至不能保证终止,但如果它终止了,它会有很大程度的随机性,而且你确实说过一个理想的品质是简洁,这肯定是简洁的:

    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
    

    正如评论的那样,对于 xy 的小值(尤其是 y!),这实际上是一个相当快速的解决方案。您的每个 5 示例在“立即”的平均时间内完成。一副纸牌示例(4 和 13)可能需要更长的时间,因为它通常需要数十万次洗牌。 (同样,保证完全终止。)

    【讨论】:

    • 幸好product() 首先迭代ys,否则你的any() 条件在开始时永远不会为真,它甚至不会尝试洗牌! :-D
    • 但是,是的,有一个随机的机会,它永远不会终止。在某个地方,某人的机器将真正忙碌很长时间。
    • @MartijnPieters:关于product() 的工作原理:确实,如果您根本不关心随机性,并且真的只想要一个序列满足没有连续对具有相同@987654329 的要求@value,只需执行s = [(b, a) for (a, b) in product(y, x)] 并完全放弃循环是一种简洁快速完成的方法。 ;)
    • 这是“bogo”解决方案。如果 y 的大小增加而 x 保持较小,那么这个想法就会停止工作
    • @wim:我试图澄清这(1)不是一个“严肃”的解决方案,(2)除了小争论之外,它甚至不是一个可行的解决方案。对于我提到的纸牌示例,它已经几乎无法使用了。
    【解决方案7】:

    在您的输出中平均分配 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
    

    这会首先对每个xy 值进行洗牌,然后为每个x 提供一个 x, y 组合。 xs 的产生顺序在每次迭代时都会被打乱,您可以在其中测试限制。

    这是随机的,但您会在 x 位置获得 1 到 5 之间的所有数字,然后再看到相同的数字:

    >>> 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 组)。 总体而言,这可以根据您的限制对固定输入集进行很好的随机改组。

    它也很有效;因为您必须重新洗牌 x 订单的可能性只有 1 分之一N,所以在算法的完整运行期间,您平均应该只看到一次重新洗牌。因此,整个算法保持在 O(N*M) 范围内,非常适合产生 NM 输出元素的东西。因为在退回到简单的反向之前,我们最多将重新洗牌限制为 N 次,所以我们避免了(极不可能的)无休止重新洗牌的可能性。

    唯一的缺点是它必须预先创建 M y 值的 N 个副本。

    【讨论】:

    • 后半部分问题不大;这也是我解决方案的一部分。当然,这不是完全随机的,但我可以忍受这种特殊情况。 (事实上​​,对于我的应用程序,最好“均匀地”展开 x)
    • @slhck:那里有一个(非常非常非常小的)无限循环的机会。现在我只是颠倒了组的顺序(发生 N ** N 的一次机会)。
    【解决方案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) 变大,这将更难找到解决方案(并且旨在在这种情况下返回空列表),在这种情况下可以增加参数popSizenumGens。事实上,它能够非常快速地找到 20x20 的解决方案。 XY 的大小为 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
      

      这开始很简单,但是当它发现找不到具有不同第一个元素的项目时,它会计算出它需要多远才能将该元素与其他元素交错。因此,主循环最多遍历数组 3 次(向后一次),但通常只遍历一次。当然,第一次前向传递的每次迭代都会检查数组中的每个剩余项,并且数组本身包含每一对,因此总体运行时间为 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 值相同,重复值可能会触发将所有对混在一起的后备情况。

      【讨论】:

        猜你喜欢
        • 2020-03-27
        • 1970-01-01
        • 1970-01-01
        • 2013-11-09
        • 1970-01-01
        • 2012-01-04
        • 2021-12-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多