生成拉丁方块的有效方法(或在两个轴上唯一地在矩阵中随机置换数字 - 使用NumPy)

时间:2018-04-02 06:33:32

标签: python numpy scipy

例如,如果有5个数字1,2,3,4,5

我想要一个像

这样的随机结果
[[ 2, 3, 1, 4, 5]
 [ 5, 1, 2, 3, 4]
 [ 3, 2, 4, 5, 1]
 [ 1, 4, 5, 2, 3]
 [ 4, 5, 3, 1, 2]]

确保其行和列中的每个数字都是唯一的。

有一种有效的方法吗?

我尝试使用while循环为每次迭代生成一行,但似乎效率不高。

import numpy as np

numbers = list(range(1,6))
result = np.zeros((5,5), dtype='int32')
row_index = 0
while row_index < 5:
    np.random.shuffle(numbers)
    for column_index, number in enumerate(numbers):
       if number in result[:, column_index]:
           break
       else:
           result[row_index, :] = numbers
           row_index += 1

5 个答案:

答案 0 :(得分:4)

仅为了您的信息,您正在寻找的是一种生成latin squares的方法。 至于解决方案,它取决于随机“随机”对你有多少。

我会设计至少四种主要技术,其中两项已经提出。 因此,我将简要介绍另外两个:

  1. 循环遍历项目的所有可能排列并接受第一个满足行中的unicity约束的项目
  2. 仅使用循环排列来构建后续行:这些是通过构造满足沿行的单一性约束(循环变换可以向前或向后完成);为了改善“随机性”,行可以改组
  3. 假设我们使用标准的Python数据类型,因为我没有看到使用NumPy的真正优点(但如果需要,结果可以很容易地转换为np.ndarray),这将在代码中(第一个函数只是检查解决方案是否真的正确):

    import random
    import math
    import itertools
    
    # this only works for Iterable[Iterable]
    def is_latin_rectangle(rows):
        valid = True
        for row in rows:
            if len(set(row)) < len(row):
                valid = False
        if valid and rows:
            for i, val in enumerate(rows[0]):
                col = [row[i] for row in rows]
                if len(set(col)) < len(col):
                    valid = False
                    break
        return valid
    
    def is_latin_square(rows):
        return is_latin_rectangle(rows) and len(rows) == len(rows[0])
    
    # : prepare the input
    n = 9
    items = list(range(1, n + 1))
    # shuffle items
    random.shuffle(items)
    # number of permutations
    print(math.factorial(n))
    
    
    def latin_square1(items, shuffle=True):
        result = []
        for elems in itertools.permutations(items):
            valid = True
            for i, elem in enumerate(elems):
                orthogonals = [x[i] for x in result] + [elem]
                if len(set(orthogonals)) < len(orthogonals):
                    valid = False
                    break
            if valid:
                result.append(elems)
        if shuffle:
            random.shuffle(result)
        return result
    
    rows1 = latin_square1(items)
    for row in rows1:
        print(row)
    print(is_latin_square(rows1))
    
    
    def latin_square2(items, shuffle=True, forward=False):
        sign = -1 if forward else 1
        result = [items[sign * i:] + items[:sign * i] for i in range(len(items))]
        if shuffle:
            random.shuffle(result)
        return result
    
    rows2 = latin_square2(items)
    for row in rows2:
        print(row)
    print(is_latin_square(rows2))
    
    rows2b = latin_square2(items, False)
    for row in rows2b:
        print(row)
    print(is_latin_square(rows2b))
    

    为了进行比较,还提出了通过尝试随机排列和接受有效排列(基本上是@hpaulj提出的建议)的实现。

    def latin_square3(items):
        result = [list(items)]
        while len(result) < len(items):
            new_row = list(items)
            random.shuffle(new_row)
            result.append(new_row)
            if not is_latin_rectangle(result):
                result = result[:-1]
        return result
    
    rows3 = latin_square3(items)
    for row in rows3:
        print(row)
    print(is_latin_square(rows3))
    

    我没有时间(还)实现其他方法(从@ConfusedByCode回溯类似Sudoku的解决方案)。

    n = 5的时间安排:

    %timeit latin_square1(items)
    321 µs ± 24.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    
    %timeit latin_square2(items)
    7.5 µs ± 222 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    
    %timeit latin_square2(items, False)
    2.21 µs ± 69.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    
    %timeit latin_square3(items)
    2.15 ms ± 102 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    

    ...以及n = 9

    %timeit latin_square1(items)
    895 ms ± 18.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
    %timeit latin_square2(items)
    12.5 µs ± 200 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    
    %timeit latin_square2(items, False)
    3.55 µs ± 55.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    
    %timeit latin_square3(items)
    The slowest run took 36.54 times longer than the fastest. This could mean that an intermediate result is being cached.
    9.76 s ± 9.23 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
    

    因此,解决方案1给出了相当大的随机性,但速度并不快(并且使用O(n!)缩放),解决方案2(和2b)要快得多(使用O(n)缩放)但是不像解决方案1那样随机。解决方案3非常慢并且性能可能会有很大差异(可能通过计算最后一次迭代而不是猜测来加速)。

    在下面讨论了更多技术性的,其他有效的算法:

答案 1 :(得分:2)

这可能看起来很奇怪,但你基本上已经描述了生成一个随机的n维数独谜题。来自a blog post by Daniel Beer

  

解决数独谜题的基本方法是通过回溯搜索每个细胞的候选值。一般程序如下:

     
      
  1. 为每个单元格生成候选值列表,方法是从所有可能值的集合开始,并消除与正在检查的单元格出现在同一行,列和框中的值。

    < / LI>   
  2. 选择一个空单元格。如果没有,拼图就解决了。

  3.   
  4. 如果单元格没有候选值,则难题无法解决。

  5.   
  6. 对于该单元格中的每个候选值,将值放在单元格中并尝试​​以递归方式解决难题。

  7.         

    有两种优化可以大大提高此算法的性能:

         
        
    1. 选择单元格时,请始终选择候选值最小的单元格。这降低了分支因子。随着值添加到网格中,其他单元格的候选数量也会减少。

    2.   
    3. 在分析空单元格的候选值时,从上一步骤的分析开始并通过删除最后修改单元格的行,列和框中的值来更快地修改它。这是拼图大小的O(N),而从头开始分析是O(N3)。

    4.   

在您的情况下,“无法解决的难题”是无效的矩阵。在可解决的难题中,矩阵中的每个元素在两个轴上都是唯一的。

答案 2 :(得分:1)

编辑:以下是norok2答案中第二个解决方案的实现。

编辑:我们可以再次对生成的方块进行洗牌,使其真正随机。 所以求解函数可以修改为:

def solve(numbers):
    shuffle(numbers)
    shift = randint(1, len(numbers)-1)
    res = []

    for r in xrange(len(numbers)):
        res.append(list(numbers))
        numbers = list(numbers[shift:] + numbers[0:shift])

    rows = range(len(numbers))
    shuffle(rows)

    shuffled_res = []
    for i in xrange(len(rows)):
        shuffled_res.append(res[rows[i]])

    return shuffled_res
编辑:我以前误解了这个问题。 所以,这里有一个快速的&#39;产生“某种程度”的方法。随机解决方案 基本的想法是,

    a, b, c
    b, c, a
    c, a, b

我们可以通过固定步骤移动一行数据以形成下一行。这符合我们的限制。

所以,这是代码:

from random import shuffle, randint


def solve(numbers):
    shuffle(numbers)
    shift = randint(1, len(numbers)-1)
    res = []

    for r in xrange(len(numbers)):
        res.append(list(numbers))
        numbers = list(numbers[shift:] + numbers[0:shift])

    return res


def check(arr):
    for c in xrange(len(arr)):
        col = [arr[r][c] for r in xrange(len(arr))]
        if len(set(col)) != len(col):
            return False
    return True


if __name__ == '__main__':
    from pprint import pprint
    res = solve(range(5))
    pprint(res)
    print check(res)

这是itertools可能的解决方案,如果你不坚持使用我不熟悉的numpy:

import itertools
from random import randint
list(itertools.permutations(range(1, 6)))[randint(0, len(range(1, 6))]

# itertools returns a iterator of all possible permutations of the given list.

答案 3 :(得分:1)

我尝试了一种蛮力的随机选择。生成一行,如果有效,则添加到累计行:

def foo(n=5,maxi=200):
    arr = np.random.choice(numbers,n, replace=False)[None,:]
    for i in range(maxi):
        row = np.random.choice(numbers,n, replace=False)[None,:]
        if (arr==row).any(): continue
        arr = np.concatenate((arr, row),axis=0)
        if arr.shape[0]==n: break
    print(i)
    return arr

一些示例运行:

In [66]: print(foo())
199
[[1 5 4 2 3]
 [4 1 5 3 2]
 [5 3 2 1 4]
 [2 4 3 5 1]]
In [67]: print(foo())
100
[[4 2 3 1 5]
 [1 4 5 3 2]
 [5 1 2 4 3]
 [3 5 1 2 4]
 [2 3 4 5 1]]
In [68]: print(foo())
57
[[1 4 5 3 2]
 [2 1 3 4 5]
 [3 5 4 2 1]
 [5 3 2 1 4]
 [4 2 1 5 3]]
In [69]: print(foo())
174
[[2 1 5 4 3]
 [3 4 1 2 5]
 [1 3 2 5 4]
 [4 5 3 1 2]
 [5 2 4 3 1]]
In [76]: print(foo())
41
[[3 4 5 1 2]
 [1 5 2 3 4]
 [5 2 3 4 1]
 [2 1 4 5 3]
 [4 3 1 2 5]]

所需的尝试次数在各地都有所不同,有些超过了我的迭代限制。

在没有进入任何理论的情况下,快速生成2d排列与生成某种意义上的最大排列或最大随机排序之间存在差异。我怀疑我的方法更接近这个随机目标,而不是更系统和有效的方法(但我无法证明)。

def opFoo():
    numbers = list(range(1,6))
    result = np.zeros((5,5), dtype='int32')
    row_index = 0; i = 0
    while row_index < 5:
        np.random.shuffle(numbers)
        for column_index, number in enumerate(numbers):
            if number in result[:, column_index]:
                break
            else:
                result[row_index, :] = numbers
                row_index += 1
        i += 1
    return i, result

In [125]: opFoo()
Out[125]: 
(11, array([[2, 3, 1, 5, 4],
        [4, 5, 1, 2, 3],
        [3, 1, 2, 4, 5],
        [1, 3, 5, 4, 2],
        [5, 3, 4, 2, 1]]))

我的速度比OP快一点,但我的是正确的。

这是对我的改进(快2倍):

def foo1(n=5,maxi=300):
    numbers = np.arange(1,n+1)
    np.random.shuffle(numbers)
    arr = numbers.copy()[None,:]
    for i in range(maxi):
        np.random.shuffle(numbers)
        if (arr==numbers).any(): continue
        arr = np.concatenate((arr, numbers[None,:]),axis=0)
        if arr.shape[0]==n: break
    return arr, i

Why is translated Sudoku solver slower than original?

我发现使用Java Sudoku解算器的这个翻译,使用Python列表比numpy数组更快。

我可能会尝试让这个脚本适应这个问题 - 明天。

答案 4 :(得分:0)

无法从手机输入代码,这是伪代码:

  1. 创建一个矩阵,其中一个diamention超过tge目标矩阵(3 d)

  2. 使用1到5的数字初始化25个元素

  3. 迭代25个元素。

  4. 从元素列表中选择第一个元素的随机值(包含数字1到5)

  5. 从行和列中的所有元素中删除随机选择的值。

  6. 对所有元素重复步骤4和5。