给定一个数字列表,找到所有矩阵,使得每列和每行总和为 264

Posted

技术标签:

【中文标题】给定一个数字列表,找到所有矩阵,使得每列和每行总和为 264【英文标题】:Given a list of numbers, find all matrices such that each column and row sum up to 264 【发布时间】:2020-04-13 09:46:54 【问题描述】:

假设我有一个包含 16 个数字的列表。使用这 16 个数字,我可以创建不同的 4x4 矩阵。我想找到所有 4x4 矩阵,其中列表中的每个元素都使用一次,并且每行和每列的总和等于 264。

首先我找到列表中所有元素的组合,总和为 264

numbers = [11, 16, 18, 19, 61, 66, 68, 69, 81, 86, 88, 89, 91, 96, 98, 99]

candidates = []
result = [x for x in itertools.combinations(numbers, 4) if sum(x) == 264]

result 成为一个列表,其中每个元素都是一个包含 4 个元素的列表,其中 4 个元素的总和 = 264。我认为这些是我的行。然后我想对我的行进行所有排列,因为加法是可交换的。

for i in range(0, len(result)):
    candidates.append(list(itertools.permutations(result[i])))

现在给出总和为 264 的所有可能行。我想选择 4 行的所有组合,这样每列的总和都是 264。

test = []
for i in range(0, len(candidates)):
    test = test + candidates[i]
result2 = [x for x in itertools.combinations(test, 4) if list(map(add, x[0], list(map(add, x[1], list( map(add, x[2], x[3])))))) == [264, 264, 264, 264]]

有更快/更好的方法吗?最后一部分,查找 4 行的所有组合,需要大量时间和计算机能力。

【问题讨论】:

您正确地说的result 不是所有的行,也是所有的列?很确定你可以用它做点什么,但我现在看不到。 @blueteeth 在某种意义上是的。但是如果 a+b+c+d=264,那么 b+a+c+d=264 也是如此,因此我需要所有排列。所以实际上候选人拥有所有的行和列及其所有排列。那么有没有一种聪明的方法可以从候选人中选择元素......也看不到它 对于其他想知道的人来说,有 30 个组合加起来为 264,而 30^4 是要检查的 810,000 个排列。 【参考方案1】:

这是constraint satisfaction problem的一种;有 16 个变量,每个变量都具有相同的域,8 个关于它们的总和的约束,以及一个约束,它们都应该具有与域不同的值。

可能存在大量的解决方案,因此任何生成更大的候选集然后检查哪些候选确实是解决方案的算法在很大程度上可能是低效的,因为真正的解决方案的比例可能非常低你的候选人。 backtracking search 通常更好,因为它允许部分候选者在违反任何约束时被拒绝,可能会消除许多完整的候选者,而不必首先生成它们。

您可以使用现有的约束求解器,例如python-constraint library,而不是编写自己的回溯搜索算法。这是一个例子:

numbers = [11, 16, 18, 19, 61, 66, 68, 69, 81, 86, 88, 89, 91, 96, 98, 99]
target = 264

from constraint import *

problem = Problem()
problem.addVariables(range(16), numbers)

for i in range(4):
    # column i
    v = [ i + 4*j for j in range(4) ]
    problem.addConstraint(ExactSumConstraint(target), v)
    # row i
    v = [ 4*i + j for j in range(4) ]
    problem.addConstraint(ExactSumConstraint(target), v)

problem.addConstraint(AllDifferentConstraint())

例子:

>>> problem.getSolution()
0: 99, 1: 88, 2: 66, 3: 11, 4: 16, 5: 61, 6: 89, 7: 98, 8: 81, 9: 96, 10: 18, 11: 69, 12: 68, 13: 19, 14: 91, 15: 86
>>> import itertools
>>> for s in itertools.islice(problem.getSolutionIter(), 10):
...     print(s)
... 
0: 99, 1: 68, 2: 81, 3: 16, 4: 66, 5: 91, 6: 18, 7: 89, 8: 88, 9: 19, 10: 96, 11: 61, 12: 11, 13: 86, 14: 69, 15: 98
0: 99, 1: 68, 2: 81, 3: 16, 4: 66, 5: 91, 6: 18, 7: 89, 8: 11, 9: 86, 10: 69, 11: 98, 12: 88, 13: 19, 14: 96, 15: 61
0: 99, 1: 68, 2: 81, 3: 16, 4: 18, 5: 89, 6: 66, 7: 91, 8: 86, 9: 11, 10: 98, 11: 69, 12: 61, 13: 96, 14: 19, 15: 88
0: 99, 1: 68, 2: 81, 3: 16, 4: 18, 5: 89, 6: 66, 7: 91, 8: 61, 9: 96, 10: 19, 11: 88, 12: 86, 13: 11, 14: 98, 15: 69
0: 99, 1: 68, 2: 81, 3: 16, 4: 11, 5: 86, 6: 69, 7: 98, 8: 66, 9: 91, 10: 18, 11: 89, 12: 88, 13: 19, 14: 96, 15: 61
0: 99, 1: 68, 2: 81, 3: 16, 4: 11, 5: 86, 6: 69, 7: 98, 8: 88, 9: 19, 10: 96, 11: 61, 12: 66, 13: 91, 14: 18, 15: 89
0: 99, 1: 68, 2: 81, 3: 16, 4: 61, 5: 96, 6: 19, 7: 88, 8: 18, 9: 89, 10: 66, 11: 91, 12: 86, 13: 11, 14: 98, 15: 69
0: 99, 1: 68, 2: 81, 3: 16, 4: 61, 5: 96, 6: 19, 7: 88, 8: 86, 9: 11, 10: 98, 11: 69, 12: 18, 13: 89, 14: 66, 15: 91
0: 99, 1: 68, 2: 81, 3: 16, 4: 88, 5: 19, 6: 96, 7: 61, 8: 11, 9: 86, 10: 69, 11: 98, 12: 66, 13: 91, 14: 18, 15: 89
0: 99, 1: 68, 2: 81, 3: 16, 4: 88, 5: 19, 6: 96, 7: 61, 8: 66, 9: 91, 10: 18, 11: 89, 12: 11, 13: 86, 14: 69, 15: 98

这是前十个解决方案。 problem.getSolutions() 方法返回一个包含所有它们的列表,但这需要相当长的时间来运行(在我的机器上大约需要 2 分钟),因为要找到其中的 6,912 个。

一个问题是每个解决方案都有许多对称的对应物;您可以置换行,置换列,并进行转置。可以通过添加更多约束来消除对称性,这样您就可以从每个对称类中获得一个解决方案。这使得搜索更加可行:

# permute rows/cols so that lowest element is in top-left corner
m = min(numbers)
problem.addConstraint(InSetConstraint([m]), [0])

from operator import lt as less_than

for i in range(3):
    # permute columns so first row is in order
    problem.addConstraint(less_than, [i, i+1])
    # permute rows so first column is in order
    problem.addConstraint(less_than, [4*i, 4*i + 4])

# break transpose symmetry by requiring grid[0,1] < grid[1,0]
problem.addConstraint(less_than, [1, 4])

这会破坏所有对称性,因此现在它会在大约 0.2 秒内返回 6,912 / (4! * 4! * 2) = 6 个解。

【讨论】:

@StefanPochmann 这 7 个约束不允许行排列、列排列和镜像对称。这个问题是否存在其他对称性? @StefanPochmann 对称群的大小为 4! * 4! * 2,并且对称约束精确地减少了该因子的解数,因此它不能从任何对称类中输出多个解。还是您的意思是更一般的证明?您的第一个解决方案不遵守“最小数字在左上角”约束。 @StefanPochmann 所以,你的意思是 0 元素也应该固定(到 11)以避免它在网格中的其他地方结束? @JohanC 分别破坏行和列的对称性并不意味着所有的对称性都必然被破坏;只是剩下的对称组不包括任何只作用于行或只作用于列的对称。仍然可能存在作用于行和列的对称性,需要“左上角的最小值”约束来消除这些对称性。我认为对于所有数字都不同的问题,这些约束应该打破所有对称性,但我不能 100% 确定这一点。打破 CSP 中的对称性非常困难。 好吧,我现在相信了。给定任何解决方案,将最小的数字排列到左上角也决定了第一行中还有什么,因此您的第一行约束决定了列排列。与行排列相同。【参考方案2】:

这是一种使用z3py 的方法,Python 的Z3 SAT/SMT solver 版本。请注意,行和/或列的每个排列以及镜像都提供了一个额外的解决方案。每个原始解决方案共同导致 24*24*2 等效解决方案。

添加约束来强制排序,应该能够找到所有原始解决方案。如果没有错误,下面的程序会找到所有 6 个错误。所以,总共应该有 6*24*24*2 = 6912 个解决方案。

from z3 import Solver, BitVec, Or, Distinct, sat

numbers = [11, 16, 18, 19, 61, 66, 68, 69, 81, 86, 88, 89, 91, 96, 98, 99]

# X is a table to store the 16 variables for the solution
X = [BitVec(f'xij', 16) for i in range(4) for j in range(4)]
s = Solver()
for x in X:
    s.add(Or([x == n for n in numbers]))  # all X[i] should be one of the given numbers

# constraints to avoid reordered solutions
s.add(X[0] == 11)
s.add(X[0] < X[1])
s.add(X[1] < X[2])
s.add(X[2] < X[3])
s.add(X[1] < X[4])
s.add(X[4] < X[8])
s.add(X[8] < X[12])

# all X[i] have to be distinct
s.add(Distinct(X))
for i in range(4):
    # all rows and all columns need to sum to 264
    s.add(sum([X[4*i+j] for j in range(4)]) == 264)
    s.add(sum([X[4*j+i] for j in range(4)]) == 264)

# start solving
res = s.check()

while res == sat:
    m = s.model()
    # show the solution
    for i in range(4):
        print([m[X[i*4+j]] for j in range(4)])
    print()

    # add the just found solution as a constraint so it doesn't get outputted again
    s.add(Or([X[i] != m[X[i]].as_long() for i in range(16)]))

    # solve again to find different solutions
    res = s.check()

输出:

[11, 68, 89, 96]
[69, 16, 91, 88]
[86, 99, 18, 61]
[98, 81, 66, 19]

[11, 68, 86, 99]
[69, 16, 98, 81]
[88, 91, 19, 66]
[96, 89, 61, 18]

[11, 66, 89, 98]
[69, 18, 91, 86]
[88, 99, 16, 61]
[96, 81, 68, 19]

[11, 66, 88, 99]
[68, 19, 91, 86]
[89, 98, 16, 61]
[96, 81, 69, 18]

[11, 66, 88, 99]
[69, 18, 96, 81]
[86, 91, 19, 68]
[98, 89, 61, 16]

[11, 66, 89, 98]
[68, 19, 96, 81]
[86, 91, 18, 69]
[99, 88, 61, 16]

【讨论】:

大约需要1.8秒 确认一下,我也得到了六个解决方案。

以上是关于给定一个数字列表,找到所有矩阵,使得每列和每行总和为 264的主要内容,如果未能解决你的问题,请参考以下文章

Python:查找所有 6x6 矩阵,其中每个值在每列和每行中仅出现一次

如何获得二维数组中每一列和每一行的总和?

ACM Sudoku

下面的方阵图中,每行、每列、每条对角线上的3个数的和相等

给定一个数字列表,如何创建总和的所有组合并返回这些总和的列表

[VijosP1764]Dual Matrices 题解