DBFS解二阶魔方——一次c++学习之旅
目录
前言
构思解法
优化方案
代码及详细注释
1.定义魔方的一个状态
2.状态初始化
3.转动
4.查重
5.双向广搜
6.输出
7.输入
8.主函数
几段实用代码
前言
本人是c++初学者,对魔方有浓厚的兴趣,希望用c++最小步还原魔方。本文是我对DBFS还原二阶魔方的详细思考过程,文章末尾还记录了学习c++的几条笔记。希望各位大神批评指正!
参考文章:
写一个解二阶魔方的程序 - 终末之冬 - 博客园
研究非常透彻,还做了交互式网页。
二阶魔方求解算法研究(44页)-原创力文档
对最小步还原二阶魔方的算法的详尽剖析,优化也做得非常好。
https://pan.baidu.com/s/1inzGNldd_EHc6nE4pvaYqw
这是本文优化前后的代码。提取码:5d8y
构思解法
一、DBFS而非BFS
魔方状态数有7!*3^6=3671460种,从优化后的DBFS算法看来,30ms搜索25000种状态,10s之内是可以单向广搜完370万种状态的,内存占用预计不会超过100MB。单向广搜费时费空间,毕竟我的目的并非用它测试c++和电脑的运行效率。
二、预计搜索量及时间
由二阶魔方的对称性,六个面随意转动相当于只转动其中的三个面,每个面有顺时针90度、180度、270度三种,因此每一步有9种扩展方法。又已知二阶魔方的QTM最小步数为14,HTM(上述的9种扩展{U,U2,U’,F,F2,F’,R,R2,R’})的最小步数是11。每一步对某一个面的转动是完全的,下一步不需要考虑搜索上一步转动的面,因此第一步搜索有9种扩展方法,以后每一步只有6条分支。
采取双向广搜DBFS,每一个广搜分支最大深度为6,最大搜索量不会超过9*6^5+9*6^4=69984+11664=81648,预计最长搜索时间小于100ms,这比单向广搜优化了不少。
三、定义魔方状态
最简单的想法是定义21个面的颜色(<b,h,d>角块不参与转动,24-3=21),每次转动进行12次赋值。因为每个块的朝向和位置是独立的,我们可以分别定义处于7个位置上的块的编号和朝向,每次转动只需要赋值8次,而且节省空间。
魔方状态的定义,理论上最少只需要(3+2)*7=35bit(0~6共7种位置,012共3种朝向,分别需要3个、2个bit来存储),一个long int(64bit)就能够按位存储一种魔方状态。然而频繁按位读取、写入比较麻烦,因此我采用两个short int[7]数组,共14个元素,记录每一种状态的位置和朝向,考虑用string记录搜索路径。
四、查重
首先用to_string()将14个位置和状态数连接成字符串,作为成魔方的标识,再运用map容器查找是否已搜索过。
优化方案
一、用unordered_map代替map
unordered_map散列哈希表的时间O(1)比map红黑树O(logn)快。结果证明,unordered_map是map查找时间的一半以下。
二、位运算及位存储
查重时,运用移位的方法,分别把两个数组的前6个元素存储到同一个int中,(3+2)*6<32,来作为魔方状态的标识。这样既减少了to_string()时间消耗,查找也更快,时间直接减少至原来的1/3。
按位int比string存储路径快得多,每4位存储一步搜索路径,加上必要的终止符“1111”(15),(6+1)*4<32,时间减少到原算法的一半。
%16,、%4、%2可以用&15、&3、&1代替,按位与 比 取余快得多。
三、int改为short int
代码中大部分数据是小于10的整型,int存储浪费空间,可以考虑只占用1个字节的char。但是char字符数组‘\0’与‘0’无法区分,操作不方便,因此使用short类型。
四、查重时免查己方路径
从结果看来,6步以内重复状态数很少,多扩展节点数也就几十到一百个,但是查询己方路径的时间开销远大于重复扩展的时间,因此可以免查己方路径。
代码及详细注释
1.定义魔方的一个状态
typedef struct Cube{int pos[7];int state[7];string path;int last;}st;
state[i]表示第i∈{0,1,2,3,4,5,6}个位置的现有角块序号,如图2表示为state[7]={2,3,6,1,0,4,5}
path路径,比如从起始节点通过{R,U2,F}扩展而来的状态,path=“613”
last上一次转动,last∈{0,1,2,3,4,5,6,7,8}
pos[i]表示第i个位置朝向,怎么定义朝向呢?可以参考盲拧高、中、低级色的定义:
定义上、下面为0号面,前、后面为1号面,左、右面为2号面。不妨通过整体旋转使得7号位黄色或白色向下,那么对于0~6号位的角块,黄色或白色在几号面上,它的朝向就是几。
如图4,7号位置是<白,橙,绿>,白色已经在底面。此时5号位的<黄,蓝,红>的黄色面朝前(即1号面),因此pos[5]=1。
2.状态初始化
st org,rest;
org为被打乱需要复原的状态,rest目标状态
void shuffle()
{int i;for(i=0;i<7;i++){rest.pos[i]=0;rest.state[i]=i;}
设置目标状态rest每个白面都朝上,黄面都朝下,每个块的序号与位置对应int mv[11]={1,7,3,0,7,0,7,4,0,4,0};//int mv[10]={7,1,8,1,3,6,0,5,6,0};//int mv[7]={7,1,4,6,1,7,0};
三组从rest开始打乱的测试公式,分别为11、10、7步org=rest;for(i=0;i<11;i++){org=exchange(mv[i],org);}rest.path="";org.path="";rest.last=10;
org.last=10;
初始化搜索路径,last=10本来不存在,但能达到第一次扩展进行9种旋转的目的。
}
3.转动
将{U,U2,U’,F,F2,F’,R,R2,R’}映射到0~8每个数字,记一次转动为int num,
st exchange(int num,st sat) //num为0~8转动,sat为父状态
{int x=num/3,y=num%3+1,qi,ho,i;st wen=sat; //新的子状态,这样拷贝似乎不会出问题~int change[3][4]={{2,1,0,3},{0,1,5,4},{2,6,5,1}};//change的每组4个元素,分别代表U、F、R面参与转动的有序位置循环for(i=0;i<4;i++){qi=change[x][i]; //转动前的位置qi,i与qi、ho一一对应ho=change[x][(i+y)%4]; //转动后的位置howen.state[ho]=sat.state[qi]; //将sat的qi位置块赋值给wen的ho位置块if(y==2) //若旋转180°wen.pos[ho]=sat.pos[qi]; //所有块转动前后朝向不变else if(sat.pos[qi]==x) //若白/黄面在转动的面上,转动前后朝向不变wen.pos[ho]=x;else //操作是90°或270°, 且白/黄面不在转动的面上wen.pos[ho]=(3-sat.pos[qi]-x)%3; //ho朝向是qi朝向除去转动面外的另一个数//例如:pos[qi]==1,转动2号面(特指R面),必然有pos[ho]==0}wen.path=sat.path+ to_string(num); //int转string并添加在path末尾wen.last=num;return wen;
}
4.查重
#include<map>
map<string,string> app[2];
map<string,string>::iterator ite;
bool found=false; //指示是否找到
string fro,bhd; //成功找到路径后,分别存储两个搜索方向的路径bool isappear(st sat,short p) //sat某一状态,p=0或1,区分两个搜索方向
{int i,dex=(p+1)%2; //p=0则dex=1,p=1则dex=0string wt="";for(i=0;i<7;i++) //将pos、state每个元素顺次连接成字符串,作为该状态的识别码{wt+=to_string(sat.state[i]);wt+=to_string(sat.pos[i]);}ite=app[dex].find(wt); //在对面容器是否搜索过wt?if (ite!=app[dex].end()) //对面搜索过,表示已成功找到路径{fro=sat.path;bhd=ite->second;found=true;return true;}else //对面未曾搜索过{ite=app[p].find(wt); //自己是否曾经搜索过?if(ite==app[p].end()) //自己也没搜索过{app[p][wt]=sat.path; //wt为key,对应path值,加入自己这边的容器return true;}else return false; //自己搜索过,不用扩展节点了}
}
5.双向广搜
st pcr[2][70000]; //用数组构造先进先出队列
int DBFS()
{pcr[0][0]=org;pcr[1][0]=rest; //初始、目标状态入队isappear(org,0);isappear(rest,1); //在map容器里标记 int i,dex[2]={1,1},mk,j, count=-1; //dex[i]表示在队尾添加节点时的数组下标st now,tp;while (!found){count++;for(i=0;i<2;i++){now=pcr[i][count]; //分别取pcr[0]、pcr[1]的第count个节点扩展mk=(now.last)/3; //父节点最后一次转动for(j=0;j<9;j++) //9种转动{if (j/3!=mk) //如果它上一次不转这个面{tp=exchange(j,now); //按j转动if (isappear(tp,i)) //若可以扩展{pcr[i][dex[i]]=tp; //加入队尾dex[i]++;if (found) //若成功碰头{cout<<"search joints:"<<dex[0]+dex[1]<<endl;//输出总搜索节点数return 0;}}}}}}return 0;
}
6.输出
例如:打乱公式shf为:UFUF2R2URF2U2R’F’
还原公式slv为:FRU2F2R’U’R2F2U’F’U’
而搜索得到的是:fro=”074030”, bhd=”84163”
分别对应fro:UR2F2UFU ,bhd:R’F2U2RF
欲得shf:反序读取fro并对bhd进行处理(转动面不变,90度与270度互换,180度不变)
欲得slv:反序读取bhd并正序处理fro
void output()
{short i,t1,t2;string shf="",slv="",tp=""; //shf打乱步骤,slv解决公式,二者互逆string output[9]={"U","U2","U'","F","F2","F'","R","R2","R'"};char c1[20],c2[20],c; strcpy(c1,fro.c_str());strcpy(c2,bhd.c_str()); //把fro、bhd从string转化成字符数组,再拷贝到c1、c2中t1=fro.size();t2=bhd.size();for(i=0;i<t1;i++){shf+=output[c1[i]-48]; //利用char的字符、数字两重性,如:‘9’-‘0’==9c=c1[t1-1-i]-48; //逆序读取tp+=output[c/3*3+2-c%3]; //处理后,再串联成字符串}for(i=0;i<t2;i++){slv+=output[c2[i]-48];c=c2[t2-1-i]-48;shf+=output[c/3*3+2-c%3];}slv+=tp;cout<<"shuffle:"<<shf<<endl;cout<<"steps:"<<t1+t2<<endl;cout<<"solution:"<<slv<<endl;
}
7.输入
void input()
{short i;for(i=0;i<7;i++)scanf("%hd",&org.state[i]);for(i=0;i<7;i++)scanf("%hd",&org.pos[i]);
}
因为懒,没有写检查输入是否合法的语句,但千万要注意输入的格式!前7个是位置为0~6的块的编号,不重不漏。后7个是pos,pos[i]∈{0,1,2},且pos[i]之和为3的倍数,否则得到的结果是错误的。
样例输入:
2 4 1 5 0 6 3 1 0 2 2 1 0 0 (14个数字,每敲入一个后,按回车换行)
样例输出:
search joints:2896
shuffle:F’UR2U2F’U’RF2
steps:8
solution:F2R’UFU2R2U’F
76.888000ms
8.主函数
int main()
{clock_t t1=clock();shuffle();input(); //若注释掉这一行,可以用shuffle()里的打乱公式进行测试DBFS();output();float dt=clock()-t1;printf("%fms",dt/1000);return 0;
}
几段实用代码
以下是笔者学习过程中认为挺实用的代码。
1.测量时间间隔
#include<time.h>
clock_t start=clock();…主程序…float duration=clock()-start;
printf("%f ms",duration/1000);
2.自定义数据结构
typedef struct Student{int id;char *name;}st;Student是结构名称,st是调用关键字,调用如下:st stu1;
st.id=20220502;
3.字符串
字符串不能直接赋值,只能拷贝:strcpy(c1,c2); //将c2拷贝到c1
区别于拷贝数组:memcpy(b,a,sizeof(a));
字符数组:char p[]=”I am a student”;
c2拼接到c1末尾:strcat(c1,c2);
获取长度(注意与 sizeof(c1) 区别)c1.size() 或者 c1.length()
string 转 char 数组:char c1[]=”I am a student”;string c2=c1.c_str();
反转字符串:#include<algorithm>reverse( c1.begin(), c1.end() );
4.队列
#include<queue>//或者priority_queue用法类似
定义队列:queue <string> a;
队头元素:a.top
非空:if ( !a.empty() )
元素个数:a.size()
在队尾加入元素:a.push(i)
弹出队头:a.pop()
5.map容器
#include<map> //unordered_map用法类似
声明:map<string,int> app;
迭代器声明:map<string,int>::iterator it;
赋值有3种方法,最简洁的一种:app[“one”]=1;
查找:it=app.find(“two”);if (it==app.end())//若为真,则未找到;若为假,则容器中已存在
遍历访问:正向遍历:map<string,int>::iterator it;for( it=app.begin; it!=app.end(); it++ )逆向遍历:map<string,int>::reverse_iterator it;for( it=app.rbegin; it!=app.rend(); it++ )
6.其他
(1)三目运算符
Money=(age>12) ? 80 : 20;
i ? isappear1(tp) : isappear2(tp);
变量d=(判断语句c)?(a):(b)//如果c真,执行a或者将a赋值给d,反之b
(2)指针操作
用指针访问优缺点并存,缺点是容易出错,优点提高运行效率、简洁。
(3)预定义函数
定义函数:#define Swap(a,b) {int tp=a;a=b;b=tp;}
定义常量:#define LEN “please press any key to continue…”
(4)整型的位运算
乘法:a=a*4 <=> a<<2a=a*7 <=> a=a<<2+a<<1+a
整除:b=b/4 <=> b=b>>2
取余:x=w%8 <=> x=w&7
只有2^n才能移位整除、按位与求余!
(本文完)
DBFS解二阶魔方——一次c++学习之旅相关推荐
- python解魔方程序_写一个解二阶魔方的程序
本文需要读者有一定的魔方基础, 最起码也要达到十秒内还原二阶魔方的水平, 并且手上最好有一个二阶魔方, 否则文中的很多东西理解不了. 另外, 这里使用的算法是我自己写着玩的, 如果你需要更成熟和专业的 ...
- 【项目实践】二阶魔方搜索算法
前言 课程<智能控制基础>课后作业要求编写一个二阶魔方搜素求解的算法,由于本人的代码水平真的不行,只能"面向互联网编程",前前后后找了不少资料,也确实学习到一点东西 ...
- 黑魔方之《计算机学习金手册》(无格式纯文本版)
讨论1 为什么学? 现在已经很少有人再提这样的问题了. 因为计算机的普及已经实实在在地渗透到人们生活的方方面面.你.我.他,还有更多的人正在享受着计算机带来的高效.便利.神奇和快乐.几乎没有人愿意拒绝 ...
- 【二阶魔方还原】第十次OJ的总结
问题描述 二阶魔方是 2x2x2 的立方体结构魔方,它共有 8 块,如下图所示: 图1 二阶魔方示意图 我们可以定义魔方作为一个正六面体的六个面为 F(ront), B(ack), U(p), D( ...
- 二阶魔方还原 Rubik’s Cube 双向广度优先搜索
1. 算法简介 使用搜索算法完成二阶魔方从任意初始状态向目标状态的操作转换. 根据已有的研究,二阶魔方的上帝之数为11(进行FTM计数)或14(进行QTM计数),本算法采用QTM计数对 ...
- IOS 14.5版本之解档和归档的API学习
IOS 14.5版本之解档和归档的API学习 第一部分 回顾一下老api的使用,将对象持久化至硬盘里面. 1.为什么我们要学习解档和归档, 有什么作用.当 plist 文件存储无法满足我们的需求的时候 ...
- matlab ode45 二阶微分,matlab关于ode45解二阶微分方程的困惑
matlab关于ode45解二阶微分方程的困惑 matlab关于ode45解二阶微分方程的困惑 一个二阶微分方程: y''+y'+y=sin(t) 初始条件为y(0)=5,y'(0)=6. 过程: 先 ...
- 魔方(4)二阶魔方、六阶魔方、七阶魔方
目录 二阶魔方 1,二阶魔方与三阶魔方的关系 2,二阶魔方的定位 3,二阶魔方的盲拧 六阶魔方 七阶魔方 二阶魔方 1,二阶魔方与三阶魔方的关系 可以理解为,二阶魔方就是三阶魔方的八个角块. ...
- python解常微分方程龙格库_excel实现四阶龙格库塔法runge-kutta解二阶常微分方程范例.xls...
excel实现四阶龙格库塔法runge-kutta解二阶常微分方程范例,rungekutta,四阶rungekutta法,rungekuttamatlab,四阶rungekutta,rungekutt ...
最新文章
- NR 5G 网络切片
- 巧用ActionFilterAttribute实现API日志的记录
- 1.5 编程基础之循环控制 35 求出e的值
- Python flask 特殊装饰器 @app.before_request 和 @app.after_request 以及@app.errorhandler介绍
- 洛谷 P3372 【模板】线段树 1
- 物联卡认识易陷入的几大误区
- Python_Django_01_day
- Ran 0 tests in 0.000s
- 【无标题】RC抽取工艺文件(三)Layer map错误
- cf1009 C. Annoying Present
- Kubeadm部署-Kubernetes-1.18.6集群
- iOS的电量测试(Sysdiagnose)
- 关于实名认证上线时无法立即返回实名认证结果的问题
- python安装matplotlib库三种失败情况
- 企业抖音账号流量提升3步法,新号也能过百万播放量
- mysql查询日期_mysql 查询当前日期
- Android:单位和尺寸(px、pt、dip、dp、sp、layoutparams)
- 【数据库】第九章习题
- 怎样设计MindMapper中的导图结构
- 如何给你的微信公众号排版