5507 机关
题意:
有12个旋钮,每个旋钮开始时处于状态 1~4,每次操作可以往规定方向转动一个旋钮(1⇒2⇒3⇒4⇒1)
并且会触发一次连锁反应:处某个状态的旋钮在旋转时会引起另一个旋钮发生相同方向的转动(另一个旋钮转动不会再触发连锁反应)。问将12个旋钮都置为 11 至少需要几次转动,并输出每次转动的旋钮编号
这个题有两种做法,双向BFS和启发式搜索
双向BFS
其实也可以简单暴力的BFS,每次转动都有12种选择,时间复杂度会炸
那么BFS会炸,是因为搜索树太大了,并且这个题的起始状态和终止状态都是明确的,我们使用神奇的双向BFS了,来限制搜索树的生长
双向 BFS 非常适合在起始状态和终止状态明确的条件下使用,他的做法是从起点和终点同时进行BFS,让两边BFS搜索树的生长受对面搜索树的限制,不要过于野蛮的生长,偏离目标太远
双向BFS的示意图
可以看到双向BFS可以在某一状态相同就停止了
通过回溯可以找到沿路选择的点
然后在存储的时候用状态压缩来存储,把两个二进制位做成一个四进制位
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
#include<queue>
using namespace std;
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(int i=0;i<=11;i++)
{
button = read();//读入按钮状态
Start |= (button - 1) << (i << 1);//记录初始状态
for(int j=0;j<=3;j++)
nxt[i][j]=read()-1;
}
vis[Start]=vis[0] = 1; //标记访问
v[Start] = 1;
v[0] = 2;
q.push(0);
while(!q.empty() && !flag)
{
int state = q.front(),direction = v[state];
q.pop();
int si,sNext,nx,nextState;
for(int i=0;i<=11;i++)
{
if(direction == 1)
{ //正向
si = (state >> (i << 1))&3; //1、获取第i+1个旋钮状态(0~3)
nx = nxt[i][si]; //2、获取牵连旋钮编号
sNext = (state >> (nx << 1)) & 3; //3、获取牵连旋钮状态,方式同1
nextState = state ^ (si << (i << 1)) ^ (((si + 1) & 3) << (i << 1)); //4、修改状态为第i+1个旋钮旋转后的状态
nextState ^= (sNext << (nx << 1)) ^ (((sNext + 1) & 3) << (nx << 1)); //5、修改状态为牵连旋钮旋转后的状态
} 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 = direction == 1 ? state : nextState;// m1 记录正向与逆向的连接点
mid = i+1;
m2 = direction == 1 ? nextState : state;//m2 记录逆向与正向的连接点
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];
}
printf("%d\n",cnt1+cnt2+1);//计算答案
for(int i = cnt1; i; i--)
printf("%d ", ans1[i]);
printf("%d ",mid);
for(int i=1;i<=cnt2;i++)
printf("%d ", ans2[i]);
return 0;
}
启发式搜索
在 A* 算法中,我们要利用当前状态的信息对状态进行评价,以此来决定下一次的操作,极大地限制了搜索树的生长
(所以,树的生长是时间的关键)
我们使用F来表示x状态的代价:F(x)=g(x)+h*(x),其中g(x)表示从初始状态到当前状态付出的最小代价,本题中就是操作步数
h*(x)是从当前状态到目标状态走最佳路径付出的代价
所以我们是用h函数进行估计的
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
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 * 1.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;
while(!q.empty()){
int state = q.top().state;
q.pop();
if(state == 0) break;
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;
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;
}