To the Moon 记忆碎片解谜

引言

记忆碎片(Memento)是游戏《去月球》(To the Moon)中的一个解谜小游戏。基本规则已在游戏中介绍。即通过点击按钮翻转某一行、列或者对角线,直至所有碎片都转成同一面。

解谜画面

这个解谜与成就/剧情分支无关,而且谜题设置比较简单,比较适合自己玩。谜题也非随机生成,答案都可以在网上找到。

兴趣使然地写了一个编程通用解法。在此介绍一下。

分析

注意到同一位置进行任意偶数次变换都等价于不变,而奇数次变换等价于进行 1 次。为使总变换次数最少,只需决定每个位置是否进行变换即可。

其次,多个不同变换之间满足交换律和结合律,即任意改变变换之间的顺序不会影响最终的结果。

因此,每个解决方案,对应到将变换集合映射到 {进行,不进行} 集合的一个函数映射。若有 N 种可能的变换,则共有 2^N 种可能的解决方案。

可以使用二进制编码,宽度为 行数 + 列数 + 1 。每一位都对应着翻转行、列或对角线。该位为 1 代表进行变换,该位为 0 代表不进行变换。

接下来我们只需要枚举所有可能的解决方案,并将其中正确的挑选出来即可。

游戏中给出了最佳步数,因此可以作为加速搜索的方式,一旦解决方案中,变换的次数超过最佳步数,则跳过。这样,整个搜索空间大小从 2^N 减小到了 C(N, B) ,其中 B 是最佳变换次数, C 是组合数。

实现

首先处理输入,我们将输入的图形转换为一个布尔矩阵。

rowNum = int(input('Row number: '))
colNum = int(input('Col number: '))
best = int(input('Best move: '))
print("Input table, 1 for solved, 0 for unsolved")
table = [list(map(lambda x: True if int(x) == 1 else False, 
         input().split())) for i in range(rowNum)]

接下来定义一些工具函数:

def check():
    return all([all(row) for row in table])

def flip(i, j):
    table[i][j] = False if table[i][j] else True

我们需要从解决方案中提取对应的行、列、对角线进行翻转:

def apply_solution(solution):
    for r in range(rowNum):
        if solution[r]:
            for c in range(colNum):
                flip(r, c)
    for c in range(colNum):
        if solution[rowNum+c]:
            for r in range(rowNum):
                flip(r, c)
    if solution[-1]:
        # diagnol, from left-bottom
        for i in range(min(rowNum, colNum)):
            flip(rowNum-1-i, i)

注意,对角线是从左下角开始的。

类似的,我们定义打印解决方案的函数:

def print_solution(solution):
    for r in range(rowNum):
        if solution[r]:
            print('r%s' % r)
    for c in range(colNum):
        if solution[rowNum+c]:
            print('c%s' % c)
    if solution[-1]:
        print('d')

注意,编号从 0 开始,方向是从上到下、从左到右。 d 代表对角线。

主要过程是枚举整个解决方案空间:

for solution in itertools.product([False, True], repeat=rowNum+colNum+1):
    if solution.count(True) > best:
        continue
    apply_solution(solution)
    if check():
        print_solution(solution)
        break
    else:
        apply_solution(solution)
else:
    print('No answer for best move %s' % best)

首先,使用 itertools.product 产生所有的编码。

对于每一个编码,如果变换数大于最佳,则跳过当前。

如果找到了一个解,则打印并跳出,否则重新调用 apply_solution 函数将表格恢复原状态。

测试

Row number: 3
Col number: 3
Best move: 2
Input table, 1 for solved, 0 for unsolved
1 0 0
1 0 0
1 0 0
c1
c2


Row number: 3
Col number: 3
Best move: 3
Input table, 1 for solved, 0 for unsolved
1 0 0
0 0 0
0 0 1
r1
c1
d

附注

itertools.product

该函数产生一系列 iterable 的笛卡尔积(Cartesian product),如果指明了 repeat 关键字参数,则会将前面所有的 iterable 再和自己进行笛卡尔积。

全局依赖

由于对角线的存在,每一次枚举行/列无法完全确定其他某个变换是否进行,因此没有进一步减小空间的机会。

但事实上,除了第一个谜题,游戏中每个谜题都包含对角线。因此,可以默认只在奇数编码中搜索。将对角线放在最后一位也有利于快速找到解决方案。

最佳步数

代码稍作修改可以找到所有可能的方案,并对其排序,找到变换数最小的方案。也就是最佳步数无需输入。

完整代码

gist

import itertools

def solve_memento():
    rowNum = int(input('Row number: '))
    colNum = int(input('Col number: '))
    best = int(input('Best move: '))
    print("Input table, 1 for solved, 0 for unsolved")
    table = [list(map(lambda x: True if int(x) == 1 else False,
             input().split())) for i in range(rowNum)]

    def check():
        return all([all(row) for row in table])

    def flip(i, j):
        table[i][j] = False if table[i][j] else True

    def apply_solution(solution):
        for r in range(rowNum):
            if solution[r]:
                for c in range(colNum):
                    flip(r, c)
        for c in range(colNum):
            if solution[rowNum+c]:
                for r in range(rowNum):
                    flip(r, c)
        if solution[-1]:
            # diagnol, from left-bottom
            for i in range(min(rowNum, colNum)):
                flip(rowNum-1-i, i)

    def print_solution(solution):
        for r in range(rowNum):
            if solution[r]:
                print('r%s' % r)
        for c in range(colNum):
            if solution[rowNum+c]:
                print('c%s' % c)
        if solution[-1]:
            print('d')

    for solution in itertools.product([False, True], repeat=rowNum+colNum+1):
        if solution.count(True) > best:
            continue
        apply_solution(solution)
        if check():
            print_solution(solution)
            break
        else:
            apply_solution(solution)
    else:
        print('No answer for best move %s' % best)

solve_memento()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容