1 前言

三阶数字华容道问题又称八数码问题,目前解决数字华容道问题的方法主要有DFS、贪婪算法、A*算法等。DFS时间复杂度较高,贪婪算法和A*算法都能得到一个有效解,但都不是最优解。笔者通过大量实验,使用BFS进行数据预处理后,能够得到最优解。

(1)定义

状态(S):每个棋盘的布局称为一个状态,其中状态 [[1,2,3],[4,5,6],[7,8,9]] 称为零状态

代价(C):从当前状态到零状态所需的最小步数

相连:若状态S1可以通过一步到达状态S2,则称S1与S2相连,每个状态最多与4个状态相连

可达性:若状态S1可以通过有限步到达状态S2,则称S1与S2可达(并不是所有状态都是可达的)

连通集:将所有可达的状态归位一类,称为连通集,一共有2个连通集,每个连通集中有 9!/2=181,440 个状态

本博客只讨论与零状态可达的状态。

(2)算法描述

  1. 预处理:从零状态开始,使用BFS遍历每个状态,在遍历的同时,给每个状态标记遍历的层号(零状态为第0层),即代价。为防止重复遍历某些状态,使用一个字典保存遍历过的状态及其对应层号,即 q_tab={s1:c1,  s2:c2, ...,   s181440:c181440}。
  2. 归位:在q_tab 中查找与当前状态相连的状态的代价,选择代价最小的状态作为下一步决策。

项目打包->三阶数字华容道最优解(更新版)

项目界面

2 实验

笔者工作空间如下:

2.1 数据预处理

通过 BFS 构建 q_tab 表,即“状态->代价”映射表

Generator.py

import numpy as npclass Generator:def __init__(self):self.n=3  #棋盘阶数self.N=self.n*self.n  #棋盘中棋子个数(包含空格)self.dict={}  #用于判重self.que_qi=[]  #用于广度优先搜索中,辅助队列(存储棋盘)self.que_bk=[]  #用于广度优先搜索中,辅助队列(存储空格)self.que_lv=[]  #用于广度优先搜索中,辅助队列(存储层数)self.deep=31  #遍历深度self.X=[-1,-self.n,1,self.n]  #棋子移动方位self.qi_init=""  #初始棋盘布局for i in range(1,self.N+1):self.qi_init+=str(i)def move(self,qi,blank,x,level):  #将空格 blank 移向 x 位置if x<0 or x>=self.N or blank==x:returnif abs(blank%self.n-x%self.n)>1:returnif blank<x:temp=qi[:blank]+qi[x]+qi[blank+1:x]+qi[blank]+qi[x+1:]else:temp=qi[:x]+qi[blank]+qi[x+1:blank]+qi[x]+qi[blank+1:]if temp in self.dict:returnself.dict[temp]=level+1self.que_qi=[temp]+self.que_qiself.que_bk=[x]+self.que_bkself.que_lv=[level+1]+self.que_lvdef bfs(self):  #广度优先搜索self.que_qi=[self.qi_init]+self.que_qiself.que_bk=[self.N-1]+self.que_bkself.que_lv=[0]+self.que_lvself.dict[self.qi_init]=0lv=0while lv<self.deep and self.que_qi!=[]:qi=self.que_qi.pop()bk=self.que_bk.pop()lv=self.que_lv.pop()direction=np.random.permutation(4) #生成0~3的随机排列for i in direction:x=bk+self.X[i]self.move(qi,bk,x,lv)def save_jie3(self):  #保存样本数据num=len(self.dict)data=np.zeros((num,self.N))label=np.zeros(num)i=0for k in self.dict:label[i]=self.dict[k]for j in range(self.N):data[i][j]=float(k[j])i+=1np.savez("jie3.npz",data=data,label=label)def save_q_tab(self):  #保存Query表k=list(self.dict.keys())v=list(self.dict.values())np.savez("q_tab.npz",k=k,v=v)def save_txt(self):  #保存为TXT文档file=open("jie3.txt",'w')s=""for k in self.dict:v=self.dict[k]for i in range(self.N):s+=k[i]+' 's+=str(v)+'\n'file.write(s)file.flush()file.close()gen=Generator()
gen.bfs()
gen.save_jie3()
gen.save_q_tab()
gen.save_txt()

运行 Generator.py 文件后,生成 q_tab.npz 和 jie3.txt 两个文件。q_tab.npz 保存了“状态 -> 代价”的映射,jie3.txt 的部分内容如下:

1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 9 7 8 6 1
1 2 3 4 5 6 7 9 8 1
1 2 9 4 5 3 7 8 6 2
1 2 3 4 9 5 7 8 6 2
1 2 3 4 5 6 9 7 8 2
1 2 3 4 9 6 7 5 8 2
1 9 2 4 5 3 7 8 6 3
....................
9 8 7 6 5 4 3 2 1 28
....................
6 8 7 2 5 1 3 4 9 30
6 4 9 8 5 7 3 2 1 30
6 4 7 8 5 9 3 2 1 31
8 6 7 2 5 4 3 9 1 31

状态[[9,8,7],[6,5,4],[3,2,1]] 竟然不是最难的状态,最难的状态只有2个,只需31步,并且这2个状态还不对称,还挺有趣的。

最难的2个棋盘状态

笔者统计了一下,代价均值:21.97,中位数:22(出现23952次),众数:24(出现24047次)

代价分布图如下:

2.2 归位

(1)预测下一步的方向

Prediction.py

import numpy as npclass Prediction:def __init__(self):self.n=3self.N=self.n*self.nq_tab=np.load('q_tab.npz')k=q_tab['k']v=q_tab['v']self.q_tab=dict(zip(k,v))  #构建Q表self.X=[-1,0,1,0]self.Y=[0,-1,0,1]def pre_step(self,x):  #预测状态 x 对应的步数x=x.reshape(1,-1)k=""for i in range(self.N):k+=str(x[0,i])v=self.q_tab.get(k,-1)return vdef pre_next(self,sta,bk_x,bk_y,bk_x_p,bk_y_p):  #预测下一步往哪个方向走step=[10000,10000,10000,10000]direction=np.random.permutation(4) #生成0~3的随机排列for i in direction:x=bk_x+self.X[i]y=bk_y+self.Y[i]if x<0 or x>=self.n or y<0 or y>=self.n or x==bk_x_p and y==bk_y_p:continuet=sta[x][y]sta[x][y]=self.Nsta[bk_x][bk_y]=tstep[i]=self.pre_step(sta)sta[x][y]=tsta[bk_x][bk_y]=self.Nreturn np.argmin(step)

(2)棋盘逻辑类

Qipan.py

import numpy as np
from Prediction import Predictionclass Qipan:def __init__(self):self.n=3self.N=self.n*self.nself.init=np.arange(1,self.N+1).reshape(self.n,self.n)self.qipan=self.init.copy()self.bk_x=self.n-1self.bk_y=self.n-1self.bk_x_p=-1self.bk_y_p=-1self.pre=Prediction()self.started=False  #标记是否开始self.X=[-1,0,1,0]self.Y=[0,-1,0,1]def make_qipan(self):  #生成随机棋盘max_step=np.random.randint(40000,80000)  #随机生成移动棋子步数step=0while step<max_step or self.qipan[self.n-1][self.n-1]!=self.N:i=np.random.randint(4)x=self.bk_x+self.X[i]y=self.bk_y+self.Y[i]self.move(x,y)step+=1self.bk_x_p=-1self.bk_y_p=-1self.step=0  #提示计步self.started=True  #标记是否开始def move(self,x,y):  #移动棋子if x<0 or x>=self.n or y<0 or y>=self.n:returnself.qipan[self.bk_x][self.bk_y]=self.qipan[x][y]self.qipan[x][y]=self.Nself.bk_x_p=self.bk_xself.bk_y_p=self.bk_yself.bk_x=xself.bk_y=ydef is_finish(self):  #判断游戏是否结束for i in range(self.n):for j in range(self.n):if self.qipan[i][j]!=self.init[i][j]:return Falsereturn Truedef show(self):  #打印当前棋盘状态s=""for i in range(self.n):for j in range(self.n):if self.qipan[i][j]==self.N:s+="  "else:s+=str(self.qipan[i][j])+" "s+="\n"print(s)def tips(self):  #提示一步i=self.pre.pre_next(self.qipan,self.bk_x,self.bk_y,self.bk_x_p,self.bk_y_p)x=self.bk_x+self.X[i]y=self.bk_y+self.Y[i]self.move(x,y)self.step+=1print("step",self.step)self.show()

(3)棋盘显示类

img目录里的图片

MyFrame.py

import wx
from Qipan import Qipan
import threading
import timeclass MyFrame(wx.Frame):def __init__(self,parent=None,id=-1):wx.Frame.__init__(self,parent,id,title='数字华容道',size=(370,450))panel=wx.Panel(self)self.qipan=Qipan()self.bt_start=wx.Button(panel,size=(80,45),label='开始')self.bt_start.Bind(wx.EVT_BUTTON,self.OnClickStart)self.label_step=wx.StaticText(panel,label="0")font_lab=wx.Font(18,wx.DEFAULT,wx.FONTSTYLE_NORMAL,wx.NORMAL)self.label_step.SetFont(font_lab)hsizer=wx.BoxSizer(wx.HORIZONTAL)hsizer.Add(self.bt_start,proportion=0,flag=wx.RIGHT|wx.ALIGN_CENTER,border=100)hsizer.Add(self.label_step,proportion=0,flag=wx.ALL|wx.ALIGN_CENTER,border=20)grid_sizer=wx.GridSizer(3,3,2,2)  #3*3网格,组件边界为2base_path='..\\img\\q_'self.img_id=[]self.bmp_qi=[]for i in range(self.qipan.n):img_t=[]bmp_t=[]for j in range(self.qipan.n):img_t+=[wx.Image(base_path+str(i*self.qipan.n+j+1)+'.png', wx.BITMAP_TYPE_PNG).Rescale(100, 100).ConvertToBitmap()]bmp_t+=[wx.StaticBitmap(panel,-1,img_t[j])]grid_sizer.Add(bmp_t[j],0)self.img_id+=[img_t]self.bmp_qi+=[bmp_t]vsizer=wx.BoxSizer(wx.VERTICAL)vsizer.Add(hsizer,0,wx.ALIGN_CENTER|wx.ALIGN_TOP)vsizer.Add(grid_sizer,0,wx.ALIGN_CENTER)panel.SetSizer(vsizer)def OnClickStart(self,event):if self.qipan.is_finish():  #开局self.qipan.make_qipan()self.draw()self.label_step.SetLabel(str(self.qipan.step))threading.Thread(target=self.demo).start()self.bt_start.SetLabel("暂停")elif not self.qipan.started:  #开始self.qipan.started=Trueself.bt_start.SetLabel("暂停")else:  #暂停self.qipan.started=Falseself.bt_start.SetLabel("开始")def update(self):while self.qipan.started==False:time.sleep(0.2)self.qipan.tips()x=(self.qipan.qipan[self.qipan.bk_x_p][self.qipan.bk_y_p]-1)//self.qipan.ny=(self.qipan.qipan[self.qipan.bk_x_p][self.qipan.bk_y_p]-1)%self.qipan.nself.bmp_qi[self.qipan.bk_x_p][self.qipan.bk_y_p].SetBitmap(self.img_id[x][y])self.bmp_qi[self.qipan.bk_x][self.qipan.bk_y].SetBitmap(self.img_id[self.qipan.n-1][self.qipan.n-1])self.label_step.SetLabel(str(self.qipan.step))def demo(self):while self.qipan.is_finish()==False:time.sleep(0.8)self.update()print("success!")self.qipan.started=Falseself.bt_start.SetLabel("开始")def draw(self):for i in range(self.qipan.n):for j in range(self.qipan.n):x=(self.qipan.qipan[i][j]-1)//self.qipan.ny=(self.qipan.qipan[i][j]-1)%self.qipan.nself.bmp_qi[i][j].SetBitmap(self.img_id[x][y])app=wx.App()
frame=MyFrame()
img=wx.Image()
frame.Show()
app.MainLoop()

演示

本项目采用多线程实现:主线程负责监听用户动作(点击“开始/暂停”按钮),子线程负责棋盘更新

3 改进

受强化学习中DQN学习Q表的启发,笔者尝试使用神经网络映射 q_tab 表。基于神经网络的方法和使用 q_tab 表差不多,主要差别是:在预处理阶段,加了神经网络训练;在归位时,不在q_tab 表中查找代价,而是在神经网络中映射代价。神经网络的网络参数空间为2041KB,q_tab 表的空间为7088KB,这样节省了不少内存空间。

样本、DNN参数、q_tab

笔者使用5层 DNN 训练“状态->代价”网络,网络参数:(9*500+500)+(500*500+500)+(500*500+500)+(500*32+32)=522,032个参数,学习率为0.002,迭代200次,得到准确率为90%,均方差为0.4。笔者尝试过采用CNN训练,但效果并不好,可能是因为图像中,连续几个像素有误对预测影响不大,但棋盘状态某个维度稍有变动,对代价影响较大。考虑到当前状态与零状态之间的距离和代价存在相关性,笔者尝试了RBF网络,但效果仍不理想。

DNN 网络结构

(1)训练DNN

DNN.py

import tensorflow as tf
import numpy as np
from keras.utils import to_categoricalclass DNN:def __init__(self,data,label,lr=0.01,train_step=1000):self.n=3  #阶数self.N=self.n*self.n  #棋子个数(包含空格)self.data=data #训练集self.label=to_categorical(label)  #标签(one_hot编码)self.size=len(label)  #训练集大小self.batch_size=425  #训练批次大小self.batch_num=self.size//self.batch_size+1  #训练批次数self.lr=lr  #学习率self.train_step=train_step  #训练步数#生成每一批次的样本    def next_batch(self,batch_size):index=np.random.randint(0,self.size,batch_size)return self.data[index],self.label[index]#初始化权值函数def weight_variable(self,shape):initial=tf.truncated_normal(shape,stddev=0.1)return tf.Variable(initial)#初始化偏置值函数def bias_vairable(self,shape):initial=tf.constant(0.1,shape=shape)return tf.Variable(initial)def train(self):x=tf.placeholder(tf.float32,[None,9])y=tf.placeholder(tf.float32,[None,32])w_fc1=self.weight_variable([9,500])b_fc1=self.bias_vairable([500])w_fc2=self.weight_variable([500,500])b_fc2=self.bias_vairable([500])w_fc3=self.weight_variable([500,500])b_fc3=self.bias_vairable([500])w_fc4=self.weight_variable([500,32])b_fc4=self.bias_vairable([32])h_fc1=tf.nn.tanh(tf.matmul(x,w_fc1)+b_fc1)h_fc2=tf.nn.sigmoid(tf.matmul(h_fc1,w_fc2)+b_fc2)h_fc3=tf.nn.relu(tf.matmul(h_fc2,w_fc3)+b_fc3)h_fc4=tf.matmul(h_fc3,w_fc4)+b_fc4loss=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y,logits=h_fc4))train=tf.train.AdamOptimizer(self.lr).minimize(loss)accuracy=tf.reduce_mean(tf.cast(tf.equal(tf.argmax(h_fc4,1),tf.argmax(y,1)),tf.float32))mse=tf.reduce_mean(tf.cast(tf.square(tf.argmax(h_fc4,1)-tf.argmax(y,1)),tf.float32))init=tf.global_variables_initializer() with tf.Session() as sess:sess.run(init)x_test,y_test=self.next_batch(50000)for epoch in range(self.train_step):for i in range(self.batch_num):x_,y_=self.next_batch(self.batch_size)sess.run(train,feed_dict={x:x_,y:y_})acc,m=sess.run([accuracy,mse],feed_dict={x:x_test,y:y_test})print("epoch:",epoch,"accuracy:",acc,"mse:",m)self.w_fc1,self.b_fc1,self.w_fc2,self.b_fc2,self.w_fc3,self.b_fc3,self.w_fc4,self.b_fc4=\sess.run([w_fc1,b_fc1,w_fc2,b_fc2,w_fc3,b_fc3,w_fc4,b_fc4],feed_dict={x:x_test,y:y_test})def predict(self,x):h_fc1=tf.nn.tanh(tf.matmul(x,self.w_fc1)+self.b_fc1)h_fc2=tf.nn.sigmoid(tf.matmul(h_fc1,self.w_fc2)+self.b_fc2)h_fc3=tf.nn.relu(tf.matmul(h_fc2,self.w_fc3)+self.b_fc3)h_fc4=tf.matmul(h_fc3,self.w_fc4)+self.b_fc4pre=tf.argmax(h_fc4,1)with tf.Session() as sess:pre=sess.run(pre)return predef save_para(self):np.savez("jie3_dnn.npz",w_fc1=self.w_fc1,b_fc1=self.b_fc1,w_fc2=self.w_fc2,b_fc2=self.b_fc2,\w_fc3=self.w_fc3,b_fc3=self.b_fc3,w_fc4=self.w_fc4,b_fc4=self.b_fc4)temp=np.load('jie3.npz')
data=temp['data']
label=temp['label']
dnn=DNN(data,label,0.002,200)
dnn.train()
dnn.save_para()
x=np.array([[1,2,3,4,5,9,7,8,6]],dtype='float32')
step=dnn.predict(x)

(2)预测

Prediction.py

import numpy as np
import tensorflow as tfclass Prediction:def __init__(self):self.n=3self.N=self.n*self.ntemp=np.load('jie3_dnn.npz')self.w_fc1=temp['w_fc1']self.b_fc1=temp['b_fc1']self.w_fc2=temp['w_fc2']self.b_fc2=temp['b_fc2']self.w_fc3=temp['w_fc3']self.b_fc3=temp['b_fc3']self.w_fc4=temp['w_fc4']self.b_fc4=temp['b_fc4']self.X=[-1,0,1,0]self.Y=[0,-1,0,1]def pre_step(self,x):x=x.reshape(1,-1).astype('float32')h_fc1=tf.nn.tanh(tf.matmul(x,self.w_fc1)+self.b_fc1)h_fc2=tf.nn.sigmoid(tf.matmul(h_fc1,self.w_fc2)+self.b_fc2)h_fc3=tf.nn.relu(tf.matmul(h_fc2,self.w_fc3)+self.b_fc3)h_fc4=tf.matmul(h_fc3,self.w_fc4)+self.b_fc4pre=tf.argmax(h_fc4,1)with tf.Session() as sess:pre=sess.run(pre)return predef pre_next(self,sta,bk_x,bk_y,bk_x_p,bk_y_p):step=[10000,10000,10000,10000]for i in range(4):x=bk_x+self.X[i]y=bk_y+self.Y[i]if x<0 or x>=self.n or y<0 or y>=self.n or x==bk_x_p and y==bk_y_p:continuet=sta[x][y]sta[x][y]=self.Nsta[bk_x][bk_y]=tstep[i]=self.pre_step(sta)sta[x][y]=tsta[bk_x][bk_y]=self.Nreturn np.argmin(step)

三阶数字华容道最优解相关推荐

  1. python数字华容道算法_用React写一个数字华容道,你需要知道的秘密

    还在上班?很无聊? 这个叫前言 年末了.哦,不,要过年了.以前只能一路站到公司的我,今早居然是坐着过来的.新的一年,总要学一个新东西来迎接新的未来吧,所以选择了一直未碰的那个据说是全宇宙最牛逼的前端框 ...

  2. 看最强大脑的数字华容道,尝试理解与总结

    目录 前言 华容道起源 简介 发展 华容道解法 横刀立马解(最少步数) 数字华容道 界面 解法前言 定理 定理解释 针对定理的证明 正解解法细分3种(非官方定义,都是按照自己摸索和理解定义的) 第一种 ...

  3. matlab制作数字华容道,从技术角度实现实现数字华容道

    目的 上周新一期的最强大脑出来了,虽然上季被称为最强黑幕,不过呢.我决定还是看看= =.它里面第一关是叫做数字华容道.说白了,就是和拼图差不多.一开始我准备下一个玩玩的.结果没搜到.所以决定写了一个. ...

  4. 制作Python数字华容道(可选择关卡)

    制作Python数字华容道(可选择关卡) 由于比赛需要,我这边制作了一份数字华容道,内含有3,4,5阶的数字华容道,开头在壳窗口内选择,运用了随机数模块(random)和图形化用户界面(tkinter ...

  5. C语言开发数字华容道实现,从技术角度实现实现数字华容道

    目的 上周新一期的最强大脑出来了,虽然上季被称为最强黑幕,不过呢.我决定还是看看= =.它里面第一关是叫做数字华容道.说白了,就是和拼图差不多.一开始我准备下一个玩玩的.结果没搜到.所以决定写了一个. ...

  6. unity3d 华容道_Android数字华容道小游戏开发

    目的 上周新一期的最强大脑出来了,虽然上季被称为最强黑幕,不过呢.我决定还是看看= =.它里面第一关是叫做数字华容道.说白了,就是和拼图差不多.一开始我准备下一个玩玩的.结果没搜到.所以决定写了一个. ...

  7. mui做一个数字华容道

    绪论 其实好久以前就看到这个数字华容道了,只是一直没时间自己写一个,十月份就在草稿箱(⊙o⊙)-,所以整理了一下. 代码上会存在一些问题,多扩展的部分也没去做,后面会说明原因. 思路 初始化数组. 随 ...

  8. Python游戏开发:最强大脑第一关,数字华容道

    前言 freegames是Apache2许可的Python游戏集合,旨在用于教育和娱乐,完全是开源的,我们只要引用编写就好,当前在最强大脑的舞台上的第一关就是数字华容道,好多人都栽在了上面,如果你也想 ...

  9. 超赞的贪吃蛇、吃豆人和数字华容道等童年小游戏1行Python代码就能玩

    今天分享一个有趣的Python游戏库freegames,它包含20余款经典小游戏,像贪吃蛇.吃豆人.乒乓.数字华容道等等,依托于标准库Turtle. 我们不仅可以通过1行代码进行重温这些童年小游戏,还 ...

最新文章

  1. 世界上最完美的公式 ----欧拉公式
  2. 第一个WindowService服务
  3. Swift - 类型属性(类静态属性)和类方法(类静态方法)
  4. sql 2012先分离迁移mdf mlf 文件到别的机器后附加 数据库成只读的修复方法
  5. 克隆CentOS6虚拟机eth0被修改为eth1如何修改eth0
  6. Java面试技巧—如何自我介绍
  7. 【QT】QT从零入门教程(五):图像文件操作 [新建打开保存]
  8. 要注意观察我们周围的人,不要一天只是低头写代码!
  9. 单细胞----关于Seurat的一些知识
  10. 第二次作业—熟悉使用工具
  11. Java--工厂模式
  12. Easy Data Transform如何在Excel中删除重复的行?
  13. 【Git/Github学习笔记】Github私钥的问题
  14. 自动驾驶 2-2 硬件配置设计 Hardware Configuration Design
  15. oracle 取awr报告,Oracle生成awr报告
  16. 20个免费网站测试工具
  17. 电池SOC仿真系列-基于EKF算法的电池SOC估算研究
  18. 鸿蒙系统安全模式,安全模式怎么连接wifi
  19. Java工作5年的迷茫,是否要转互联网?
  20. cPanel主机自定义php.ini文件

热门文章

  1. python音频处理相关库
  2. 期待已久的—YOLO V3
  3. Linux 基础命令快速入门
  4. 遇到公司共享文件夹,打不开时的处理方式
  5. 【JDBC知识总结】---JDBC连接数据库、连接池、JDBC在框架中的使用等
  6. android之UI美化
  7. MySQL日期时间带T_Mysql 时间和日期函数
  8. 手把手教你使用Electron5+vue-cli3开发跨平台桌面应用
  9. 新能源汽车行业资讯-2022-9-22
  10. Layers>Reshape层