Hammerwatch 谜题:用 Python 解题

4 投票
3 回答
3103 浏览
提问于 2025-04-17 21:24

我妻子在Steam上玩Hammerwatch的时候,遇到了一个我决定尝试编程解决的谜题。

这个谜题的玩法是:
激活一个开关会让这个开关要么打开,要么关闭,同时也会影响到它旁边的开关。

这里有个YouTube视频展示了游戏中的谜题:
http://www.youtube.com/watch?v=OM1XD7IZ0cg


我弄明白了这个谜题的机制。后来我意识到,我有两种选择来让电脑解决这个问题:

A) 让电脑随机选择开关来解决
...或者...
B) 创建一个算法,让电脑更有效率地解决这个谜题。

作为一个新手程序员(我正在学习CodeAcademy的教程,正在进行《Learn Python the Hard Way》,还在MIT edX的计算机科学Python课程中),我觉得自己在解决这个问题上有点力不从心。我已经开始学习了!请帮帮我!

我需要帮助:

我需要帮助,想要找到一种更好的随机解决方法,或者更好的是,创建一个算法,让电脑能够系统性地解决这个谜题。

我唯一想到的就是让电脑存储谜题的状态在一个列表或字典里,这样程序就可以跳过那些已存储的状态,指向新的可能解决方案。

当前程序的工作方式:

我打算让用户通过前9个输入来输入当前谜题板的状态。然后程序进入一个循环,随机切换谜题板上的开关,直到所有开关都打开。

附言:在我注册StackOverflow账号和输入这条信息的时候,我的电脑在后台运行这个程序来寻找解决方案。已经快一个小时了,还是没找到解决方案,目前已经进行了大约9200万次迭代。我觉得这可能不太行……

import random

def switcheroo(x):
    """
    switches 'x' to 1 if it's a 0 and vice-versa
    """
    if x == 0:
        x = 1
    else:
        x = 0
    return x

# original input variables
a1 = 0
a2 = 0
a3 = 0
b1 = 0
b2 = 0
b3 = 0
c1 = 0
c2 = 0
c3 = 0


# puzzleboard   
print "\n\n"
print "    1   2   3   "
print "  -------------"
print "a |",a1,"|",a2,"|",a3,"|"
print "  -------------"
print "b |",b1,"|",b2,"|",b3,"|"
print "  -------------"
print "c |",c1,"|",c2,"|",c3,"|"
print "  -------------"
print "\n\n"



print "What's ON/OFF? (type 0 for OFF, 1 for ON)"
a1 = int(raw_input("a1: "))
a2 = int(raw_input("a2: "))
a3 = int(raw_input("a3: "))
b1 = int(raw_input("b1: "))
b2 = int(raw_input("b2: "))
b3 = int(raw_input("b3: "))
c1 = int(raw_input("c1: "))
c2 = int(raw_input("c2: "))
c3 = int(raw_input("c3: "))

# for counting the iterations within the loop
iteration = 0

# to stop loop if all switches are ON
ans = a1 and a2 and a3 and b1 and b2 and b3 and c1 and c2 and c3


while ans == False:
    # randomly generates number, flipping random switches
    counter = random.randint(1,9)
    if counter == 1:
        switch = "a1"
    elif counter == 2:
        switch = "a2"
    elif counter == 3:
        switch = "a3"
    elif counter == 4:
        switch = "b1"
    elif counter == 5:
        switch = "b2"
    elif counter == 6:
        switch = "b3"
    elif counter == 7:
        switch = "c1"
    elif counter == 8:
        switch = "c2"
    elif counter == 9:
        switch = "c9"


    # PUZZLE MECHANICES #
    if switch == "a1":
        a1 = switcheroo(a1)
        a2 = switcheroo(a2)
        b1 = switcheroo(b1)

    if switch == "a2":
        a2 = switcheroo(a2)
        a1 = switcheroo(a1)
        a3 = switcheroo(a3)
        b2 = switcheroo(b2)        

    if switch == "a3":
        a3 = switcheroo(a3)
        a2 = switcheroo(a2)
        b3 = switcheroo(b3)

    if switch == "b1":
        b1 = switcheroo(b1)
        b2 = switcheroo(b2)
        a1 = switcheroo(a1)
        c1 = switcheroo(c1)

    if switch == "b2":
        b2 = switcheroo(b2)
        a2 = switcheroo(a2)
        b1 = switcheroo(b1)
        b3 = switcheroo(b3)
        c2 = switcheroo(c2)

    if switch == "b3":
        b3 = switcheroo(b3)
        b1 = switcheroo(b1)
        b2 = switcheroo(b2)
        c3 = switcheroo(c3)
    # Edit 1
    if switch == "c1":
        c1 = switcheroo(c1)
        c2 = switcheroo(c2)
        b1 = switcheroo(b1)

    if switch == "c2":
        c2 = switcheroo(c2)
        c1 = switcheroo(c1)
        c3 = switcheroo(c3)
        b2 = switcheroo(b2)

    if switch == "c3":
        c3 = switcheroo(c3)
        c2 = switcheroo(c2)
        b3 = switcheroo(b3)
    if switch == "stop":
        break

    # prints puzzle-board state at end of loop iteration
    print "\n\n"
    print "    1   2   3   "
    print "  -------------"
    print "a |",a1,"|",a2,"|",a3,"|"
    print "  -------------"
    print "b |",b1,"|",b2,"|",b3,"|"
    print "  -------------"
    print "c |",c1,"|",c2,"|",c3,"|"
    print "  -------------"
    print "\n\n"

    # prints which # was randomly generated
    print "random #: ", counter

    # tracks loop iteration
    iteration += 1
    print "iteration", iteration



if ans == True:
    print "I figured it out!"

3 个回答

0

我觉得你应该能想出一个算法来解决这个问题。这应该不会太难。

首先,你不需要按任何开关超过一次。因为第二次按下只会取消第一次的效果,开关会回到最初的状态(不管你在这期间对其他开关做了什么)。

那么我们可以把开关的状态称为

A11, A12, A13
A21, A22, A23
A31, A32, A33

这就是你开始时开关的状态。

接着,使用同样的坐标系统,我们用T来表示触碰的次数。根据之前说的,每次T的值要么是0,要么是1。

如果你能做一点数学,并且了解代数,知道如何在一个值只取0或1的范围内进行运算,那么我觉得你应该能把这个问题简化成一个在那个空间里的问题,像这样:

A11 + T11 + T12 + T21 = 1
A12 + T11 + T12 + T13 + T22 = 1
A13 + T12 + T13 + T23 = 1
...

如果你把这个当作矩阵运算,可能只需要反转一个矩阵,然后根据A来找到T的值。

也许这个问题更适合在一些更偏向数学的网站上讨论,而不是Stackoverflow……

0

首先,你现在只设置了一次答案(ans)。你需要在循环里面不断评估它。(不过这可能是因为缩进搞错了)。这可能就是你现在的方法没有完成的原因。

另外,一个更“自然”的表示方式可能是使用布尔数组(或者甚至用0到511的数字),这样切换的过程就变成了

a1 = not a1

而且,随机的方法会让你输入很多东西。我觉得你应该考虑从一个解决方案走到你当前的状态。可以把它想象成一个迷宫。总共有2^9 = 512种可能的状态(或者说迷宫里的位置)。每次你按下一个开关就像是在迷宫里走了一步。你真正想要的是找到从初始状态到解决方案的最短路径。所以可以看看Dijkstra算法: http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

1

有一种大家都知道的方法可以解决这个问题。假设 x_1, ..., x_n 是一些变量,用来表示你是否按下第 n 个按钮,而 a_1, ..., a_n 则是初始状态。

比如说,你在解决一个 3x3 的问题,变量可以这样设置:

x_1 x_2 x_3
x_4 x_5 x_6
x_7 x_8 x_9

而这个初始状态是:

a_1 a_2 a_3
a_4 a_5 a_6
a_7 a_8 a_9

现在,你可以写一些方程(在模 2 的算术下),这些方程是解决方案必须满足的条件。简单来说,就是把哪些开关会导致某个灯亮起的规则编码成方程。

a_1 = x_1 + x_2 + x_4
a_2 = x_1 + x_2 + x_3 + x_5
...
a_5 = x_2 + x_4 + x_5 + x_6 + x_8
...
a_9 = x_6 + x_8 + x_9

接下来,你可以用高斯消元法来解这组方程。因为你是在模 2 的算术下工作,所以其实比用实数解方程要简单一些。举个例子,如果你想在第二个方程中消去 x_1,只需把第一个方程加到它上面就可以了。

a_1 + a_2 = (x_1 + x_2 + x_4) + (x_1 + x_2 + x_3 + x_5) = x_3 + x_4 + x_5

具体来说,这里是模 2 下的高斯消元算法:

  • 选择一个包含 x_1 的方程,叫它 E_1。
  • 把 E_1 加到每个其他包含 x_1 的方程上。
  • 对 x_2, x_3, ...., x_n 重复这个过程。

现在,E_n 是一个只包含 x_n 的方程。你可以把从这个方程得到的 x_n 的值代入之前的方程中。对 E_{n-1}, ..., E_1 重复这个步骤。

总的来说,这个方法可以在 O(n^3) 的操作中解决问题。

这里有一些代码。

class Unsolvable(Exception):
    pass

def switches(n, m, vs):
    eqs = []
    for i in xrange(n):
        for j in xrange(m):
            eq = set()
            for d in xrange(-1, 2):
                if 0 <= i+d < n: eq.add((i+d)*m+j)
                if d != 0 and 0 <= j+d < m: eq.add(i*m+j+d)
            eqs.append([vs[i][j], eq])

    N = len(eqs)
    for i in xrange(N):
        for j in xrange(i, N):
            if i in eqs[j][1]:
                eqs[i], eqs[j] = eqs[j], eqs[i]
                break
        else:
            raise Unsolvable()
        for j in xrange(i+1, N):
            if i in eqs[j][1]:
                eqs[j][0] ^= eqs[i][0]
                eqs[j][1] ^= eqs[i][1]

    for i in xrange(N-1, -1, -1):
        for j in xrange(i):
            if i in eqs[j][1]:
                eqs[j][0] ^= eqs[i][0]
                eqs[j][1] ^= eqs[i][1]
    return [(i//m,i%m) for i, eq in enumerate(eqs) if eq[0]]

print switches(4, 3, ([1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 0]))

你只需要输入开关数组的高度和宽度,以及初始状态(每次一行)。它会返回你需要按下的开关,以便把所有的灯都关掉。

撰写回答