目录

前言

构思解法

优化方案

代码及详细注释

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++学习之旅相关推荐

  1. python解魔方程序_写一个解二阶魔方的程序

    本文需要读者有一定的魔方基础, 最起码也要达到十秒内还原二阶魔方的水平, 并且手上最好有一个二阶魔方, 否则文中的很多东西理解不了. 另外, 这里使用的算法是我自己写着玩的, 如果你需要更成熟和专业的 ...

  2. 【项目实践】二阶魔方搜索算法

    前言   课程<智能控制基础>课后作业要求编写一个二阶魔方搜素求解的算法,由于本人的代码水平真的不行,只能"面向互联网编程",前前后后找了不少资料,也确实学习到一点东西 ...

  3. 黑魔方之《计算机学习金手册》(无格式纯文本版)

    讨论1 为什么学? 现在已经很少有人再提这样的问题了. 因为计算机的普及已经实实在在地渗透到人们生活的方方面面.你.我.他,还有更多的人正在享受着计算机带来的高效.便利.神奇和快乐.几乎没有人愿意拒绝 ...

  4. 【二阶魔方还原】第十次OJ的总结

    问题描述 二阶魔方是 2x2x2 的立方体结构魔方,它共有 8 块,如下图所示: 图1  二阶魔方示意图 我们可以定义魔方作为一个正六面体的六个面为 F(ront), B(ack), U(p), D( ...

  5. 二阶魔方还原 Rubik’s Cube 双向广度优先搜索

    1. 算法简介 使用搜索算法完成二阶魔方从任意初始状态向目标状态的操作转换.         根据已有的研究,二阶魔方的上帝之数为11(进行FTM计数)或14(进行QTM计数),本算法采用QTM计数对 ...

  6. IOS 14.5版本之解档和归档的API学习

    IOS 14.5版本之解档和归档的API学习 第一部分 回顾一下老api的使用,将对象持久化至硬盘里面. 1.为什么我们要学习解档和归档, 有什么作用.当 plist 文件存储无法满足我们的需求的时候 ...

  7. matlab ode45 二阶微分,matlab关于ode45解二阶微分方程的困惑

    matlab关于ode45解二阶微分方程的困惑 matlab关于ode45解二阶微分方程的困惑 一个二阶微分方程: y''+y'+y=sin(t) 初始条件为y(0)=5,y'(0)=6. 过程: 先 ...

  8. 魔方(4)二阶魔方、六阶魔方、七阶魔方

    目录 二阶魔方 1,二阶魔方与三阶魔方的关系 2,二阶魔方的定位 3,二阶魔方的盲拧 六阶魔方 七阶魔方 二阶魔方     1,二阶魔方与三阶魔方的关系 可以理解为,二阶魔方就是三阶魔方的八个角块. ...

  9. python解常微分方程龙格库_excel实现四阶龙格库塔法runge-kutta解二阶常微分方程范例.xls...

    excel实现四阶龙格库塔法runge-kutta解二阶常微分方程范例,rungekutta,四阶rungekutta法,rungekuttamatlab,四阶rungekutta,rungekutt ...

最新文章

  1. NR 5G 网络切片
  2. 巧用ActionFilterAttribute实现API日志的记录
  3. 1.5 编程基础之循环控制 35 求出e的值
  4. Python flask 特殊装饰器 @app.before_request 和 @app.after_request 以及@app.errorhandler介绍
  5. 洛谷 P3372 【模板】线段树 1
  6. 物联卡认识易陷入的几大误区
  7. Python_Django_01_day
  8. Ran 0 tests in 0.000s
  9. 【无标题】RC抽取工艺文件(三)Layer map错误
  10. cf1009 C. Annoying Present
  11. Kubeadm部署-Kubernetes-1.18.6集群
  12. iOS的电量测试(Sysdiagnose)
  13. 关于实名认证上线时无法立即返回实名认证结果的问题
  14. python安装matplotlib库三种失败情况
  15. 企业抖音账号流量提升3步法,新号也能过百万播放量
  16. mysql查询日期_mysql 查询当前日期
  17. Android:单位和尺寸(px、pt、dip、dp、sp、layoutparams)
  18. 【数据库】第九章习题
  19. 怎样设计MindMapper中的导图结构
  20. 如何给你的微信公众号排版

热门文章

  1. 在chrome中设置禁止访问的网站
  2. 工业相机QE-量子转换效率
  3. 兵法三十六计第三计-借刀杀人。
  4. K近邻(K Nearest Neighbor-KNN)原理讲解及实现
  5. 获取安卓系统自带CA证书
  6. papers-06-07
  7. Chrome源码剖析——多线程模型、进程通信、进程模型
  8. “道”与“术”之关系
  9. 应用fiddler,使用har2case 将api参数转成yaml格式
  10. P1014 [NOIP1999 普及组] Cantor 表