我的博客链接
队友博客链接.
github.
具体分工:
| 分工 | | |||
| :------| ------: | :------: |:------: |:------: |:------: |
| 杜筱 | 算法分析 | AI设计 |博客编写|代码修改|
|董翔云|原型设计|代码编写|博客修改|
目录:
1,原型设计
页面设计说明
原型设计开发工具
结对过程
遇到的困难及解决方法
2,AI算法
代码实现思路
Github的代码签入记录
遇到的困难及解决方法
评价你的队友
PSP和学习进度条
2.1原型设计
2.1.1 页面设计说明
小游戏采取网页端实现,双击html文件在浏览器中打开即可开始游戏
设计说明:页面主体是 打乱的图片,原图和功能按钮
按钮介绍:
1、开始游戏/结束游戏
按下之后能够为你记录游戏时间,同时只有按下了才能移动方块。按下开始游戏后,开始游戏按钮变成结束游戏按钮,按下结束游戏则游戏暂停,游戏停止计时。
2、重新开始
按下之后,图片顺序随机打乱,游戏时间重新开始记录,游戏重新开始
3、开挂试试
按下之后如果存在路径(即逆序数为偶),电脑以动画形式自动演示,如若不存在路径(即逆序数为奇),则弹出弹窗,提醒无解。
4、选择图片
按下之后,弹出页面左边的导航栏,可选择想要拼的图。
5、交换图片
当强制交换后无解,可人为强制交换,在交换图片按钮旁边的text中写入想要交换的两个块,按下交换图片按钮,这两个块就会被强制交换。
弹窗介绍:
1、当赢了之后会弹出弹窗“你赢了!!!,你的步数是:_____”
2、当你走的步数到达49步却仍旧没有成功还原所有图片,则电脑弹出弹窗,提醒强制交换任意两个小块
3、电脑强制交换后无解,电脑会弹出弹窗,提醒在交换图片按钮旁边的text中输入两个数字,认为交换想要交换的两个块
4、自动演示时,当布局无解时,会弹出弹窗提醒无解
5、自动演示时,当布局有解,会弹出弹窗,给出最小步数及还原路径
如图:
2.1.2 原型设计开发工具
采用Axure Rp实现
2.1.3结对过程
2.1.4 遇到的困难及解决方法
1,困难描述
原型设计工具如何选择并使用,怎样设计画风满意的页面,设计的界面如何实现,页面如何兼容不同的浏览器
2,解决尝试
在网上找一些axure rp的教程,认真学习;
参考别人的界面,逐个尝试配色和界面装饰,借鉴别人修改页面的经历
学习css和html的基础知识,在网上找一些模板,逐步改进
认真分析浏览器对css哪些方法响应不同,避免使用这些方法即可
3,是否解决
是
4,有何收获
第一次接触专业的快速原型设计工具,从专业角度,学习设计页面,怎样设计出自己满意并且用户体验感高的页面
从网上找到的模板怎样修改为自己的风格,怎样与js结合
2.2 AI与原型设计实现
2.2.1 代码实现思路
1,普通拼图功能:
点击一个非空的块,如果它的周围有空的快,被点击的块就往空块的方向移动,直至图像完整,则通关
注:这里我们得解释一下,图片与数字的对应:将原图分为九个小块,从左至右,从上至下,每个小块上的图片分别与123456789一一对应,于是问题就被大大简化了,在下面的代码实现中,我们随机打乱的是1-9的数字,从而对应也就打乱了数字对应的图片,当数字顺序时,也就是123456789顺序排列时,对应的照片也就完整了
普通拼图需要做以下几件事:
选择图片:
function choose_image(img){
img_src=img.src;
document.getElementById("correct_img").src=img_src;
$("#play_ground div").css('background-image',"url("+img_src+")");
$('#correct_img').css('background-image',"url("+img_src+")");
shuffle_div();
}
随机打乱:
// 图片先归位,再随机打乱图片, 交换div的位置
function shuffle_div(){
for(var i=0;i<=9;i++){
value_div[i]=i;
}
// 图片归位
lefts=[0,0,150,300,0,150,300,0,150,300]
tops=[0,0,0,0,150,150,150,300,300,300]
for(var i=1;i<=9;i++){
document.getElementById("u"+i).style.left=lefts[i]+"px";
document.getElementById("u"+i).style.top=tops[i]+"px";
}
// 上次设置的隐形图片还原
document.getElementById("u"+transprant_img).style.opacity=1;
// 记录的步数也要清零
move_walks=0;
var divs=[]
// 每个div的图片序号
for(var i=1;i<=9;i++){
divs[i]=document.getElementById("u"+i);
}
arr=[9,1,2,3,4,5,6,7,8];
i=arr.length; //i=9
while(i){
let j=Math.floor(Math.random()*i--);
[arr[j], arr[i]] = [arr[i], arr[j]];
}
// 随机设置隐形图片
transprant_img=arr[2];
document.getElementById("u"+transprant_img).style.opacity=0.5;
console.log("设置隐形的图片:"+transprant_img)
for (var i=1;i<=9;i++){
// 第arr[i-1]个div 和 第i个div 交换位置
divs[i].style.left=lefts[arr[i-1]]+"px";
divs[i].style.top=tops[arr[i-1]]+"px";
// 第i个图片在第arr[i-1]的位置上
value_div[arr[i-1]]=i;
}
console.log("shuffle之后的序列:"+value_div)
judge();
}
点击移动:
如果到达一定步数,需要强制交换图片, 如果成功,则可以结束游戏
判断是否成功:123456789从左到右,从上至下顺序排列,当图片对应的数字与123456789对应相等时,即图片对应的数字顺序排列,则成功,图像完整。
// 判断是否成功
function judge(){
var count=0;
for(var i=1;i<=9;i++){
if(value_div[i]==i){
count+=1;
}else{
break;
}
}
if(count==9){
tan_show_close("你赢了!!!你的步数是:"+move_walks);
return 1;
}
return 0;
}
判断是否强制交换:
当步数达到20步,却仍旧没有成功,将强制交换,强制交换后如果无解,需要用户输入自由交换的图片位置进行自由交换
强制交换
var change_from=arr[3]
var change_to=arr[5]
moveByPos(change_from,change_to);
判断交换后是否有路径:
判断布局是否有解,首先,要弄清楚什么是逆序数。逆序数,即一个数字序列,将其中所有数字依次两两对比,若大数在前,小数在后,那么这就是一对逆序数。对于33的八数码问题,若有解,逆序数对数必须是偶数,并且33的序列不管怎样移动都不会改变逆序数的个数。所以我们只需要判断当前图片id序列的逆序数个数是否是偶数,从而判断出是否有路径。
//判断是否有路径(逆序数)
function judgeHaveSolution(){
console.log("判断是否有解,空格数字是:"+transprant_img)
var inversion_number=0; //逆序数
// 正常的求逆序数
for (var i = 1; i < value_div.length; i++) { //i在前,j在后,求逆序数,从1求到9,要去掉空白块再排列
if(value_div[i]==transprant_img) continue;
for (var j = i+1; j < value_div.length; j++) {
if(value_div[j]==transprant_img) continue;
if(value_div[i]>value_div[j]){
inversion_number++;
}
}
}
console.log("数字排列串:"+str+" 逆序数个数"+inversion_number)
// 逆序数为偶数,则返回有解,反之返回无解,
if(inversion_number%2==0) return true;
else {return false};
}
人为交换:
当强制交换后,布局仍然无解,则用户输入自由交换的位置,进行人为交换
moveByPos(change_from,change_to);
2、自动演示部分:
这里就涉及到一些算法问题了。 拼图游戏其实就是 : N数码问题 , 而我们写的是 3 * 3 的 , 所以就是 8 数码问题的求解 总结一下 , 我们需要做的事情 包括以下几个 :
1.判断8数码问题是否有解 (其实就是判断该拼图是否可以还原,也就是布局是否有解,也就是是否存在路径,也就是逆序数是奇是偶,该部分在前面已经叙述过,此处就不重复叙述了)
2.求解(寻找复原路径)
3.渲染(根据找出的复原路径在页面中渲染出来)
所以此处最为关键的是求解(寻找复原路径部分),也就是路径寻找问题。此类问题大多可以用图的遍历算法解决,但如果这些问题的图却不是事先给定的,从程序中读入的,而是由程序动态生成的,则称这样的图为隐式图。
那么我们现在来分析一下,八数码问题的状态空间,如果把每个状态看成图的一个结点,每个状态转移看成图的一条有向边(边上无权),且可以由下图可知,程序动态生成的图很可能重复,也就是说八数码问题的状态很可能重复走到,即存在重复结点,所以由此可见,八数码问题的状态空间为一个隐式图。
到此处,八数码问题的求解,复原路径的寻找,也就归结为求图上的最短路径问题。
在此处我们先采用容易理解的广度优先搜索
现在我们已知BFS的相关概念 ,那么如何结合到8数码问题中呢?
1、首先我们需要将 8 数码中 1-9这 九个数每一种组合当做一种状态 ,那么按照排列组合定义 , 我们可以求出 可能存在的状态数 : 9!
2、对 8 数码的每一种状态转换为代码的表达方式 , 在此作者是通过 二维数组的形式
为什么选择二维数组?因为对1的移动限定是有一定空间边界的,比如1如果在第二行的最右边,那么1只能进行左上下三种移动方式。通过二维数组的两种下标可以很方便的来判断下一个状态的可选方向
3、将每种状态转化为二维数组后,就可以配合广搜来进行遍历。初始状态可以设定为广搜中图的第一层,由初始状态通过判断1的移动方向可以得到不大于4种状态的子节点,同时需要维护一个对象来记录每个子节点的父节点是谁以此来反推出动画的运动轨迹及一个对象来负责判断当前子节点先前是否已出现过,出现过则无需再压入队。至此反复求出节点的子节点并无重复的压入队
4、状态压缩:在遍历状态的过程中,可以将二维数组转化为数字或字符串,如123456789。在变为一维数组后便可以直接判断该状态是否等于最终状态,因为从数组变为了字符串或数字的基本类型就可以直接比较是否相等。如果相等那么从该节点一步步反推父节点至起始节点,得到动画路径
广度搜索简易流程框图
但是广度优先搜索遵循从初始结点开始一层层扩展直到找到目标结点的搜索规则,它只能较好地解决状态不是太多的情况,承受力很有限。如果扩展结点较多,而目标结点又处在较深层,就比如本题中每个结点都能衍生出4个子结点,扩展结点加起来是很多的,采用前文叙述的广度搜索解题,搜索量巨大是可想而知的,往往就会出现内存空间不够用的情况。
所以我们采用了对广度优先的搜索方式进行了改良改造的 双向广度搜索 ,使搜索尽快接近目标结点,减少空间和时间上的复杂度。
所谓双向广度搜索指的是搜索沿两个方向同时进行:(1)正向搜索:从初始结点向目标结点方向搜索;(2)逆向搜索:从目标结点向初始结点方向搜索;当两个方向的搜索生成同一子结点时终止此搜索过程。
广度双向搜索通常有两种方法:(1)两个方向交替扩展;(2)选择结点个数较少的那个方向先扩展。方法(2)克服了两方向结点的生成速度不平衡的状态,可明显提高效率。
另外,由于中间会出现大量的重复状态,重结点很多,所以还需要优化的一点就是判重(也就是建立状态数字串(一个int数据)和是否出现(一个bool数据)之间的联系)。我们本可以开两个987654321大小的bool数组,如若某个序列出现,就将其value值置为1或2(正向搜索1,反向搜索2),但是这样空间肯定不允许,于是我们很自然想到使用map,map是STL的一个关联容器,每一个关键值映射一个唯一的value值,对应我们题目中一个状态对应0或1或2,判断状态是否被扩展,但是我们知道,map的实质是使用红黑树,在有需要排序的问题中使用更加方便,与map类似的有unordered-map,使用哈希表判重,在处理查找问题中效率高,所以本例使用unordered-map存储已搜索到的结点,假设m[key]=value表示已搜索到的key状态表示的棋盘,当value=1表示棋盘由BFS从前到后搜索到,则当value=2表示棋盘由BFS从后到前搜索到。
双向广度搜索+Hash主要代码:
function doubleBFS(){
// first当前状态的序列
//q1:从前向后遍历
q1.push(first);
dis.add(first,1);
vis.add(first,1);
route1.add(first,"");
//q2从后往前遍历
q2.push(last);
dis.add(last,1);
vis.add(last,2);
route2.add(last,"");
//q1,q2中元素都非空才可循环,找到答案前,任意一边为空,都说明路径不可达
while(!q1.empty() && !q2.empty()){
//广度双向搜索,选择节点个数较少的那个方向先扩展
if(q1.dataStore.length < q2.dataStore.length){
str1 = q1.front();
q1.pop();
flag = 1;
}
else{
str1 = q2.front();
q2.pop();
flag = 2;
}
//选择二维数组形式,以便于判断下一个状态的可选方向
toMatrix(str1);
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
//找空格在的位置
if(m[i][j]== transprant_img){
BFSx = i;
BFSy = j;
break;
}
}
if(m[i][j]==transprant_img) break;
}
// 移动9的位置,并修改m数组的值,m数组的值是str1的位置图
for (var i = 0; i < 4; i++) {
str2 = ""; //每次寻找时都清零,不然会累计
var tx = BFSx + dir[i][0];//dir上左左右四个方向
var ty = BFSy + dir[i][1];
if(inBoundary(tx,ty)){ //就是c++里的swap
var temp = m[BFSx][BFSy];
m[BFSx][BFSy] = m[tx][ty];
m[tx][ty] = temp;
// 原来函数里的tostring功能
for (var j = 0; j < 3; j++) {
for (var k = 0; k < 3; k++) {
str2 += m[j][k];//str2是str1移动后的位置
}
}
if(!dis.containsKey(str2)){
dis.add(str2,dis.getValue(str1) + 1) ;
vis.add(str2,vis.getValue(str1) );
str = i.toString();
if(flag == 1 ){
q1.push(str2);
route1.add(str2,route1.getValue(str1) + str) ;
}else if(flag==2){
q2.push(str2);
route2.add(str2,route2.getValue(str1) + str) ;
}
}
else{
str = i.toString();
//从前向后
if(flag == 1 ){
route1.add(str2,route1.getValue(str1) + str) ;
}else if(flag==2){
route2.add(str2,route2.getValue(str1) + str) ;
}
//如果两个value值相加等于3,则表示找到路径
if(vis.getValue(str1) + vis.getValue(str2) == 3){
//计算距离
var ans = dis.getValue(str1) + dis.getValue(str2) -1;
var ahead_route;
var later_route;
var change_later_route = "";
var r11 = route1.getValue(str1);
var r12 = route1.getValue(str2);
var r21 = route2.getValue(str1);
var r22 = route2.getValue(str2);
//为了尽快地找到共同的结点,选择长的
if(r11 && r12) r11.length>r12.length? ahead_route=r11 : ahead_route=r12 ; //三元选择符输出长的
else if(!r11 && r12) ahead_route=r12; //r11是null,输出r12
else if(r11 && !r12) ahead_route=r11; //r12是null,输出r11
// 回推的方向还要再颠倒下...
if(r21 && r22) r21.length>r22.length? later_route=r21 : later_route=r22 ; //三元选择符输出长的
else if(!r21 && r22) later_route=r22; //r21是null,输出r22
else if(r21 && !r22) later_route=r21; //r22是null,输出r21
later_route = later_route.split('').reverse().join('');
for (var i = 0; i < later_route.length; i++) {
if(later_route[i]=="0") change_later_route += "1";
else if(later_route[i]=="1") change_later_route += "0";
else if(later_route[i]=="2") change_later_route += "3";
else if(later_route[i]=="3") change_later_route += "2";
}
route = ahead_route + change_later_route ;
return ans;
}
}
//恢复现场
var temp = m[BFSx][BFSy];
m[BFSx][BFSy] = m[tx][ty];
m[tx][ty] = temp;
}
}
}
return -1;
}
性能测试:
开挂试试:
性能分析与改进
改进:
不管是改进前还是改进后,主要都是在广度搜索时耗时较长,直接就提一下改进思路。
第一方面是八数码状态存储及判重部分
八数码一个状态可以看作含九个元素的字符串,每种状态对应一个字符串,因此我们只需利用队列保存字符串作为状态即可。判重部分,STL中的Map和Set自带判重功能,但是在时间复杂度上需要乘上一个log,而hash则可以完美的实现O(1)处理,复杂度较小。所以我们改进采用Hash判重。
第二方面是广度搜索上
八数码问题采用单向广度暴力搜索是可以的,但是在八数码问题中,每个状态可以衍生四个不同的状态,寻找路径的过程中,结点个数非常多,搜索量很大,所以我们采取改进的双向广度搜索,以实现更快更好的查找路径。
2.2.2 Github的代码签入记录
2.2.3 单元测试
测试是以网页形式展现,故代码涉及到js,html,css多个文件,所以就不贴代码了,详细代码见GitHub中findRount.xxx
2.2.4 遇到的代码模块异常或结对困难及解决方法
1,困难描述
把图片放在九个小方块中,图片和页面方块大小不一,在图片等比例缩放时总是出现意想不到的error;
点击图片时触发move()函数,但图片没有按照预想移动位置;
for循环中给String字符串的单个字符重新赋值,发现字符没有变化
2,解决尝试
这个问题在现在看来可以说真的很弱智,但刚开始是真的纠结了挺久,因为不是很了解前端css,甚至想在js中加一段代码统一像素再使用图片,后面css直接修改图片大小,真的很不明白自己为什么卡在这边,九个小块存放图片的不同部分则归结于图片不同位置的定位问题。(问题很弱智,嘲笑一下自己);
console.log()定位问题所在(虽然是个笨方法,但也挺方便);
发现赋值失败时简直怀疑人生,后来百度一下发现是js的特别之处,于是改用赋值新的字符串的方法
3,是否解决
是
4,有何收获
bug和error真是一波未平一波又起,层出不穷,绵延无尽。。。。。但我们还是兵来将挡水来土掩,熬了很多夜,掉了很多宝贝头发,不管是换个方法,还是拆东补西,反正最后改完了所有代码
只要活的时间够长,就没有改不完的bug,这是我悟到的如何与bug相处第一守则
2.2.5 评价你的队友
值得学习的地方:
太多方面都值得我学习,非常优秀的女孩,自学能力强,不焦不燥,乐观向上,尽管过程中各种困难,却仍旧不放弃,积极热情,给了我很多的鼓励。
需要改进的地方:
To翔云,我觉得叭,其实你很优秀,不要妄自菲薄,自信点,这一方面我们两个人都需改进,加油,我们可以的!
2.2.6 此次结对作业的PSP和学习进度条
第N周 | 新增代码 | 累计代码 | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 300 | 300 | 11 | 11 | 原型设计工具的安装使用 |
2 | 800 | 1100 | 30 | 41 | 了解了关于css的基本语法,数字华容道有解和最优解的算法,双向广搜的原理 |
3 | 700 | 1800 | 15 | 56 | css和js的具体知识点和使用,接口测试工具的了解和使用 |
4 | -300 | 1500 | 16 | 72 | 页面美化,css模板的使用 |
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | ||
估计这个任务需要多少时间 | ||
开发 | 1000 | 1600 |
需求分析 (包括学习新技术) | 80 | 150 |
生成设计文档 | 70 | 80 |
设计复审 | 30 | 40 |
代码规范 (为目前的开发制定合适的规范) | 40 | 40 |
具体设计 | 600 | 600 |
具体编码 | 2600 | 3000 |
代码复审 | 200 | 300 |
测试(自我测试,修改代码,提交修改) | 300 | 400 |
报告 | 120 | 150 |
测试报告 | 50 | 50 |
计算工作量 | 10 | 10 |
事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 5130 | 6450 |