python解最强大脑: 黑白迭代

本文思路参考https://zhuanlan.zhihu.com/p/152349573

黑白迭代规则

(已经看过的可以直接跳到下一部分了)







B站视频:

最强大脑:B圈S圈层破圈突围赛,一对一挑战“黑白迭代”

解法分析

重要性质

黑白迭代游戏有以下几个重要性质:

  1. 所有方格只有两种状态:白和黑,程序中可以定义为BLACKWHITE
  2. 最终状态与操作的顺序无关。(即random.shuffle了也没事)
  3. 同一方格改变偶数次状态后,不改变状态;同一方格改变奇数次状态,相当于只改变了一次状态

简化问题

由空白盘面构造目标盘面,也可以简化为从目标盘面还原空白,两者操作顺序相同。

公式法

这时我们需要寻找一种只翻转某个特定方格,其余方格不变的公式。
X ( a , b ) X_{\left( a,b\right) } X(a,b)只翻转(a, b)位方格的解法


上图中,将找到的 X ( 8 , 3 ) X_{\left( 8,3\right) } X(8,3) X ( 8 , 4 ) X_{\left( 8,4\right) } X(8,4)合并,即可消除。

合并多个公式

根据黑白迭代的性质3,可根据偶消奇不消的规则将其整合成一个解法。
通过观察发现,这一规则实际上就是异或(XOR)

推导公式

点击(0, 0)后,出现三个黑色点,如下图所示:

根据合并解法的方法,可列出如下等式:

X ( 0 , 0 ) ⊻ X ( 0 , 1 ) ⊻ X ( 1 , 0 ) = [ [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ] X_{\left( 0,0\right) } \veebar X_{\left( 0,1\right) } \veebar X_{\left( 1,0\right) } = [ [1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0] ] X(0,0)X(0,1)X(1,0)=[[1,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0]]

⊻ \veebar 表示异或

所以,可列出64个异或方程:

X ( 0 , 0 ) ⊻ X ( 0 , 1 ) ⊻ X ( 1 , 0 ) = [ [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ] X_{\left( 0,0\right) } \veebar X_{\left( 0,1\right) } \veebar X_{\left( 1,0\right) } = [ [1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0] ] X(0,0)X(0,1)X(1,0)=[[1,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0]]

X ( 0 , 0 ) ⊻ X ( 0 , 1 ) ⊻ X ( 1 , 1 ) ⊻ ( 1 , 2 ) = [ [ 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ] X_{\left( 0,0\right) } \veebar X_{\left( 0,1\right) } \veebar X_{\left( 1,1\right) } \veebar {\left( 1,2\right) } = [ [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0] ] X(0,0)X(0,1)X(1,1)(1,2)=[[0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0]]

. . . ... ...

X ( 7 , 7 ) ⊻ X ( 7 , 6 ) ⊻ X ( 6 , 7 ) = [ [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] , [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ] ] X_{\left( 7,7\right) } \veebar X_{\left( 7,6\right) } \veebar X_{\left( 6,7\right) } = [ [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1] ] X(7,7)X(7,6)X(6,7)=[[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,1]]

用高斯消元法解这个方程组,得到答案。

高斯消元法

高斯消元解异或方程组的方法不介绍了,这里分析具体算法。
由于64个未知数的解是一个矩阵,为了简化计算,可以将矩阵转化为数字。
例如,下面的矩阵:

[
     [1, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0]
]

可以转变为如下二进制数,方便按位异或计算:

0b1000000000000000000000000000000000000000000000000000000000000000000000000000000000

解出X的值后,将整数转为二进制,不够64位前面补0,获得矩阵。

可视化

pygame简单写一个,用到的黑白方块:

将上图保存为black.bmpwhite.bmp

代码实现

项目结构

如下所示:

solve.py

解法部分,如下:

from math import ceil
from typing import List, Tuple

# 定义各种类型标注
SolveType = List[Tuple[int, int]]
BoardType = List[List[int]]
EquationType = List[Tuple[List[Tuple[int, int]], Tuple[int, int]]]

WIDTH = 8   # 格数
WHITE, BLACK = 0, 1


def get_blank_board():  # 获取一个空白盘面
    return [[WHITE] * WIDTH for _ in range(WIDTH)]


def get_periphery(x: int, y: int):  # 求所有周边点,包括自己
    res = [(x, y)]
    for tx, ty in ((0, 1), (1, 0), (0, -1), (-1, 0)):
        nx, ny = x + tx, y + ty
        if 0 <= nx < WIDTH and 0 <= ny < WIDTH:
            res.append((nx, ny))
    return res


def equation():  # 列异或方程组
    res = []
    for x in range(WIDTH):
        for y in range(WIDTH):
            periphery = get_periphery(x, y)
            res.append((periphery, (x, y)))
    return res


def chunk(x: list, step: int):  # 按步长切割列表x
    step = int(step)
    return list(
        map(
            lambda n: x[n * step: n * step + step],
            list(range(0, ceil(len(x) / step)))
        )
    )


def gaussian(equ: EquationType):  # 异或方程组转矩阵
    matrix = []  # 增广矩阵
    # 系数1 系数2 ... 系数64 等号右侧
    for xs, pos in equ:
        p = []
        for x, y in xs:
            p.append(x * WIDTH + y)
        a = [0] * (WIDTH * WIDTH)
        for i in p:
            a[i] = 1  # 系数只有1或0
        a.append(array_to_int([pos]))  # 右端常数
        matrix.append(a)
    return matrix


def array_to_int(array: SolveType):  # 解法转十进制整数
    board = get_blank_board()
    for x, y in array:
        board[x][y] = BLACK
    flatten = lambda x: [y for L in x for y in flatten(L)] if type(x) is list else [x]   # 碾平二维列表
    a = flatten(board)
    del flatten
    res = [0] * (WIDTH * WIDTH)
    for inx, item in enumerate(a):
        if item == BLACK:
            res[inx] = 1
    res = ''.join(str(i) for i in res)   # 矩阵转二进制
    return int(res, base=2)   # 二进制转整数


def int_to_array(n: int):   # 十进制整数转解法
    lst = list(bin(n).replace('0b', ''))
    lst = ['0'] * (WIDTH * WIDTH - len(lst)) + lst   # 补0
    board = [0] * (WIDTH * WIDTH)
    for inx, item in enumerate(lst):
        if item == '1':
            board[inx] = BLACK
    board = chunk(board, WIDTH)
    return board
    

def guass(n: int, matrix: BoardType):  # 高斯消元法解异或方程组
    r = 0
    for c in range(n):
        t = r
        for i in range(r, n):  # 找1
            if matrix[i][c] == 1:
                t = i
                break
        
        if matrix[t][c] == 0:
            continue
        matrix[r], matrix[t] = matrix[t], matrix[r]  # 交换两行
        
        for i in range(r + 1, n):  # 1与r行异或
            if matrix[i][c] == 1:
                for j in range(c, n + 1):
                    matrix[i][j] ^= matrix[r][j]  # 合并
        
        r += 1
    
    if r < n:  # 这个BUG只在30阶出现
        for i in range(r, n):
            if matrix[i][n] == 1:
                raise SystemExit('方程组无解')
        raise SystemExit('方程组有多组解')
    
    for i in range(n - 1, -1, -1):  # 注意是倒序
        for j in range(i):
            if matrix[j][i] == 1:
                matrix[j][n] ^= matrix[i][n]
    
    for i in range(n):
        yield matrix[i][n]


def solve():  # 获取解方程的结果
    m = gaussian(equation())
    res = {}  # 哈希表存储
    for inx, num in enumerate(guass(WIDTH * WIDTH, m)):
        val = int_to_array(num)
        x, y = inx // WIDTH, inx % WIDTH  # 一维转二维公式
        res[(x, y)] = val
    return res


def merge(a: BoardType, b: BoardType):  # 合并两组解,实际上就是异或
    if not a:
        return b
    if not b:
        return a
    res = get_blank_board()
    for x, i in enumerate(a):
        for y, j in enumerate(i):
            k = b[x][y]
            if k == j:  # 异或核心
                res[x][y] = WHITE
            else:
                res[x][y] = BLACK
    return res
    

def get(board: BoardType):  # 主函数
    solution = solve()
    res = []
    for x, i in enumerate(board):
        for y, j in enumerate(i):
            if j == BLACK:
                res.append(solution[(x, y)])
    ans = []
    for i in res:
        ans = merge(ans, i)  # 合并解法,减少空间和可视化消耗
    
    res = []
    for x, i in enumerate(ans):
        for y, j in enumerate(i):
            if ans[x][y] == BLACK:
                res.append((x, y))
    return res


if __name__ == '__main__':
    test = [
        [0, 0, 1, 0, 0, 1, 0, 0],
        [0, 1, 0, 0, 0, 0, 1, 0],
        [0, 0, 0, 1, 1, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 1, 0, 0, 1, 0, 1],
        [0, 1, 1, 0, 0, 1, 1, 0],
        [1, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 1, 1, 1, 1, 0, 1]
    ]  # 节目最难的第三题
    print(get(test))

gui.py

可视化部分,如下:

import sys
import os

import pygame
from pygame.locals import *

from solve import *

BLACK_PATH = os.path.join('resources', 'black.bmp')  # 黑色图路径
WHITE_PATH = os.path.join('resources', 'white.bmp')
GRID_WIDTH = 40
SPEED = 5  # 显示速度,越高越慢
FPS = 60  # 帧率


def destroy():
    pygame.quit()
    sys.exit(0)


class Window(object):
    def __init__(self, solution: SolveType):
        pygame.init()  # 必不可少
        self.solution = solution
        
        self.screen = pygame.display.set_mode((GRID_WIDTH * WIDTH, GRID_WIDTH * WIDTH))
        pygame.display.set_caption('黑白迭代可视化')
        
        self.board = get_blank_board()
        self.black = pygame.image.load(BLACK_PATH)
        self.white = pygame.image.load(WHITE_PATH)
        
        self.inx = 0   # 播放索引
        self.clock = pygame.time.Clock()
        
    def draw_board(self):
        for r, i in enumerate(self.board):
            for c, j in enumerate(i):
                x, y = c * GRID_WIDTH, r * GRID_WIDTH  # 特别注意:c和r不能反
                if j == WHITE:
                    self.screen.blit(self.white, (x, y))
                if j == BLACK:
                    self.screen.blit(self.black, (x, y))
                    
    def reverse(self, x: int, y: int):
        lst = [(x, y)]
        for tx, ty in ((0, 1), (1, 0), (0, -1), (-1, 0)):
            nx, ny = x + tx, y + ty
            if 0 <= nx < WIDTH and 0 <= ny < WIDTH:
                lst.append((nx, ny))
        for nx, ny in lst:
            self.board[nx][ny] = WHITE if self.board[nx][ny] == BLACK else BLACK
        
    def show(self):
        rates = 0
        while True:
            self.clock.tick(FPS)
            for event in pygame.event.get():
                if event.type == QUIT:
                    destroy()
            if rates % SPEED == 0:
                if self.inx >= len(self.solution):  # 溢出,结束
                    break
                self.reverse(*self.solution[self.inx])  # 翻转
                self.inx += 1
            rates += 1
            self.draw_board()
            pygame.display.update()
        while True:  # 第二循环,显示复原结果
            self.clock.tick(FPS)
            for event in pygame.event.get():
                if event.type == QUIT:
                    destroy()
            self.draw_board()
            pygame.display.update()

main.py

入口程序,如下:

from gui import Window
from solve import get

test = [
    [0, 0, 1, 0, 0, 1, 0, 0],
    [0, 1, 0, 0, 0, 0, 1, 0],
    [0, 0, 0, 1, 1, 0, 0, 0],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 0, 1, 0, 1],
    [0, 1, 1, 0, 0, 1, 1, 0],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 1, 1, 1, 0, 1]
]  # 节目最难的第三题

solution = get(test)

Window(solution).show()

效果

复杂度分析

这里指解法部分。
设n为边长(阶数),高斯消元法时间复杂度 O ( n 3 ) O(n^{3}) O(n3)
而一共要求解 n 2 n^{2} n2个格子的解法,所以时间复杂度为 O ( n 5 ) O(n^{5}) O(n5)

在最坏情况下(全部变黑),需要 n 2 n^{2} n2的空间,空间复杂度 O ( n 2 ) O(n^{2}) O(n2)

最好情况最坏情况
时间复杂度 O ( n 5 ) O(n^{5}) O(n5) O ( n 5 ) O(n^{5}) O(n5)
空间复杂度 O ( 1 ) O(1) O(1) O ( n 2 ) O(n^{2}) O(n2)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值