如何进一步优化“Lights Out”变体的求解器?

时间:2014-12-12 03:15:40

标签: python performance

我仍在解决this problem,取自目前的#34; Google Foobar"挑战。它是" Lights Out"的变体。游戏中,按下一个灯将翻转同一行和同一列上每个灯的状态。

我之前尝试using a BFS,结果证明对于n>来说太慢了6,虽然我需要处理2< n< 16.我目前有一个程序可以处理除13和15之外的所有偶数n和所有奇数。这就是它的作用:

  1. 我使用@Aryabhata概述的strategy找到一个特殊的解决方案x'某些系统Ax = b可以与此问题的实例相关联(有关详细信息,请参阅here)。

  2. 找到A的零空格的base后,我计算了x'的所有总和。加上基数向量的线性组合。

  3. 这些总和的集合是原始问题的所有解决方案的集合,因此我可以通过蛮力找到达到最小值的解决方案。

  4. 应该注意的是,对于n even,零空间是空的(A是可逆的),因此x'实现最小化,因为它是唯一的解决方案。如果n为奇数,则零空间的基数中的向量数为2n-2,因此搜索空间的大小为2 ^(2n-2),在最坏的情况下为2 ^ 28(n = 15)。 / p>

  5. 这是我的计划:

    from itertools import product
    
    
    MEMO = {}
    
    
    def bits(iterable):
        bit = 1
        res = 0
        for elem in iterable:
            if elem:
                res |= bit
            bit <<= 1
        return res
    
    
    def mask(current, n):
        if (current, n) in MEMO:
            return MEMO[(current, n)]
    
        result = 0
        if current < n:
            for j in xrange(n):
                result += (2 ** ((current - 1)*n + j) + 2 ** (current*n + j))
        else:
            for i in xrange(n):
                result += (2 ** (i*n + current - n) + 2 ** (i*n + current - n + 1))
    
        MEMO[(current, n)] = result
    
        return result
    
    
    # See: https://math.stackexchange.com/a/441697/4471
    def check(matrix, n):
        parities = [sum(row) % 2 for row in matrix]
        for i in xrange(n):
            parities.append(sum([row[i] for row in matrix]) % 2)
    
        return len(set(parities)) == 1
    
    
    def minimize(matrix, current, n):
        if current == 0:
            # See: https://stackoverflow.com/a/9831671/374865
            return bin(matrix).count("1")
        else:
            return min(minimize(matrix ^ mask(current, n), current - 1, n),
                       minimize(matrix, current - 1, n))
    
    
    def solve(matrix, n):
        result = [0 for i in xrange(n) for j in xrange(n)]
    
        for i, j in product(xrange(n), repeat=2):
            if matrix[i][j]:
                for k in xrange(n):
                    result[i*n + k] ^= 1
                    result[k*n + j] ^= 1
                result[i*n + j] ^= 1
    
        if n % 2 == 0:
            return sum(result)
        else:
            return minimize(bits(result), 2*n - 2, n)
    
    
    def answer(matrix):
        n = len(matrix)
    
        if n % 2 == 0:
            return solve(matrix, n)
        else:
            if check(matrix, n):
                return solve(matrix, n)
            else:
                return -1
    

    我已经尝试过优化它:例如,函数bits将矩阵编码为二进制数,而函数mask创建用于添加单个元素的二进制掩码的基础到x&#39;。这些面具也被记忆,因为它们经常被使用,所以它们只计算一次。

    然后使用成语bin(n).count('1')计算1的数量,其中should be是最快的实现(我将其与Kernighan的经典实例进行了对比)。

    那么,我还能做些什么来从我的程序中挤出更多性能?以下是一些测试用例:

    print answer([
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    ]), 1
    
    print answer([
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
    ]), 14
    
    print answer([
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
    ]), 15
    
    print answer([
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    ]), 14
    
    print answer([
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]), 15
    

    编辑:我通过了这一轮。这个实现正确地解决了5个测试用例中的4个,然后我强行强迫第五个。我仍然对进一步的优化或不同的算法感兴趣!

    编辑2: This answer,特别是this paper,证明这个特殊问题是NP难的(第3节),暗示我们不应该&#39 ;寻找多项式算法。所以问题就变成了:&#34;我们能得到的最佳指数是什么?&#34;。

3 个答案:

答案 0 :(得分:4)

我尝试了关于线性代数的一切,因为它是GF2,我认为我不能找到多项式解。由于最大数量为15,我进一步将其优化为大约2 ^ 15。

对于偶数

因此,对于n是偶数,有一种比标准线性代数更快的方法。如果您有这样的事情, 0000 0100 0000 0000 一个解决方案应该是(正好翻转点的行和列n次) 0100 1111 0100 0100 如果你考虑一下,如果你有一个想要翻转的点,你可以翻转行和列的每一个点。 (如果这有意义),那么很容易找到一个特定的解决方案。

如果我有这样的话 0100 0010 0010 0000 一个解决方案可能是 1131 1221 1221 0120 并且由于翻转两次没有区别,因此解决方案可以减少到 1111 1001 1001 0100

然后是奇数

如果n是奇数,我除了搜索之外什么都不会想。但是,我们可以扩展n - > n + 1使得问题的解决方案不应包含最后一行和最后一列的翻转点。

如果你有3x3喜欢的东西: 010 001 001 您可以随时尝试将解决方案扩展为: 010x 001x 001x xxxx 首先,您将确定3乘3中的所有点, 1111 1001 + ? 1001 0100 哪里?应该是解决方案 000x 000x 000x xxxx 正如您所看到的,无论如何翻转,除非xxx是相同的位,否则无法满足。并且您可以尝试底部翻转的所有组合,然后您可以通过确定翻转结果是否最小数量为1行来确定右侧是否翻转。

我很难解释事情,希望它足够清楚。

答案 1 :(得分:2)

我想回应一下darwinsenior的回答非常有帮助!然而,即使在多次阅读该答案之后,我花了很长时间才弄明白。

所以,如果你像我一样迟到foobar,但想要通过这个而不诉诸Java,这里有一个可能有帮助的提示。

以下光照模式无法解决,我认为这让我很困惑。

010
001
001

这是展示darwinsenior想法的一个非常重要的例子: 假设你要解决这个问题(N = 5)

11010
01000
11100
10011
00010

我们知道这是可以解决的,因为所有和和列的奇偶校验都是奇数。

如果N是偶数,那么找到答案会更容易。 因此,扩展到N = 6如下:

110100
010000
111000
100110
000100
000000

就像darwinsenior所说,我们想要一个解决方案,不触及底行或最右列中的任何灯光。然后我们可以采用该解决方案,忽略底行和右列,我们可以找到原始N = 5问题的解决方案。因此,我们希望插入值(不仅仅是零),而不是在答案的那些列中按任何按钮。

这意味着您无法在右下方放置1。右下角的灯表示在底行或最右列中至少按下一个按钮。这就是一个“自由度”消失了。接下来,对于没有按钮按下的底行,所有这些灯必须具有偶校验。看看甚至N案的答案,看看为什么。所以在上面的情况下,奇偶校验是奇数。我们可以填充底行,但我们必须使用奇数1。这消除了另一个“自由度”。如果我们插入4个值(1或0),则第5个值由此奇偶校验要求确定。所以,这里有N-1个自由度。

这是蛮力部分的来源。我必须在这里尝试所有可能的值(在这种情况下,所有5位奇数奇偶校验集) 一个例子是插入10101

110100
010000
111000
100110
000100
10101_

现在我们可以将规则用于偶数N并获得解决方案。 我会记下每个点的行和列的实际总和,即使只需要奇偶校验,以便更清楚我所做的。

65555o      01111o
53343o      11101o
65465o   -> 01001o
66554o      00110o
54333o      10111o
66464_      00000_

我在最右边说了一点,说奇偶校验是奇怪的,因为我们还没有做过任何事情。因为奇偶校验是奇数,这是不好的,我们将有一个解决方案,所有这些都被触及。但它们都有奇校验,所以我们只需要插入值,使得最右列的奇偶校验为奇数,因此每个点的奇偶校验是偶数(如果这是有意义的)

这就是darwinsenior在上面的评论中所说的(但我很难跟进)唯一的要求是列的奇偶校验是奇数,因此最右边的按钮不需要在解决方案中推送。 我们不需要暴力破解,我们可以使用一些逻辑来确定在保持奇偶校验要求的同时推送哪些按钮。顺便说一下,我们这里有N-1个自由变量,因此2 *(N-1)个自由变量,就像在其他解决方案中提到的那样。就在这里,我们可以看到我们的选择对按钮推动计数的影响。我将为列选择这些值:11001 现在的例子是:

110101                                    X00000 
010001                                    000X00 
111000   -- again use even N solution ->  0X00X0 
100110                                    00XX00 
000101                                    0X0000 
10101_                                    000000 

所以,我认为这给了我们原始N = 5案例的答案(只需删除底部和右侧的零)。它有7个按钮按下,我认为这是我们用这个按钮最好的,但我不确定。

还有一件事 - 即使需要粗暴强制的案件数量大幅度减少,我仍然必须做Eugene所说的并使用一个整数列表,而不是一组整数列表。看看Jacopo的代码和“位”功能。有趣的东西。

答案 2 :(得分:0)

所以我认为你根本不需要粗暴地对待奇怪的情况。我的线性不是太强,但是在R ^ n中,如果你想找到满足Ax = b的最短x(这基本上就是我们正在做的),在找到一些特殊解x'之后你可以投射到nullspace A和从x'中减去投影。我相信这种方法即使在F_2也应该有效,尽管我不确定;如果我错了,请纠正我。