题目连接 P5507 机关node
有12个旋钮,每一个旋钮开始时处于状态 \(1\) ~ \(4\) ,每次操做能够往规定方向转动一个旋钮 (\(1\Rightarrow2\Rightarrow3\Rightarrow4\Rightarrow1\)) ,而且会触发一次连锁反应:处于某个状态的旋钮在旋转时会引发另外一个旋钮发生相同方向的转动(另外一个旋钮转动不会再触发连锁反应)。问将12个旋钮都置为 \(1\) 至少须要几回转动,并输出每次转动的旋钮编号。c++
直接暴力地进行单向 \(BFS\) ,每次转动都有 \(12\) 种选择,时间复杂度是 \(O(12^{step})\) ,看数据范围,最高的步数可达 \(17\) 步,一定 \(TLE\) 。可是这样简单若是优化的比较好能够得 \(50\) ~ \(60\) 分(没吸氧气,吸了氧气反而更低了)。
单向BFS评测记录
超时的主要缘由是搜索树过于庞大,而咱们会发现本题起始状态和终止状态是明确的,这时咱们就可使用神奇的双向 \(BFS\) 来限制搜索树的生长。git
双向 \(BFS\) 很是适合在起始状态和终止状态明确的条件下使用,作法是从起点和终点同时进行单向 \(BFS\) ,让两边 \(BFS\) 搜索树的生长受到对面搜索树的限制,不要过于野蛮生长,偏离目标太远。本身画了一张很丑很丑的对比图,应该能够便于理解。
能够看到双向 \(BFS\) 能够在某一状态发现相同时就中止了,经过回溯能够找到沿路选择的点。再看看本题的数据范围,最大的点正向和反向 \(BFS\) 最可能是 \(9\) 步, \(12^9\) 是 \(5\times10^8\) 的量级,勉强能够在一秒冲过去。事实上我最大的点用时在 \(200ms\) ~ \(300ms\) 之间,仍是很稳的。
最好的一次双向BFS记录算法
能够把两个二进制位当作一个四进制位,把每一个旋钮状态减一后就恰好能够存下了,即1对应0,2对应1,以此类推。先讲一下读入处理。数组
int button,Start = 0; For(i,0,11){ button = read(); //读入第i+1个旋钮状态 Start |= (button - 1) << (i << 1); //记录初始状态 For(j,0,3) nxt[i][j] = read()-1; }
我代码中的旋钮编号和状态所有进行了减一处理(后面描述时我都会说+1),方便位运算操做。注意记录初始状态时要将 \(i*2\) (即左移一位),由于咱们把两个二进制位当作一个四进制位了,后面也有这样的乘2处理。再用一个数组 \(nxt\) 记录第 \(i+1\) 个旋钮在 \(j+1\) 状态下进行旋转时,会带动第 \(nxt[i][j]+1\) 个旋钮转动。函数
首先正向和反向的 \(BFS\) 的转移方式是不同的。设当前转到的是第 \(i+1\) 个旋钮,它如今处于 \(j+1\) 状态。测试
咱们把正向方向定义为1,反向方向定义为2,当前方向为 \(direction\) ,当前全部按钮状态为 \(state\) ,有:优化
int si,sNext,nx,nextState; For(i,0,11) { if (direction == 1) { //正向 si = (state >> (i << 1)) & 3; //一、获取第i+1个旋钮状态(0~3) nx = nxt[i][si]; //二、获取牵连旋钮编号 sNext = (state >> (nx << 1)) & 3; //三、获取牵连旋钮状态,方式同1 nextState = state ^ (si << (i << 1)) ^ (((si + 1) & 3) << (i << 1)); //四、修改状态为第i+1个旋钮旋转后的状态 nextState ^= (sNext << (nx << 1)) ^ (((sNext + 1) & 3) << (nx << 1)); //五、修改状态为牵连旋钮旋转后的状态 } else { //反向 si = (state >> (i << 1)) & 3; nx = nxt[i][(si + 3) & 3]; //获取第i+1个旋钮逆向旋转后的牵连旋钮编号 sNext = (state >> (nx << 1)) & 3; nextState = state ^ (si << (i << 1)) ^ (((si + 3) & 3) << (i << 1)); //修改状态为第i+1个旋钮逆向旋转后的状态 nextState ^= (sNext << (nx << 1)) ^ (((sNext + 3) & 3) << (nx << 1));//修改状态为牵连旋钮逆向旋转后的状态 } }
\(Code:\)spa
#include <bits/stdc++.h> using namespace std; #define For(i,sta,en) for(int i = sta;i <= en;i++) inline int read(){ int sum = 0,fu = 1;char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') fu = -1;ch = getchar();} while(isdigit(ch)) { sum = (sum<<1)+(sum<<3)+(ch^48);ch =getchar();} return sum * fu; } const int N = 1<<24; bool vis[N]; int nxt[14][6],fa[N],choice[N],v[N],flag,m1,m2,mid,ans1[30],ans2[30]; queue<int>q; int main(){ int button,Start = 0; For(i,0,11){ button = read(); //读入第i+1个旋钮状态 Start |= (button - 1) << (i << 1); //记录初始状态 For(j,0,3) nxt[i][j] = read()-1; } vis[Start] = vis[0] = 1; //是否访问过 v[Start] = 1; v[0] = 2; //区分方向 q.push(Start); q.push(0); while(!q.empty() && !flag){ int state = q.front(),direction = v[state]; q.pop(); int si,sNext,nx,nextState; For(i,0,11){ if(direction == 1){ //正向 si = (state >> (i << 1))&3; //一、获取第i+1个旋钮状态(0~3) nx = nxt[i][si]; //二、获取牵连旋钮编号 sNext = (state >> (nx << 1)) & 3; //三、获取牵连旋钮状态,方式同1 nextState = state ^ (si << (i << 1)) ^ (((si + 1) & 3) << (i << 1)); //四、修改状态为第i+1个旋钮旋转后的状态 nextState ^= (sNext << (nx << 1)) ^ (((sNext + 1) & 3) << (nx << 1)); //五、修改状态为牵连旋钮旋转后的状态 } else{ //反向 si = (state >> (i << 1))&3; nx = nxt[i][(si+3)&3]; //获取第i+1个旋钮逆向旋转后的牵连旋钮编号 sNext = (state >> (nx << 1)) & 3; nextState = state ^ (si << (i << 1)) ^ (((si + 3) & 3) << (i << 1)); //修改状态为第i+1个旋钮逆向旋转后的状态 nextState ^= (sNext << (nx << 1)) ^ (((sNext + 3) & 3) << (nx << 1));//修改状态为牵连旋钮逆向旋转后的状态 } //若是这个状态在以前访问过 if(vis[nextState]){ if(v[nextState] == direction) continue; //同方向的直接跳过,以前到达的时候确定不劣于如今 /* * 不一样方向说明已经找到答案了 * m1 记录正向与逆向的链接点 * m2 记录逆向与正向的链接点 * mid 记录从state状态转移到nextState状态选择的旋钮编号 */ m1 = direction == 1 ? state : nextState; mid = i+1; m2 = direction == 1 ? nextState : state; flag = 1;break; } vis[nextState] = 1; v[nextState] = direction; //继承方向 fa[nextState] = state; //用于回溯操做 choice[nextState] = i + 1; //记录本次操做 q.push(nextState); } } int cnt1 = 0,state = m1,cnt2 = 0; //正向回溯 while(state != Start){ ans1[++cnt1] = choice[state]; state = fa[state]; } //逆向回溯 state = m2; while(state != 0){ ans2[++cnt2] = choice[state]; state = fa[state]; } //总步数,还要加上中间那一步mid操做 printf("%d\n",cnt1+cnt2+1); for(int i = cnt1; i; i--) printf("%d ", ans1[i]); printf("%d ",mid); For(i,1,cnt2) printf("%d ", ans2[i]); return 0; }
双向 \(BFS\) 已经够快了,可是咱们可使用更快的启发式搜索。经常使用的启发式搜索有 \(IDA*\) 和 \(A*\) ,据说前者被卡了,咱们就用 \(A*\) 吧。即便你可能不知道什么是 \(A*\) 算法(我作这题的时候就没据说过),也能够继续往下看。code
在 \(A*\) 算法中,咱们要利用当前状态的信息对状态进行评价,以此来决定下一次的操做,极大地限制了搜索树的生长。首先介绍一个特别的估价函数 \(F^*\) 来表示:\(F^*(x)=g^* (x)+h^*(x)\) 。其中 \(g^* (x)\) 表示从初始状态到当前状态所付出的最小代价(在本题中意义为操做步数),而 \(h^*(x)\) 是从当前状态到目标状态走最佳路径所付出的代价。在实际代码中咱们使用的实际上是 \(F(x)=g (x)+h(x)\) ,由于咱们其实是不知道这个加星后的函数的,可是咱们能够经过一些限制,让不加星的函数也能够在必定范围内求解出正确答案;
\(h(x)\) 是一个比较玄学的东东,没有惟一的定义,不一样的定义可能会致使程序执行效率和结果不一样,这题中你还能够乘一个系数给他,能明显加快运行效率。通过笔者屡次测试,发现给 \(h\) 乘上系数从 \(1.1\) ~ \(2.3\) 都能 \(AC\) 这道题,可是乘 \(2.4\) 时会 \(WA\) 掉一个点。变化趋势是这个系数越大,跑得越快,最大的点能够跑进 \(100ms\) 。这是由于系数越大越接近真实值 \(h^*(x)\),可是更大的系数不能保证必定能够获得最优解。
代码实现相似 \(Dijkstra\) 算法,定义一个结构体存状态和这个状态对应的估价函数值 \(F\) 。每次从小根堆中取出 \(F\) 最小的状态进行转移,存状态和转移状态的操做和上面双向 \(BFS\) 相同,这里直接给出代码。
\(Code:\)
#include <bits/stdc++.h> using namespace std; #define For(a,sta,en) for(int a = sta;a <= en;a++) inline int read(){ int sum = 0,fu = 1;char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') fu = -1;ch = getchar();} while(isdigit(ch)) { sum = (sum<<1)+(sum<<3)+(ch^48);ch =getchar();} return sum * fu; } const int N = 1<<24; int g[N],nxt[14][6],fa[N],ans[30],choice[N]; struct node{ int state; //状态 double F; //状态对应估价函数值 node(int s):state(s){ //构造函数,冒号后面部分至关于 state = s; double h = 0; F = 0; For(i,0,11) if((s>>(i<<1))&3) h += 4 - ((s >> (i << 1)) & 3); //计算不处在状态1的旋钮的对应的h值 F = h / 2 + g[s]; //能够在h/2前乘一个玄学系数 } bool operator<(const node &y) const{ return F > y.F; //估价函数值小的放前面 } }; priority_queue<node>q; int main(){ int button,Start = 0; For(i,0,11){ button = read(); //读入第i+1个旋钮状态 Start |= (button - 1) << (i << 1); //记录初始状态 For(j,0,3) nxt[i][j] = read()-1; } q.push(node(Start)); //调用构造函数,顺便计算出估价函数值 g[Start] = 0; int flag = 1; while(!q.empty()&&flag){ int state = q.top().state; q.pop(); int si,sNxt,nx,nextState; For(i,0,11){ si = (state>>(i<<1))&3; nx = nxt[i][si]; sNxt = (state>>(nx<<1))&3; nextState = state ^ (si << (i << 1)) ^ (((si + 1) & 3) << (i << 1)) ^ (sNxt << (nx << 1)) ^ (((sNxt + 1) & 3) << (nx << 1)); //若是没有访问过就能够转移新状态了 if(!g[nextState]){ g[nextState] = g[state] + 1; fa[nextState] = state; //用于回溯 choice[nextState] = i + 1; if(nextState == 0) { flag = 0;break;} //到达目标状态 q.push(node(nextState)); } } } int cnt = 0,state = 0; while(state != Start){ ans[++cnt] = choice[state]; state = fa[state]; } printf("%d\n",cnt); for(int i = cnt;i;i--) printf("%d ",ans[i]); return 0; }
对于 \(A^*\) 算法我可能有些地方描述不够严谨,若是有错误的地方欢迎指出。作完这道题建议去作一下 P1379 八数码难题 ,能够同时用单向 \(BFS\) ,双向 \(BFS\) ,\(A^*\) 和 \(IDA^*\) 作这道题,若是每一个方法都写一下必定受益良多。