目录
前言
构思解法
优化方案
代码及详细注释
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个面的颜色(角块不参与转动,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步以内重复状态数很少,多扩展节点数也就几十到一百个,但是查询己方路径的时间开销远大于重复扩展的时间,因此可以免查己方路径。
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。
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种旋转的目的。
}
将{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]; //转动后的位置ho
wen.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;
}
#include
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:"<
例如:打乱公式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
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
int main()
{
clock_t t1=clock();
shuffle();
input(); //若注释掉这一行,可以用shuffle()里的打乱公式进行测试
DBFS();
output();
float dt=clock()-t1;
printf("%fms",dt/1000);
return 0;
}
以下是笔者学习过程中认为挺实用的代码。
1.测量时间间隔
#include
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
reverse( c1.begin(), c1.end() );
4.队列
#include//或者priority_queue用法类似
定义队列:queue a;
队头元素:a.top
非空:if ( !a.empty() )
元素个数:a.size()
在队尾加入元素:a.push(i)
弹出队头:a.pop()
5.map容器
#include
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<<2
a=a*7 <=> a=a<<2+a<<1+a
整除:b=b/4 <=> b=b>>2
取余:x=w%8 <=> x=w&7
只有2^n才能移位整除、按位与求余!
(本文完)