黑白反转游戏

声明:这篇文章中涉及的游戏来自@陈荣峰ayuLemon的创意,文章的主要目的还是研究这个游戏其中的算法,我所写的网页版的游戏版本也仅仅是为了验证算法的正确性。说明这个是防止大家误认游戏乃我的原创。本来原作者要求我不能在他的算法出来之前发出此文,我百思不得其解。我认为这个没有先后顺序,思前想后,还是发出来了。仅讨论之用。

事情的起因是这样的,在我又度过了一个不眠之夜、正打算睡觉之时,看到@python4cn 转发了一个同学的微博,原文是这样的:

@陈荣峰ayuLemon:用python做了个小游戏,这是5*5方格。每点击上面的方格一次,就改变周围四个方格和被点击方格的颜色,对于任意n*n方格,可否通过若干次点击使方格的颜色与最初完全相反??(3*3和4*4的都很容易~~这个5*5的似乎有困难) 大家帮忙想一想,证明行或者不行,哪些行,哪些不行~~

baw

本来已经很困的我看到这个,又勾起了我的好奇。于是拿起纸笔,开始演算。当然首先从2×2开始,一直到4×4,确实没有什么困难。到5×5,算了很久才算出一个结果。

在分析这个问题之前,如果你想试玩,我做了一个网页版的,地址在这里

从直观印象来说,对于四个角上的方块,点击了以后,会触发包括自身的三个方块;四个边上非角的方块,点击可以触发4个方块;而中间的方块可以触发5个。

我们先作一个假设,其中每一个方块最多只需要点击一次就可以完成这个任务。为什么作这个假设呢?因为一个方块它点偶数次,对自己是没有任何影响的,因为又回到原状态(如黑=>白=>黑);如果点击奇数次,相当于只点击一次。同时,假设这个方块的点击对周围某个方块Ki造成了影响,这个方块Ki的状态经过了一系列的变化s1=>s2=>...=>sj(实际上也是黑白交错的过程),源方块的偶数次点击对这个方块的的状态并不会有影响;同样的奇数次个数相当于只点击一次。

在此假设上我们了解,任何方块最多一次点击就能解决问题,前提如果有解决方案的话。

但是之前的结果会让人越想越糟。但是相信大家都玩过扫雷,通过已经揭开的方块,我们可以依次推断是否有雷。同样的这个问题我们也可以这么解决。

我们来看对于某个方块,假设点击过,我们记它的值为1;否则为0。每个方块自身以及周围的方块,可能有3块、4块、5块,那么何时这个方块会反转呢?那就是几个方块的值的和为奇数。很简单不是?

现在,我们用一个矩阵(二维数组)来表示这n×n的方块。用(i, j)表示第i行j列的方块(i,j都是从0到n-1)。对于(0, 0)即左上角的方块,如果(0, 0),(0, 1),(1, 0)这三个的值和为奇数,表明(0, 0)方块必定会反转。

于是,我们经历这样一个过程,从左上角的方块开始,由于它的值不知是0或者1,于是我们假设为1,即反转自身。接着看(0, 1)和(1, 0),这两个值必须同为1或者0。原因很简单,因为不相同,(0, 0)的周围值的和就不为奇数。假设它们的值都为1。以此类推,我们就像四周扩散,使用猜测或者准确的判断。如果发现最后的结果不对,我们就再回溯到之前某个点,比如说回溯到(0, 1)和(1, 0)的时候,当时假设他们同为1,现在假设同为0。然后依次类推。

推理很简单,可做起来就不那么容易了,很常见的是做到最后发现不对再回溯,真是让人恼火的事儿,看起来还真是碰运气啊。

不过,这种事,我们自然可以用程序来解决。由于我们的目标是求出一条路径即可,不需要求出所有的路径(当然可以求,不过费不费时间就不得知了)。下面给出我早上写的代码,采用的回溯法。我是用Python写的,还是那句话,为什么用Python,因为它方便(至于很多人拿Python命令行做计算器也就可以理解了)。不过,由于一大早,没睡觉的缘故,如果代码糟糕,希望大家理解。另外,有什么问题,欢迎大家提出。

我们从左上角开始,逐行扫描。首先,我们先定义一个列表,它专门用来做栈存放猜测的列表,到了某个方块,没法推断,就猜测是0,同时把这个方块压入这个栈中。然后接着运行,到最后如果不符合条件,就从栈顶取出方块,置为1继续运行。

那么,对于一个方块,我们何时能确定它的值呢?由于一个方块上下左右可能都有方块,但是它的值只能准确通过上面的方块(如果有的话,上面的方块的上左右以及自身值都已经确定)得出。而左边的方块由于下面的值未定,所以不能通过其确定。右边和下边的就更别说了(还没运行到呢)。遇到不确定的方块,就猜测0,然后把它的坐标值压入栈。

下面是一大段代码。

#!/usr/bin/env python
#coding=utf-8

class Matrix(object):
    '''矩阵,用来表示n*n方格'''
    def __init__(self, n):
        self.n = n
        self._matrix = [[-1 for j in range(n)] for i in range(n)]
        # 初始化

        self._guess_list = [] # 猜测列表

    def display(self):
        # 打印矩阵
        for i in range(self.n):
            output = ''
            for j in range(self.n):
                output += str(self._matrix[i][j]) + '\t'
            print output

    def _check_rec(self, i, j):
        # 检查某个方格是否反转
        result = 0
        if i > 0:
            result += self._matrix[i-1][j]
        if i < self.n-1:
            result += self._matrix[i+1][j]
        if j > 0:
            result += self._matrix[i][j-1]
        if j < self.n-1:
            result += self._matrix[i][j+1]
        result += self._matrix[i][j]

        return result % 2 == 1

    def _check_result(self):
        # 检查最后得出的矩阵是否全部反转
        for i in range(self.n):
            for j in range(self.n):
                assert self._matrix[i][j] >= 0
                if not self._check_rec(i, j):
                    return False
        return True

    def _define_rec_val(self, i, j):
        # 确定(i, j)方格的值
        # return 0: 值是否是猜测
        # return 1: 值
        guess = True
        
        above_val = -1
        if i > 0:
            above_val = 0
            if i > 1:
                above_val += self._matrix[i-2][j]
            if j > 0:
                above_val += self._matrix[i-1][j-1]
            if j < self.n-1:
                above_val += self._matrix[i-1][j+1]
            above_val += self._matrix[i-1][j]

        if above_val != -1:
            guess = False
            result = 0 if above_val%2==1 else 1
            return guess, result

        return guess, 0

    def _clear(self, i, j):
        # 将(i, j)之后的方块重置-1值
        for k in range(self.n):
            for l in range(self.n):
                if k>i or (k==i and l>=j):
                    self._matrix[k][l] = -1           

    def _run(self, i, j, start=True):
        # 主运行函数
        if not start: self._clear(i, j)
        else:
            self._guess_list.append((i, j))
        
        guess = 0 if start else 1
        self._matrix[i][j] = guess
        
        for k in range(self.n):
            for l in range(self.n):
                if k > i or (k==i and l>j):
                    temp = self._define_rec_val(k, l)
                    if temp[0] == True:
                        # 如果为猜测
                        self._matrix[k][l] = temp[1]
                        self._guess_list.append((k, l)) #添加至猜测列表
                    else:
                        self._matrix[k][l] = temp[1]

        if self._check_result():
            print 'Result: '
            self.display()
        else:
            if len(self._guess_list) > 0:
                self._run(*(self._guess_list.pop()), start=False)
        
        
    def __call__(self):
        self._run(0, 0)


if __name__ == '__main__':
    import sys
    n = 5
    if len(sys.argv) > 1:
        n = int(sys.argv[1])
    m = Matrix(n)
    m()

在命令行中运行脚本(可以指定每行方块数,如运行“python ***.py 5”),当n=5时,结果如下:

result

按照这个策略点击,可以得出正确结果。当然,如果想得出全部结果,修改代码也很容易。

标签

赞这篇文章

分享到

13个评论

  1. Python.cn(news, jobs)

    en 我是特地过来留链接的。 http://simple-is-better.com/news/375 那个评论框的 name 没有使用 WP 的那些 name ? 那样的话,下光标键可以快速让我填写表单了。

  2. @秦续业 作者

    没有链接wp,头像的问题可以用微博登录就会显示微博的头像了,然后也会有昵称和网址啥的。重复填写本来想用session解决的,唉,blog一直没人回复,就没做,回头做上。那个链接我已经看到了,呵呵,经常去看。最后,不知道评论断行是什么问题?

  3. kevin

    1. 任何一个n*n的方阵都是有解的,取C(i,j)来表示第(i, j)个方格被点击的次数,那么对于任意一个方格的反转次数m(i, j)都可一得到一个:
    m(i, j) = C(i, j) + C(i-1, j) + C(i, j+1) + C(i+1, j) + C(i, j-1).
    超出方阵的元素补为0.
    而m(i, j) = 2k(i, j) + 1. k(i, j)可以为任意值, 也就是只要保证反转为奇数次。这样就可以得到一个有n*n个方程的现行方程组,而未知量的个数为2*n*n个(k(i,j)也是变量) 所以这样的方程组必有解。

    2. 我使用了穷举方法。因为只要知道了第一行的点击方式,剩下的就都可一自动确定了。有2^n种需要测试的结果。这样也可一直接得出所有的可能的点击方式。

    3. 我觉得你可以生成任意一个n*n的方阵,不一定是黑白相错的,这样显得问题更加泛化。

  4. @秦续业 作者

    很开心有人会去仔细研究这个问题。由于前几天不能上网,很抱歉到今天才回复。
    1. 我觉得证明很对。对于任意一个点p (i, j),它的反转次数m(i, j) == ∑ C(i', j'),而题意的要求是m(i, j) % 2 == 1,因此有m(i, j) = 2 k(i, j) + 1,可知n^2个方程的2 n^2元方程组必有解。
    2. 用我的方法,确实只有第一行是猜测的。
    3. 泛化的问题由于当时也没有深究,其实有很多探讨的,包括让整个最后都全黑(或者白)之类的。

  5. guangbinw

    以前在某个文曲星上看到过这个游戏,大约是“十全十美”。
    也是用类似的方法解决的,“同一个方格,点击两次等于没有点击”

    可以利用这些东西来优化程序:
    从上往下点击;
    第n行的点击,只会影响到第n-1行和第n+1行的对应列的方格,使之变反;
    如果第n行的某个方格没有变反,则必须点击第n+1行对应列的方格;

    考虑这些之后,整个点击方案,都是受第一行的状态影响的,只要第一行的点击确定了,后续的点击方案就确定了;同时,从左或从右开始都是一样的。
    利用这个思路,根本不需要考虑回溯或者什么,单纯的循环确定第一行,然后第n行状态取反即是第n+1行,逐次遍历,检查到最后一行查看是否全变反即可。

  6. 秦续业 作者

    是这样的,其实也就是受第一行的情况所影响。
    采用回溯法和循环第一行所有情况本质上是一致的。回溯是由于最开始没有看到只有第一行是关键时的natural的想法。

  7. sokoban

    在任意图上玩这个游戏也挺有意思的。若是从全黑到全白,这个游戏还可以推广的任意图上,都有解。我写过一篇博文讨论过这个:http://sokoban.ws/blog/?p=487

给作者留言

关于作者

残阳似血(@秦续业),程序猿一枚,把梦想揣进口袋的挨踢工作者。现加入阿里云,研究僧毕业于上海交通大学软件学院ADC实验室。熟悉分布式数据分析(DataFrame并行化框架)、基于图模型的分布式数据库和并行计算、Dpark/Spark以及Python web开发(Django、tornado)等。

博客分类

点击排行

标签云

扫描访问

主题

残阳似血的微博