题设中所说的“。。。要按照能以最多次数把所有方格。。。”的提法疑似笔误,应该为最少次数。这个与后面问题中的“最多”并不矛盾。先求每种状态下到达全白状态所需要的最少翻转次数,然后再比较所有各状态(到达全白状态)所需最少翻转次数的最大值,相当于一个Maxmin问题(更常见的形式是Minmax问题)
把每种盘面状态看作是一个节点(共有2^16=65536中状态/节点,本系列中通常把节点和状态交换使用),把各状态到达全白状态所需要最少翻转次数视为该节点到达全白节点的距离,本题可以看作是在由这65536个节点构成的图中距离全白节点最远的点。当前这里隐含了一个前提,就是说这个图是全连接图,或者说任意一个状态出发经过有限次翻转操作后都能够到达全白状态。
作为Naïve approach,遍历从每个状态出发,然后搜索它们到全白状态的距离(即所需翻转次数),然后再进行比较。这样做当然也可以,但是将会导致巨大的重复和冗余搜索。作为一种改进方案,可以采用“递归+memoization”的策略,将已经搜索得到的结果记忆下来,较长距离的节点的搜索可以利用较短距离的节点的搜索结果。这样相比上述Naïve approach虽然可以得到巨大的性能提升,但是仍然不够好。
由于翻转操作是可逆的,上述的图是一个无向图,A和B两个节点的距离从A向B搜索与从B向A搜索会得到相同的结果。采用逆向思考(本系列中已经出现了多道基于逆向思考策略的题目了,等做完了本系列考虑做一次全面的各种算法策略的总结),从全白状态反向搜索到达各节点的距离,寻找其中最大值,这个问题就转化为从固定起点开始的图搜索中的最长路径搜索问题了,这类问题的经典策略是广度优先搜索。
注意,不仅仅最短路径搜索可以用广度优先搜索,最长路径搜索也可以用广度优先搜索。
在最短路径搜索中,是一旦找到目标点就停止搜索。与之不同的是,在最长路径搜索中,要遍历所有的点,最后到达的那个节点就是距离最大的节点。
4*4的棋盘共有16个方格,其黑白状态可以用16比特的二进制数来表示。
而翻转操作则可以以bit-wise异或运算来实现。对于所选中的每一格方格,其对应的翻转操作可以表示为一个16比特的二进制数,称之为掩码。让掩码与表示当前状态的16比特二进制数进行bit-wise异或运算即等价实现了题设所要求的翻转处理。16个方格对应16种掩码可以提前计算好备用。
如下图所示为一个运算示意图,包括棋盘状态、操作掩码的二进制表示及运算结果(’0b’开头表示这是一个二进制表示):
# -*- coding: utf-8 -*-
"""
Created on Fri Sep 24 08:23:21 2021
@author: chenxy
"""
import sys
import time
import datetime
import random
from typing import List
from collections import deque
import itertools as it
import numpy as np
# all_white = int('0000000000000000',2)
mask_lut = dict()
mask_lut[ 0] = 0b1111_1000_1000_1000
mask_lut[ 1] = 0b1111_0100_0100_0100
mask_lut[ 2] = 0b1111_0010_0010_0010
mask_lut[ 3] = 0b1111_0001_0001_0001
mask_lut[ 4] = 0b1000_1111_1000_1000
mask_lut[ 5] = 0b0100_1111_0100_0100
mask_lut[ 6] = 0b0010_1111_0010_0010
mask_lut[ 7] = 0b0001_1111_0001_0001
mask_lut[ 8] = 0b1000_1000_1111_1000
mask_lut[ 9] = 0b0100_0100_1111_0100
mask_lut[10] = 0b0010_0010_1111_0010
mask_lut[11] = 0b0001_0001_1111_0001
mask_lut[12] = 0b1000_1000_1000_1111
mask_lut[13] = 0b0100_0100_0100_1111
mask_lut[14] = 0b0010_0010_0010_1111
mask_lut[15] = 0b0001_0001_0001_1111
all_white = 0b0000_0000_0000_0000
visited = set()
q = deque()
q.append((all_white,0))
visited.add(all_white)
tStart = time.perf_counter()
while len(q) > 0:
board, layer = q.popleft()
for k in range(16):
mask = mask_lut[k]
nxt_board = mask ^ board
if nxt_board not in visited:
visited.add(nxt_board)
q.append((nxt_board,layer+1))
tCost = time.perf_counter() - tStart
print('final_board = {0}, layer = {1}, tCost = {2:6.3f}(sec)'.format(board,layer,tCost))
运行结果:final_board = 65535, layer = 16, tCost = 0.275(sec)
65536表示为二进制为0b1111_1111_1111_1111,也就是表示全黑的状态。所以距离全白状态最远的是全黑状态,倒是一点都不意外。
本题是到目前为止第一道coding完未经调试直接运行正确的题目,有点小小的得意^-^.
算法编程解题确实也是一个“无他、唯手熟尔”的事情。前面有些题目做的非常费劲,是因为不管是针对问题本质的洞见、算法策略、Python编程等都很不熟悉(就是对各种套路和飞刀不熟悉),但是随着对这些技能的逐渐熟练的掌握,现在的解题就一点一点地变得轻松起来了。