在 3×3 的棋盘上,摆有八个棋子,每一个棋子上标有1至8的某一数字。
棋盘中留有一个空格,空格用0来表示。空格周围的棋子能够移到空格中。
要求解的问题是: 给出一种初始布局(初始状态)和目标布局,找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变
(为了简化棋盘,咱们把每一个数字按每行缩成一个长串)html
Input | 初始:123456780 目标:087654321 | 初始:123456780 目标:123456708 | 初始:123456780 目标:123456870 | 初始:123456780 目标:123456780 |
---|---|---|---|---|
Ouput | 28 | 1 | No solve | 0 |
ios
这是一个很是经典的搜索问题,可是有时候咱们会发现从 初始状态 到达不了 目标状态 ,这时候咱们就须要提早判断是否有解 ( 否则无解的时候搜索算法会一直搜 )web
先说结论:算法
把棋盘状态表示成一维的形式,求出除 0 以外全部数字的逆序数之和,称为这个状态的逆序
(也就是每一个数字前面比它大的数字的个数的和)
若两个状态的逆序 奇偶性相同 ,则 可相互到达,不然 不可相互到达数据结构
例如: 123456780 和 213456780
逆序对和数为 28 和 27
因此没法互相到达app
证实其必要性: 若是两个状态逆序对的和奇偶性不一样,则必然不能互相抵达svg
首先,对于
的格子,当
元素与任意元素交换(进行移动),表如今压缩以后的长串数字上分别是:
左移:将
与前一位交换,此时总逆序奇偶性不变函数
由于 自己不算在逆序对计算内,整体顺序没有改变布局
右移:将
与后一位交换,同左移
上移:将
与前第三位交换,此时总逆序奇偶性不变优化
假设被 交换元素是 A,中间元素有两个,分别是B,C
- 若是有 A>B 和 A>C 则整体逆序对个数 -2 ,奇偶性不变
- 若是有 A>B 和 A<C 则整体逆序对不变 ,奇偶性不变
- 若是有 A<B 和 A>C 同理 2
- 若是有 A<B 和 A<C 则整体逆序对个数 +2, 奇偶性不变
下移:将 与后第三位交换,同上移
证实其充分性: 若是两个状态逆序对的和奇偶性相同,则一定能够互相抵达
能够看到这两个状态是能够相互抵达的
对应:
其余左右移同理
也就是说任意 左移和右移 的步骤均可以相互抵达,又由于左移和右移不改变逆序对奇偶性
则有
能够将全部偶数逆序对和的状态转化成:0 1 2 3 4 5 6 7 8
能够将全部奇数逆序对和的状态转化成:0 2 1 3 4 5 6 7 8
由于根据前面的左右移无限制证实,你能够经过有限次操做将最大的元素放在最后面,同时把次大的元素放在倒数第二位而不打乱最后一位,前推同理,直到达到状态 0 1 2 和 0 2 1 此时由于位数限制,没法继续这样操做,因此证得
一样的因为全部偶状态和全部奇状态都收束与不一样的一个状态,根据移动的可逆性,故若是两个状态逆序对的和奇偶性相同,则一定能够互相抵达
int P(string S){ int jShu = 0; for(int i=0; i<9; i++) for(int j=0; j<i; j++) if(S[i]>S[j]&&S[j]!='0') jShu++; return jShu; //返回逆序对和,把两个状态的逆序对和都%2,相等则有解,不等则无解 }
判断了是否有解,接下来就是看如何搜索,总所周知,最基础的搜索算法有两种: 深搜(DFS) 和 广搜(BFS)
对于朴素的DFS和BFS而言,显然这道题用广搜更好,由于是找最小步骤,若是是深搜,若是不知道最小步数限制,则会一直在一个分支中搜索,并且第一次搜到的解未必是最小解,而广搜则会更快地找到最小解 ( 由于是平铺的往下搜,因此第一次碰到的解必定是最小解 ),因此说在这里只贴朴素BFS的代码
能够用,但因为没有判重,大步数会超时(甚至死循环
例如:
123456780
087654321
28
inti.h头文件代码传送
#include"init.h" queue<string> NS; //记录字符的队列 queue<int> Zhixy; //记录步数,0的位置压缩后的队列 int MinZhi; int main(){ if(Init()){ cout<<"No Solve"<<endl; return 0; } NS.push(S); for(int i=0; i<9; i++) if(S[i]=='0') Zhixy.push((int)(i/3)*10+i%3); while(!NS.empty()){ string S2 = NS.front(); NS.pop(); int A = Zhixy.front(); Zhixy.pop(); int Zhi = A/100, x = (A/10)%10, y = A%10; //用了数据压缩,Zhi表明目前状态到原状态步数,x和y表明如今0的位置,把它们压成一个数存进队列节约空间 if(S2==S_Goal){ //找到便可退出,一定是最短 MinZhi = Zhi; break; } if(x+1<3){ NS.push(exChange(S2, x+1, y, x, y)); Zhixy.push((x+1)*10+y+(Zhi+1)*100); } if(y+1<3){ NS.push(exChange(S2, x, y+1, x, y)); Zhixy.push(x*10+y+1+(Zhi+1)*100); } if(x-1>=0){ NS.push(exChange(S2, x-1, y, x, y)); Zhixy.push((x-1)*10+y+(Zhi+1)*100); } if(y-1>=0){ NS.push(exChange(S2, x, y-1, x, y)); Zhixy.push(x*10+y-1+(Zhi+1)*100); } } cout<<MinZhi<<endl; TimeB("传统BFS算法"); string B = S_Goal; return 0; }
由于每一个程序有一部分代码是彻底同样的,因此就改为头文件了XD
#include<sys/time.h> #include<algorithm> #include<iostream> #include<cstring> #include<queue> #include<cmath> #include<ctime> #include<map> using namespace std; string S, S_Goal; struct timeval start, end; //测时函数 void TimeA(){ gettimeofday(&start,NULL); } void TimeB(string S){ gettimeofday(&end,NULL); cout<<S<<": "<<(end.tv_sec-start.tv_sec)+(double)(end.tv_usec-start.tv_usec)/(double)1000000<<"s"<<endl; } //字符串元素位置替换 string exChange(string S3, int x1, int y1, int x2, int y2){ char Bet = S3[x1*3+y1]; S3[x1*3+y1] = S3[x2*3+y2]; S3[x2*3+y2] = Bet; return S3; } //判断是否有解 int P(string S){ int jShu = 0; for(int i=0; i<9; i++) for(int j=0; j<i; j++) if(S[i]>S[j]&&S[j]!='0') jShu++; return jShu; } //初始化,有解返回0,无解返回1 bool Init(){ cout<<"目标九宫格:"; cin>>S_Goal; cout<<"已有九宫格:"; cin>>S; TimeA(); if(P(S_Goal)%2!=P(S)%2) return 1; else return 0; }
对于这两个搜索来讲,很显然有一个优化方法:
BFS:若是当前状态我之前搜过,那么我就不须要继续搜这个状态
DFS:若是当前状态我之前搜过,且当前状态步数比我之前搜到这个状态用的步数多,则不继续往下搜,反之则仍是须要往下搜
这样去重之后,不只能够大幅节省搜索时间,也能够避免死循环的产生
那么怎么判断当前状态是否搜过呢?
C++为咱们提供了一个很是方便的数据结构Map,至于Map是什么,该怎么用
能够看这位前辈博客:https://blog.csdn.net/qq_39836658/article/details/78560819
(另外实际Map因为各类缘由,速度是稍微慢一点的,但对于解八数码问题已经绰绰有余了)
须要限制最大搜索深度,这里限制深度30层,挺慢的,通常都会超时
剪枝思路: 若是当前状态我之前搜过,且当前状态步数比我之前搜到这个状态用的步数多,则不继续往下搜,反之则仍是须要往下搜
inti.h头文件代码传送
#include"init.h" map<string, int> Map; int MinZhi = 30; void DFS(int jShu, string S2, int x, int y){ if(S2==S_Goal){ if(MinZhi>jShu) MinZhi = jShu; return; } //去重部分 if(Map.count(S2)){ if(Map[S2]>jShu) Map[S2] = jShu; else return; } else Map[S2] = jShu; if(jShu>=MinZhi) return; string Bet = S2; if(x+1<3){ S2 = exChange(S2, x+1, y, x, y); DFS(jShu+1, S2, x+1, y); S2 = Bet; } if(y+1<3){ S2=exChange(S2, x, y+1, x, y); DFS(jShu+1, S2, x, y+1); S2 = Bet; } if(x-1>=0){ S2=exChange(S2, x-1, y, x, y); DFS(jShu+1, S2, x-1, y); S2 = Bet; } if(y-1>=0){ S2=exChange(S2, x, y-1, x, y); DFS(jShu+1, S2, x, y-1); S2 = Bet; } } int main(){ if(Init()){ cout<<"No Solve"<<endl; return 0; } int x, y; for(int i=0; i<9; i++) if(S[i]=='0') x=i/3, y=i%3; DFS(0, S, x, y); cout<<MinZhi<<endl; TimeB("传统DFS+Map"); }
大体速度:(不只有步数限深,还有TLE限定呦~
inti.h头文件代码传送
剪枝思路: 若是当前状态我之前搜过,那么我就不须要继续搜这个状态,一样的,第一次搜到的相同状态必定是最短解
#include"init.h" map<string, int> Map; map<string, string> Map2; queue<string> NS; queue<int> Zhixy; int MinZhi; int main(){ if(Init()){ cout<<"No Solve"<<endl; return 0; } NS.push(S); for(int i=0; i<9; i++) if(S[i]=='0') Zhixy.push((int)(i/3)*10+i%3); while(!NS.empty()){ string S2 = NS.front(); NS.pop(); int A = Zhixy.front(); Zhixy.pop(); int Zhi = A/100, x = (A/10)%10, y = A%10; if(S2==S_Goal){ MinZhi = Zhi; break; } //去重部分 if(Map.count(S2)) continue; else Map[S2] = Zhi; if(x+1<3){ NS.push(exChange(S2, x+1, y, x, y)); Zhixy.push((x+1)*10+y+(Zhi+1)*100); } if(y+1<3){ NS.push(exChange(S2, x, y+1, x, y)); Zhixy.push(x*10+y+1+(Zhi+1)*100); } if(x-1>=0){ NS.push(exChange(S2, x-1, y, x, y)); Zhixy.push((x-1)*10+y+(Zhi+1)*100); } if(y-1>=0){ NS.push(exChange(S2, x, y-1, x, y)); Zhixy.push(x*10+y-1+(Zhi+1)*100); } } cout<<MinZhi<<endl; TimeB("传统BFS算法+Map"); return 0; }
大体速度:
居然Map速度不算很快,那咱们有什么其它方法来解决去重问题,其实在使用Map时,咱们已经用到了Hash,那么对于八数码问题,很显然咱们看出全部的棋盘状态其实就是 0~8 的全排列,而对于全排列的Hash,咱们能够采用 康托展开
康托展开 是一个全排列到一个天然数的双射,经常使用于构建哈希表时的空间压缩
康托展开 的实质是计算当前排列在全部由小到大全排列中的位次,并且是可逆的
例如 012345678 就是 0;102345678 就是 1
那么如何对一组数进行康托展开?
表明最后获得的
值
表明目标的元素个数
表明第
个元素后面比此元素小的元素个数
int Factorial[9]={1, 1, 2, 6, 24, 120, 720, 5040, 40320}; //康托函数须要的阶乘 int Cantor(string S){ //康拓函数 int jShu = 0; for(int i=0; i<9; i++){ int jShu2 = 0; for(int j=i+1; j<9; j++) if(S[i]>S[j]) jShu2++; jShu += jShu2*Factorial[8-i]; } return jShu; }
具体实现代码就不贴了XD
DFS用Cantor展开快了很多,甚至有但愿避免TLE的悲惨命运Orz,然而仍是有限深
BFS发挥稳定,快了一点:
介绍待更…
inti.h头文件代码传送
下面是用 Map 去重的代码:
#include"init.h" map<string, int> Map; queue<string> NS; queue<int> Zhixy; int MinZhi; int main(){ if(Init()){ cout<<"No Solve"<<endl; return 0; } NS.push(S+"2"); NS.push(S_Goal+"1"); for(int i=0; i<9; i++) if(S[i]=='0') Zhixy.push(100+(int)(i/3)*10+i%3); for(int i=0; i<9; i++) if(S_Goal[i]=='0') Zhixy.push(100+(int)(i/3)*10+i%3); while(!NS.empty()){ string S1 = NS.front(); NS.pop(); bool sign = S1[9]-'1'; string S2(S1.substr(0,9)); //有点乱,有时间我整理一下吧 int A = Zhixy.front(); Zhixy.pop(); int Zhi = A/100, x = (A/10)%10, y = A%10; if(Map.count(S2)){ int bet = Map[S2]; if((sign&&bet<0)||(!sign&&bet>0)){ cout<<Zhi+abs(Map[S2])-2<<endl; break; } else if(abs(bet)>Zhi){ if(sign) Map[S2] = Zhi; else Map[S2] = -Zhi; } else continue; } else{ if(sign){ Map[S2] = Zhi; S2 = S2+"2"; } else{ Map[S2] = -Zhi; S2 = S2+"1"; } } if(x+1<3){ NS.push(exChange(S2, x+1, y, x, y)); Zhixy.push((x+1)*10+y+(Zhi+1)*100); } if(y+1<3){ NS.push(exChange(S2, x, y+1, x, y)); Zhixy.push(x*10+y+1+(Zhi+1)*100); } if(x-1>=0){ NS.push(exChange(S2, x-1, y, x, y)); Zhixy.push((x-1)*10+y+(Zhi+1)*100); } if(y-1>=0){ NS.push(exChange(S2, x, y-1, x, y)); Zhixy.push(x*10+y-1+(Zhi+1)*100); } } TimeB("双向BFS算法"); return 0; }
讲道理,双向BFS能这么快真是超出个人想象了,估计是由于数据特殊的缘由
A*算法 能够经过当前节点状态和之后预估的状态来有选择的拓展节点,从而更快的抵达搜索目标
具体公式表现为: 表明对节点 n 评估结果
表明原始节点到当前节点 n 的实际步数
表明当前节点 n 到目标节点的估计步数,咱们称之为 启式发函数
值得注意的是,咱们把
表明为当前节点到目标节点的实际步数
那么能够证实若是有
则一定能够找到最优解,一样的,若是
越接近
,则搜索效率越高
那么对于八数码问题,咱们能够设定多种启发式函数:
代码介绍待更
inti.h头文件代码传送
#include"init.h" struct Node{ int Fn, Num; string S1; bool operator < (const Node & a) const{ return Fn>a.Fn; } }; int Compare(string a, string b){ int jShu = 0; for(int i=0; i<9; i++) if(a[i]!=b[i]) jShu++; return jShu; } priority_queue<Node> NS; map<string, int> Map; int main(){ if(Init()){ cout<<"No Solve"<<endl; return 0; } Node Head; Head.Fn = 0; Head.S1 = S; for(int i=0; i<9; i++) if(S[i]=='0') Head.Num = (int)(i/3)*10+i%3; NS.push(Head); while(!NS.empty()){ Node b, a = NS.top(); NS.pop(); int Zhi = a.Num/100, x = (a.Num/10)%10, y = a.Num%10; if(a.S1 == S_Goal){ cout<<Zhi<<endl; break; } if(Map.count(a.S1)){ if(Map[a.S1]>Zhi) Map[a.S1] = Zhi; else continue; } else Map[a.S1] = Zhi; if(x+1<3){ b.S1 = exChange(a.S1, x+1, y, x, y); b.Fn = Zhi + Compare(b.S1, S_Goal); b.Num = (x+1)*10+y+(Zhi+1)*100; NS.push(b); } if(y+1<3){ b.S1 = exChange(a.S1, x, y+1, x, y); b.Fn = Zhi + Compare(b.S1, S_Goal); b.Num = x*10+y+1+(Zhi+1)*100; NS.push(b); } if(x-1>=0){ b.S1 = exChange(a.S1, x-1, y, x, y); b.Fn = Zhi + Compare(b.S1, S_Goal); b.Num = (x-1)*10+y+(Zhi+1)*100; NS.push(b); } if(y-1>=0){ b.S1 = exChange(a.S1, x, y-1, x, y); b.Fn = Zhi + Compare(b.S1, S_Goal); b.Num = x*10+y-1+(Zhi+1)*100; NS.push(b); } } TimeB("A_Star算法+Map"); return 0; }
有一些优化,可是不是很是明显,固然若是有更好的启发式函数会更快
迭代加深搜索实际上就是逐渐加大限制深度的DFS搜索
好比说对于八数码问题,因为不知道最大深度,咱们只能提早预约一个最大的迭代深度,但这样对于解规模较小的答案,至关于浪费了大量时间搜索到最大迭代深度
因此说咱们能够在搜索中 逐渐加大迭代深度
好比说:
那么为何不用 BFS 而用 迭代加深搜索 呢?
首先,迭代加深搜索不像DFS同样,须要大量空间来存储要遍历的节点
其次,迭代加深搜索看似时间复杂度很高 (由于不断的重复搜索),但实际上它的时间复杂度跟 BFS 是相同的
举个简单的例子说明,假如说每一个节点能够扩展两个节点
对于 BFS 来说,第 层的拓展节点数是
而对于 迭代加深搜索 来说,第一次扩展的节点数是 ,第二次扩展的节点数是 ,第 次拓展的节点数是
其前 次拓展的节点数和为 ,也就是说重复的遍历之前的节点代价相对于拓展下一层来讲并不高,对于每一个节点能够拓展更多节点的状况更是如此
加入了剪枝,具体细节会在之后更新
#include"init.h" int jShu; bool B; int Compare(string a, string b){ int jShu2 = 0; for(int i=0; i<9; i++) if(a[i]!=b[i]) jShu2++; return jShu2; } void IDA(int jShu2, string S2, int x, int y, int u){ if(S2==S_Goal){ B=1; return; } if(B) return; if(Compare(S2, S_Goal) + jShu2 - 1>jShu) return; string S3; if(x+1<3&&u!=3){ S3 = S2; IDA(jShu2+1, exChange(S3, x+1, y, x, y), x+1, y, 0); } if(y+1<3&&u!=2){ S3 = S2; IDA(jShu2+1, exChange(S3, x, y+1, x, y), x, y+1, 1); } if(x-1>=0&&u!=0){ S3 = S2; IDA(jShu2+1, exChange(S3, x-1, y, x, y), x-1, y, 3); } if(y-1>=0&&u!=1){ S3 = S2; IDA(jShu2+1, exChange(S2, x, y-1, x, y), x, y-1, 2); } } int main(){ if(Init()){ cout<<"No Solve"<<endl; return 0; } int x, y; for(int i=0; i<9; i++) if(S[i]=='0') x=i/3, y=i%3; while(1){ IDA(0, S, x, y, -1); if(B){ cout<<jShu<<endl; break; } jShu++; } TimeB("IDA算法"); return 0; }
还算比较快的,重点是空间占用少