程序员的算法趣题Q66: 设计填字游戏

目录

 

1. 问题描述

2. 解题分析

2.1 基本算法流程

2.2 连通性检查

3. 代码及测试

4. 后记


1. 问题描述

2. 解题分析

        与前面的Q32、Q59以及后面的Q68(碰巧先做了Q68)是类似的搜索问题。有兴趣的小伙伴如果在思考本问题时卡住了可以先看看本系列的以上三题的题解看看是否能找到头绪,如果还是卡住了的话再看本题的题解吧^-^。

2.1 基本算法流程

        本题的基本搜索框架直接基于Q68(因为Q68是前几天刚做的记忆犹新改起来方便),

        算法流程如下(代码中实际上用grids替代seats):

        要点说明:

  1. 与Q32, Q59,Q68一样,逐行扫描,每次向右移一格。右边到头即移到下一格。到达最下边的围栏行则表明已经完成一次搜索,找到一个合规的“预备”方案(因为还要做白色格子连通性检查)
  2. “违规检查”只需要针对当前填黑色的格子进行。这是因为扫描是向右、下方向进行,而且只需要检查是否存在两个相邻格子都为黑色

        与Q68不同的地方在于:

  1. 违规检查不同
  2. 不要求黑、白格子数相同,因此search()函数不需要传入各种颜色(对应Q68的boy/girl)的数量

        本题最后还要进行白色格子形成的区域的连通性,这个是最大的不同点。处理方式参见下一节

2.2 连通性检查

        本题要求填完色后,黑色格子不能分裂整个表格,这是这道题目与Q32,A59,Q68最大的差异之所在。换言之,白色格子构成的区域必须保持连通,这在图论算法中等价于reachability问题,即从某个白色格子出发只允许横向或纵向走一格的话能够到达任何其它的白色格子。解决Reachability问题的经典策略是深度优先搜索。

        算法流程如下:

        补充说明:

  1. 本题中可以直接将已经访问的格子清零,因此不需要另设visited来记录已访问的节点。这样做的好处是对于最后判断是否还有没有访问到的格子也很方便
  2. 最后判断是否还有为1的格子。有的话表示还有白色格子未被访问到,即白色区域不是连通的。

3. 代码及测试

# -*- coding: utf-8 -*-
"""
Created on Wed Oct 20 07:28:54 2021

@author: chenxy
"""

import sys
import time
import datetime
import math
# import random
from   typing import List
from   collections import deque
import itertools as it
import numpy as np

H = 5 # Height, vertical
W = 6 # Width,  horizontal

# grids initialization, with a guard band surrounding the original grids
# The guard band is initialized to '-1' to simplify the judgement processing.
grids = np.zeros((H+2, W+2))
grids[0,:] = -1
grids[H+1,:] = -1
grids[:,0] = -1
grids[:,W+1] = -1

count = 0

def isNG(h,w):
    '''
    '2' represents black.
    Judge whether there are two neighbouring cells are all black

    '''
    return  (grids[h,w]==2) and ((grids[h-1,w]==2) or (grids[h,w-1]==2))

def isAllWhiteConnected(grids)->bool:    
    # Find the first white grid, which is flaged as '1'
    found = False
    for i in range(H):
        for j in range(W):
            if grids[i+1,j+1] == 1:
                start = (i+1,j+1)
                found = True
                break
        if found:
            break
    # print(start)
        
    curGrids = grids.copy()            
    s = deque() # Used as stack, LIFO
    # visited = set() # No need of visited in this problem
    s.append(start)
    # visited.add(start)
    while len(s) > 0:
        cur = s.pop()
        # print(cur)
        curGrids[cur[0],cur[1]] = 0 # Flag it to indicate that it has already been visited.
        if curGrids[cur[0]-1,cur[1]] == 1: # Up grid
            s.append((cur[0]-1,cur[1]))
        if curGrids[cur[0]+1,cur[1]] == 1: # Down grid
            s.append((cur[0]+1,cur[1]))
        if curGrids[cur[0],cur[1]-1] == 1: # Left grid
            s.append((cur[0],cur[1]-1))
        if curGrids[cur[0],cur[1]+1] == 1: # Right grid
            s.append((cur[0],cur[1]+1))        
    return not np.any(curGrids==1)

def arrange_grid(h,w)->int:
    '''
    Parameters
    ----------
    (h,w) : The current exploration point. 
            h represents row index, w represents col index.
    Returns: int
        The number of total arrangement starting from the point (h,w), together 
        with the current grids status, which is a global variable

    '''        
    global count
    # print('h = {0}, w = {1}'.format(h,w))
    if   h == H + 1:
        if isAllWhiteConnected(grids):
            count = count + 1
        # print(grids)    
    elif w == W + 1: # Go to the next row.
        # Reach the right boundary, go to explore the next row from the left 
        arrange_grid(h+1, 1)
    # elif grids[h,w] > 0: 
    #     # This grid has been occupied, move to the right one
    #     arrange_grid(h, w+1)
    else:
        # Try to arrange white to the current grid(h,w). This is always possible
        grids[h,w] = 1
        arrange_grid(h,w+1)            
        grids[h,w] = 0
        # Try to arrange black to the current grid(h,w)
        grids[h,w] = 2
        if not isNG(h,w):
            arrange_grid(h,w+1)            
        grids[h,w] = 0

# # Test of isAllWhiteConnected()
# grids[2,3] = 1
# grids[1,3] = 1
# # print(grids)
# print(isAllWhiteConnected(grids))
                                    
tStart = time.perf_counter()
arrange_grid(1, 1)
tCost  = time.perf_counter() - tStart
print('(H,W)=({0},{1}), count = {2}, tCost = {3:6.3f}(sec)'.format(H,W,count,tCost))  

运行结果:

        (H,W)=(3,4), count = 121, tCost =  0.007(sec)

        (H,W)=(4,5), count = 2749, tCost =  0.244(sec)

        (H,W)=(5,6), count = 149283, tCost = 20.695(sec) 

4. 后记

        速度上应该还有优化空间,且等我扫描完所有题目再回头来做第二轮清理工作。。。

        高级篇竟然是倒着做过来(Q69-->Q68-->Q67-->Q66...)的,不是有意为之。我一般先读一遍题目(不看原书提示和解答),如果5到10分钟内还没有什么头绪,就转向下一道题,碰到似曾相似或者有头绪觉得有可能搞定的先动手,然后就成了这样一个顺序。。。向前面Q56那种鬼脚图为题材的题目完全摸不着头脑,只能先在脑袋里存放一下

        上一篇:Q65: 图形的一笔画

        下一篇:Q67: 不挨着坐是一种礼节吗?

        本系列总目录参见:程序员的算法趣题:详细分析和Python全解

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
经典游戏:在3*3个方格的方阵中要入数1到N(N>=10)内的某9个数,每个方格一个整数,使得所有相邻两个方格内的两个整数之和为质数。试求出所有满足这个要求的各种数法。 //我们可以通过改变N的值来求不同数范围的质数数组,如果超出整型的范围,还需要改变数据类型。 //f[i]来记录数i是否使用过, //T[i]用来记录下一个可以插在数i后面的与其和为质数的数在F[i][]中的位置。 //用F[i][j]来存储按数从小到大的顺序得出的与数i和为质数的第j个数, //例如:F[1][2]存储的是与数1的和为质数的第二个数,我们可以通过查询数组F[][]的第一行找出第二个不为0 //值,然后将当前数组单元的列号存储到F[1][2]中,即F[1][2] = 4。 //算法思想是通过查询二维数组F[][],来确定下一个可以插入数组num[]的未使用过的数,并记录该数位于数组F[][]的位置, //以便回溯时寻找下一个符合要求的数。如果不存在这样的未使用的数,则需要回溯到上一个已插入num[]的数, //寻找下一个可以插在该数后面的未使用过的数进行插入,如果所有的数都已经插入到num[]中, //如是则打印结果。当所有数都已插入num[]中,开始 //进行回溯,重复上述操作,寻找其他符合要求的序列。 //这里面有一个关键在于处理位于num数组右下角四个格子的数时,不光需要判断与前一个数i的关系,还需要判断其头顶上的元素之和是否为素数。 //程序中有一大段代码是处理i和j的。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨牛慢耕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值