广搜算法之翻转棋子游戏

49 篇文章 0 订阅
28 篇文章 1 订阅

#!/usr/bin/python3
#文件:广搜算法之翻转棋子游戏
#作者:巧若拙
#日期:2018年12月30日
'''
题目描述
翻转游戏是这样玩的:
有一张4*4的棋盘,在16个位置上每个位置放着一个棋子,棋子一面是黑色,另一面是白色,棋子或者白色面朝上,或者黑色面朝上。
游戏的走法如下:每一步先选择一个位置,然后把该位置和上,下,左,右(不越界)相邻位置上的棋子翻转(白->黑,黑->白)。
我们用w表示棋子白色面朝上,b表示黑色面朝上。
例如:考虑如下棋盘状态:
bwbw
wwww
bbwb
bwwb
当我们选择第三行,第一列的位置翻转时,棋盘变化为:
bwbw
bwww
wwwb
wwwb
游戏的目的是用最少的步数把全部棋子变为白色向上或黑色向上。
时限:1s。

输入格式
4行由b和w组成的字符串描述的一个棋盘的初始状态。

输出格式
一个测试数据输出一行,为所需要的最少的翻转次数,如果无法翻转成目标状态,则输出’impossible’(小写,没有引号)。

样例输入(1)
bwbw
wwww
bbwb
bwwb

样例输出(1)
Impossible

样例输入(2)
bwwb
bbwb
bwwb
bwww

样例输出(2)
4

算法分析:
初看本题,最容易想到的是穷举法,用包含16个元素的列表d分别表示每个棋子的翻转状态,d[i]=1表示翻转第i个棋子,d[i]=0表示不翻转。
可以穷举从d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]到d=[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],即从都不翻转到都翻转。
若把列表d的值对应整数t的16位二进制数,则相当于穷举从t=0到t=2^16-1。
比较直白的算法是循环遍历每一个t,将t的16位二进制数存储到列表d;然后遍历d,若d[i]=1则翻转位置i及其周围的棋子,并计数;
完成本翻转模式后,判断是否有解,若本翻转模式能获得更好的解,则更新最优解。
上述算法思想简单明了,但把整数t转化成二进制数并存储到列表d的操作比较耗费时间。
我们可以使用位运算来处理整数t的二进制数,这样就无需引入列表d,可以直接操作整数t了,效率有所提升。
穷举法是容易想到的算法,但是效率实在太低,我们可以使用深度优先搜索(回溯加剪枝)来实现同样的功能。
更进一步思考,既然题目要求的是最优解,我们应该使用广度优先搜索才是效率更高的做法。
为避免翻转模式重复入队列,常见的算法是设置一个长度为2^16的列表f,用来标记整数t是否出现过,先初始化f[i]=False,一旦整数i入队列,则设置f[i]=True;
但是我没有使用这种方法,而是规定每次只在当前翻转模式的左侧增加翻转的棋子,这样就能避免重复。
这样我们就分别用穷举,深搜和广搜三种算法实现了所需功能。
回到最初的想法,我们使用列表d来表示整数t的二进制数的每一位,是很直观的想法,只不过每次都需要把整数转化成二进制数比较耗费时间;
其实我们可以直接修改列表d的值来模拟整数t递增的过程,这样就不需要引入整数t及其位运算了,数据结构更清晰。
我也用这种数据结构实现了穷举,深搜和广搜三种算法。

本题的各种算法也可以用c++语言来实现,两种语言相比较,我发现由于Python语言的自动回收内存机制和函数式编程特征,以及丰富的内置函数,使得编程者无需考虑过多的实现细节,能更关注算法思想本身,代码也更简洁,更优雅。
'''

from queue import Queue

#判断是否已经翻转成功:全部棋子变为白色向上或黑色向上
def check(a):
    return all(a) or not any(a)

#翻转棋盘a中位置i及其周围的棋子
def turn(a, i, size):
    a[i] = not a[i] #翻转位置i的棋子
    if i >= size:    
        a[i-size] = not a[i-size] #不是第一行则翻转上方棋子
    if i < size*(size-1):
        a[i+size] = not a[i+size] #不是第size行则翻转下方棋子
    if i % size > 0:
        a[i-1] = not a[i-1] #不是第一列则翻转左方棋子
    if (i+1) % size > 0:
        a[i+1] = not a[i+1] #不是第size列则翻转右方棋子

#将整数t的16位二进制数存储到列表d
def binary_number(t, n): 
    d =[0] * n #高位补零,凑足n位
    i = n -1
    while t > 0:
        d[i] = t & 1         #相当于t % 2
        i, t = i - 1, t >> 1 #相当于t // 2
    return d

#穷举法解翻转棋子游戏:最直观的想法,把整数t的16位二进制数存储到列表d,并根据列表d的值来确定翻转模式
def exhaustion_0(a):  
    min_c = len(a) + 1 #初始化最小步数为最大值 
    for t in range(1<<len(a)): #遍历从[0,1<<len(a)]的所有翻转模式
        b, c = a.copy(), 0 #复制棋盘a到b,c用来累计翻转棋子的数量
        d = binary_number(t, len(a)) #将整数t的16位二进制数存储到列表d
        for i in range(len(d)):
            if d[i] == 1: #t的二进制数第i位是1,则翻转位置i及其周围的棋子,并计数
                turn(b, i, size)
                c += 1
        if check(b) and c < min_c: #本翻转模式能获得更好的解,则更新最优解
            min_c = c
    return min_c

#穷举法解翻转棋子游戏:直接用位运算操作整数t的各个二进制数位,效率更高
def exhaustion_1(a):  
    min_c = len(a) + 1 #初始化最小步数为最大值 
    for t in range(1<<len(a)): #遍历从[0,1<<len(a)]的所有翻转模式
        b, c = a.copy(), 0 #复制棋盘a到b,c用来累计翻转棋子的数量
        for i in range(len(lib)):
            if t & lib[i] > 0: #t的二进制数第i位是1,则翻转位置i及其周围的棋子,并计数
                turn(b, i, size)
                c += 1
        if check(b) and c < min_c: #本翻转模式能获得更好的解,则更新最优解
            min_c = c
    return min_c

#穷举法解翻转棋子游戏:直接修改列表d的值来模拟整数t递增的过程,无需进行位运算,更好理解
def exhaustion_2(a): 
    min_c = len(a) + 1 #初始化最小步数为最大值
    d =[0] * len(a) #初始化所有的位都是0
    while True: #遍历从[0,1<<len(a)]的所有翻转模式
        b, c = a.copy(), 0 #复制棋盘a到b,c用来累计翻转棋子的数量
        for i in range(len(d)):
            if d[i] == 1: #t的二进制数第i位是1,则翻转位置i及其周围的棋子,并计数
                turn(b, i, size)
                c += 1
        if check(b) and c < min_c: #本翻转模式能获得更好的解,则更新最优解
            min_c = c
        i = len(d) - 1
        while i >= 0 and d[i] == 1: #修改列表d的值来模拟整数t递增的过程,每次递增1
            d[i] = 0
            i -= 1
        if i >= 0:  #将d[i]右侧的1都改成0,d[i]改成1,相当于二进制数递增1
            d[i] = 1
        else:  #已经递增到最大值,即所有的二进制位均为1
            break
    return min_c

'''
#2个不完全的回溯算法。说他们不完全回溯,是因为每次都是使用列表a的拷贝进入下一层递归函数的,并且每次都翻转了该模式中所有的棋子,与穷举法并无二致。
def dfs_3(a, t): #深搜法解翻转棋子游戏
    global min_cc
    b, c = a.copy(), 0 #复制棋盘a到b,c用来累计翻转棋子的数量
    for i in range(len(lib)):
        if t & lib[i] > 0: #t的二进制数第i位是1,则翻转位置i及其周围的棋子,并计数
            turn(b, i, size)
            c += 1
    if check(b): #已经有解就不必再翻转更多棋子了
        if c < min_cc:#本翻转模式能获得更好的解,则更新最优解
            min_cc = c
    else:
        for x in lib:
            if t != t | x:#从左向右依次修改0为1,直到遇到1,这样可以确保只在原数的左侧增加1个1
                dfs_3(a, t | x)
            else:
                break

def dfs_4(a, d): #深搜法解翻转棋子游戏
    global min_cc
    b, c = a.copy(), 0 #复制棋盘a到b,c用来累计翻转棋子的数量
    for i in range(len(d)):
        if d[i] == 1: #t的二进制数第i位是1,则翻转位置i及其周围的棋子,并计数
            turn(b, i, size)
            c += 1
    if check(b): #已经有解就不必再翻转更多棋子了
        if c < min_cc:#本翻转模式能获得更好的解,则更新最优解
            min_cc = c
    else:
        for i in range(len(d)):
            if d[i] == 0:#从左向右依次修改0为1,直到遇到1,这样可以确保只在原数的左侧增加1个1
                d[i] = 1
                dfs_4(a, d)
                d[i] = 0 #回溯
            else:
                break
'''
#深搜法解翻转棋子游戏:直接用位运算操作整数t的各个二进制数位,每层递归函数的棋盘都继承自上一层函数,每层只翻转1个棋子
#参数介绍:a——列表,存储当前棋盘状态;t——正整数,其二进制数代表当前翻转模式;c——正整数,表示已经翻转的棋子数量。
def dfs_1(a, t, c): 
    global min_cc
    if check(a): #已经有解就不必再翻转更多棋子了
        if c < min_cc:#本翻转模式能获得更好的解,则更新最优解
            min_cc = c
    else:
        for i in range(len(lib)):
            if c+1 < min_cc and t & lib[i] == 0: #如果没有更优解就剪枝,只在第一个值为1的二进制位左侧设置1,以避免重复
                t |= lib[i]     #将整数t的第i个二进制位设置成1
                turn(a, i, size)
                dfs_1(a, t, c+1)
                turn(a, i, size)#回溯
                t &= ~lib[i]    #将整数t的第i个二进制位恢复成0
            else:
                break

#深搜法解翻转棋子游戏:使用列表d存储当前翻转模式,每层递归函数的棋盘都继承自上一层函数,每层只翻转1个棋子
#参数介绍:a——列表,存储当前棋盘状态;d——列表,存储当前翻转模式;c——正整数,表示已经翻转的棋子数量。
def dfs_2(a, d, c):  
    global min_cc
    if check(a): #已经有解就不必再翻转更多棋子了
        if c < min_cc:#本翻转模式能获得更好的解,则更新最优解
            min_cc = c
    else:
        for i in range(len(d)):
            if c+1 < min_cc and d[i] == 0:#如果没有更优解就剪枝,只在第一个值为1的二进制位左侧设置1,以避免重复
                d[i] = 1
                turn(a, i, size)
                dfs_2(a, d, c+1)
                turn(a, i, size) #回溯
                d[i] = 0 #回溯
            else:
                break
            

'''
广搜法:把各种翻转模式存储到队列中,翻转棋子的数量从小到大依次加入队列。
一开始设置翻转棋子数为0,然后逐渐增加翻转棋子数量(通过按位或运算实现)。
为避免重复,每次增加的翻转棋子的位置只能出现在当前模式的左侧,
可以遍历lib,将其元素与代表当前翻转模式的整数t依次进行按位或运算,直到结果等于t,
相当于从左向右依次修改0为1,直到遇到1,这样可以确保只在原二进制数的左侧增加1个1。
每次按位或运算的结果就是获得新的翻转模式,将其加入队列即可。
因为翻转棋子的数量越来越多,故最早获得的解就是最优解。
'''
#广搜法解翻转棋子游戏:直接用位运算操作整数t的各个二进制数位,效率更高
def bfs_1(a):
    q = Queue() #创建队列对象
    q.put(0) #翻转棋子数为0
    while not q.empty():
        t = q.get()
        b, c = a.copy(), 0 #复制棋盘a到b,c用来累计翻转棋子的数量
        for i in range(len(lib)):
            if t & lib[i] > 0: #t的二进制数第i位是1,则翻转位置i及其周围的棋子,并计数
                turn(b, i, size)
                c += 1
        if check(b): #因为翻转棋子的数量越来越多,故最早获得的解就是最优解
            return c
        else:
            for i in range(len(lib)):
                if t & lib[i] == 0:  #从左向右依次修改0为1,直到遇到1,这样可以确保只在原二进制数的左侧增加1个1 
                    q.put(t | lib[i])#将整数t的第i个二进制位设置成1后入列
                else:
                    break
    return len(a)+1 #无解则返回len(a)+1

#广搜法解翻转棋子游戏:使用列表d存储当前翻转模式,较为直观
def bfs_2(a): 
    q = Queue() #创建队列对象
    d =[0] * len(a) #初始化所有的位都是0
    q.put(d) #翻转棋子数为0
    while not q.empty():
        d = q.get()
        b, c = a.copy(), 0 #复制棋盘a到b,c用来累计翻转棋子的数量
        for i in range(len(d)):
            if d[i] == 1: #t的二进制数第i位是1,则翻转位置i及其周围的棋子,并计数
                turn(b, i, size)
                c += 1
        if check(b): #因为翻转棋子的数量越来越多,故最早获得的解就是最优解
            return c
        else:
            for i in range(len(d)):
                if d[i] == 0:#从左向右依次修改0为1,直到遇到1,这样可以确保只在原二进制数的左侧增加1个1
                    t = d.copy()
                    t[i] = 1 
                    q.put(t) #生成列表d的拷贝,并将其第i个二进制位设置成1后入列
                else:
                    break
    return len(a)+1 #无解则返回len(a)+1


def main(a):
    min_c = exhaustion_0(a) #穷举法解翻转棋子游戏
    if min_c <= len(a):
        print(min_c)
    else:
        print("impossible")
        
    min_c = exhaustion_1(a) #穷举法解翻转棋子游戏
    if min_c <= len(a):
        print(min_c)
    else:
        print("impossible")

    min_c = exhaustion_2(a) #穷举法解翻转棋子游戏
    if min_c <= len(a):
        print(min_c)
    else:
        print("impossible")
     
    min_c = bfs_1(a) #广搜法解翻转棋子游戏
    if min_c <= len(a):
        print(min_c)
    else:
        print("impossible")

    min_c = bfs_2(a) #广搜法解翻转棋子游戏
    if min_c <= len(a):
        print(min_c)
    else:
        print("impossible")
    
    global min_cc
    min_cc = len(a)+1
    dfs_1(a, 0, 0) #深搜法解翻转棋子游戏
    if min_cc <= len(a):
        print(min_cc)
    else:
        print("impossible")

    min_cc = len(a)+1
    d =[0] * len(a) #初始化所有的位都是0
    dfs_2(a, d, 0) #深搜法解翻转棋子游戏
    if min_cc <= len(a):
        print(min_cc)
    else:
        print("impossible")

    

size = 4 #4*4的方阵
lib = tuple(map(lambda x: 1 << x, range(size**2-1,-1,-1)))#从高到低标记每个二进制位的1
print(lib)
min_cc = len(lib)+1
with open('fzqz.in', 'r') as fin:
    a, c = [], 0
    for line in fin.readlines():
        print(line.strip())#依次读取每行
        a.extend(line.strip()) #去掉每行头尾空白
        c += 1
        if c == 4:
            a = list(map(lambda x: x=="b", a))
            main(a)
            a, c = [], 0
   

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值