洛谷题单指南-进阶搜索-P5507 机关
原题链接:https://www.luogu.com.cn/problem/P5507
题意解读:有12个旋钮,每个旋钮的值是1~4,每个旋钮旋转后值加1,如果是4旋转则变成1,另外每个旋钮在1~4不同状态下旋转,可以带动另一个旋钮旋转,被带动旋转的不能再带动下一级旋转,给出每个旋钮的起始状态,求所有旋钮都变成1最少需要多少步。
解题思路:与八数码问题类似,也是一个最小步数模型,直觉上可以使用BFS求解。
分析一下复杂度,每一次操作有12种选择,对应12种状态,如果扩展8层,将达到12^8=429981696,状态爆炸了,超时是必然。
对于BFS问题,如果状态空间巨大,主要有两种优化手段:A*算法、双向BFS算法,下面介绍用两种方法实现本题。
方法一:A*算法。
A*算法也是在BFS的基础之上,每次取最有可能更快到达终点的状态进行扩展,如何实现选取最有可能更快到达终点的状态呢?
我们定义dist[stat]表示状态stat距离起始状态的步数,f[stat]表示状态stat到目标终点的最小步数(通常约定f[stat]<=stat到终点的真实最优步数)。
那么每次状态扩展后,将状态stat加入队列时,同时要记录dist[stat] + f[stat],未来要优先扩展dist[stat] + f[stat]最小的状态,要实现优先扩展最小的状态,就不能用一般的queue,可以借助priority_queue。
如何计算f[stat]?对于本题,可以将所有旋钮到1的步数累加,结果除以2。
如何表示状态呢?首先想到的是直接用string就好了。
90分代码:
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, string> PIS;
string s = "0"; //初始状态,开头补0是为了方便下标从1开始
string target = "0111111111111"; //目标状态,开头补0是为了方便下标从1开始
int mp[13][5]; //mp[i][j]表示第i个旋钮在状态j时旋转会引起共同旋转的旋钮编号
unordered_map<string, int> dist; //dist[stat]表示从初始状态到stat状态的最短步数
unordered_map<string, PIS> pre; //pre[stat]表示stat状态的上一个状态以及旋转的旋钮编号
priority_queue<PIS, vector<PIS>, greater<PIS> > q;
int f(string stat) //估价函数最小值是所有旋钮到1的距离之和的一半
{
int res = 0;
for(int i = 1; i <= 12; i++)
{
if(stat[i] != '1') res += 5 - (stat[i] - '0');
}
return res / 2.0;
}
void astar(string start)
{
q.push({f(start), start});
dist[start] = 0;
while(q.size())
{
PIS p = q.top(); q.pop();
string stat = p.second;
if(stat == target)
{
cout << dist[stat] << endl;
vector<int> ans;
while(stat != start)
{
ans.push_back(pre[stat].first);
stat = pre[stat].second;
}
for(int i = ans.size() - 1; i >= 0; i--) cout << ans[i] << " ";
return;
}
for(int i = 1; i <= 12; i++)
{
string tmp = stat;
int ii = mp[i][tmp[i] - '0']; // 旋转第i个旋钮会引起共同旋转的旋钮编号
tmp[ii] = '0' + (tmp[ii] - '0') % 4 + 1; // 旋转第ii个旋钮
tmp[i] = '0' + (tmp[i] - '0') % 4 + 1; // 旋转第i个旋钮
if(!dist.count(tmp) || dist[tmp] > dist[stat] + 1) //如果没有到达过或者到达的步数更少
{
dist[tmp] = dist[stat] + 1;
pre[tmp] = {i, stat};
q.push({dist[tmp] + f(tmp), tmp});
}
}
}
}
int main()
{
int a, b, c, d, e;
for(int i = 1; i <= 12; i++)
{
cin >> a >> b >> c >> d >> e;
s += a + '0';
mp[i][1] = b;
mp[i][2] = c;
mp[i][3] = d;
mp[i][4] = e;
}
astar(s);
return 0;
}
卡在了最后一个测试点,状态表示用string时,采用map导致的hash计算会产生开销,可以直接通过一个24位的二进制表示状态,每2位表示一个旋钮,旋钮的状态1~4调整为0~3,这样00表示0,01表示1,10表示2,11表示3,通过位运算即可实现状态的操作和变换。
100分代码:
#include <bits/stdc++.h>
using namespace std;
//为便于计算,将旋钮状态1~4分别用0~3表示
//状态用24位二进制表示,每两位表示一个旋钮的状态,00,01,10,11分别表示0,1,2,3
//旋钮编号1~12分别用0~11表示
typedef pair<int, int> PIS;
int start; //初始状态,输入时填充
int target = 0; //目标状态, 24位全0
int mp[12][4]; //mp[i][j]表示第i个旋钮在状态j时旋转会引起共同旋转的旋钮编号
int dist[1 << 24]; //dist[stat]表示从初始状态到stat状态的最短步数
PIS pre[1 << 24]; //pre[stat]表示stat状态的上一个状态以及旋转的旋钮编号
priority_queue<PIS, vector<PIS>, greater<PIS> > q;
int f(int stat) //估价函数最小值是所有旋钮到0的距离之和的一半
{
int res = 0;
for(int i = 0; i < 12; i++)
{
if(stat >> (i * 2) & 3 != 0) res += 5 - (stat >> (i * 2) & 3);
}
return res / 2.0;
}
void astar()
{
memset(dist, 0x3f, sizeof(dist));
q.push({f(start), start});
dist[start] = 0;
while(q.size())
{
PIS p = q.top(); q.pop();
int stat = p.second;
if(stat == target)
{
cout << dist[stat] << endl;
vector<int> ans;
while(stat != start)
{
ans.push_back(pre[stat].first);
stat = pre[stat].second;
}
for(int i = ans.size() - 1; i >= 0; i--) cout << ans[i] << " ";
return;
}
for(int i = 0; i < 12; i++)
{
int tmp = stat;
int ii = mp[i][tmp >> (i * 2) & 3]; // 旋转第i个旋钮会引起共同旋转的旋钮编号
int iadd = ((tmp >> (i * 2) & 3) + 1) % 4; // 旋转第i个旋钮
int iiadd = ((tmp >> (ii * 2) & 3) + 1) % 4; // 旋转第ii个旋钮
tmp &= ~(3 << (i * 2));
tmp &= ~(3 << (ii * 2));
tmp |= iadd << (i * 2);
tmp |= iiadd << (ii * 2);
if(dist[tmp] > dist[stat] + 1) //如果没有到达过这个状态或者到达这个状态的步数更少
{
dist[tmp] = dist[stat] + 1;
pre[tmp] = {i + 1, stat}; //旋钮编号加1
q.push({dist[tmp] + f(tmp), tmp});
}
}
}
}
int main()
{
int a, b, c, d, e;
for(int i = 0; i < 12; i++)
{
cin >> a >> b >> c >> d >> e;
start |= (a - 1) << (i * 2);
mp[i][0] = b - 1;
mp[i][1] = c - 1;
mp[i][2] = d - 1;
mp[i][3] = e - 1;
}
astar();
return 0;
}
方法二:双向BFS
所谓双向BFS,是同时从状态起点、终点进行BFS,用两个队列分别进行扩展状态,当其中一个队列的状态扩展时,如果发现已经在另一个方向处理过了,那么双向搜索就相遇,这样从相遇的状态点到起点、终点的距离之和就行总的步数,同样可以保存相遇点到起点、终点的路径。
实际执行时,可以根据两个队列的长度,优先扩展长度较少的。
100分代码:
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PIS;
int start; //初始状态,输入时填充
int target = 0; //目标状态, 24位全0
int mp[12][4]; //mp[i][j]表示第i个旋钮在状态j时旋转会引起共同旋转的旋钮编号
int dist1[1 << 24], dist2[1 << 24]; //dist[stat]表示从初始状态到stat状态的最短步数
PIS pre1[1 << 24], pre2[1 << 24]; //pre[stat]表示stat状态的上一个状态以及旋转的旋钮编号
void showpath(int stat)
{
cout << dist1[stat] + dist2[stat]<< endl;
int tmp = stat;
vector<int> ans;
int cnt = 0;
while(tmp != start)
{
ans.push_back(pre1[tmp].first);
tmp = pre1[tmp].second;
}
for(int i = ans.size() - 1; i >= 0; i--) cout << ans[i] << " ";
tmp = stat;
while(tmp != target)
{
cout << pre2[tmp].first << " ";
tmp = pre2[tmp].second;
}
}
void bfs()
{
queue<int> q1, q2;
q1.push(start);
q2.push(target);
dist1[start] = 0;
dist2[target] = 0;
while(q1.size() && q2.size())
{
if(q1.size() < q2.size()) //双向BFS,每次选择节点少的一端进行扩展
{
int stat = q1.front(); q1.pop();
if(dist2[stat])
{
showpath(stat);
return;
}
for(int i = 0; i < 12; i++)
{
int nstat = stat;
int ii = mp[i][nstat >> (i * 2) & 3]; // 旋转第i个旋钮会引起共同旋转的旋钮编号
int iadd = ((nstat >> (i * 2) & 3) + 1) % 4; // 旋转第i个旋钮
int iiadd = ((nstat >> (ii * 2) & 3) + 1) % 4; // 旋转第ii个旋钮
nstat &= ~(3 << (i * 2));
nstat &= ~(3 << (ii * 2));
nstat |= iadd << (i * 2);
nstat |= iiadd << (ii * 2);
if(dist1[nstat]) continue; //如果已经到达过这个状态就不用再次到达了
dist1[nstat] = dist1[stat] + 1;
pre1[nstat] = {i + 1, stat}; //旋钮编号加1
q1.push(nstat);
}
}
else
{
int stat = q2.front(); q2.pop();
if(dist1[stat])
{
showpath(stat);
return;
}
for(int i = 0; i < 12; i++)
{
int nstat = stat;
int last = ((nstat >> (i * 2) & 3) - 1 + 4) % 4; //第i个旋钮的上一个状态
int ii = mp[i][last]; //第i个旋钮会引起共同旋转的旋钮编号
int iadd = last; //旋转第i个旋钮
int iiadd = ((nstat >> (ii * 2) & 3) - 1 + 4) % 4; //旋转第ii个旋钮
nstat &= ~(3 << (i * 2));
nstat &= ~(3 << (ii * 2));
nstat |= iadd << (i * 2);
nstat |= iiadd << (ii * 2);
if(dist2[nstat]) continue; //如果已经到达过这个状态就不用再次到达了
dist2[nstat] = dist2[stat] + 1;
pre2[nstat] = {i + 1, stat}; //旋钮编号加1
q2.push(nstat);
}
}
}
}
int main()
{
int a, b, c, d, e;
for(int i = 0; i < 12; i++)
{
cin >> a >> b >> c >> d >> e;
start |= (a - 1) << (i * 2);
mp[i][0] = b - 1;
mp[i][1] = c - 1;
mp[i][2] = d - 1;
mp[i][3] = e - 1;
}
bfs();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
2024-02-18 洛谷题单指南-递推与递归-P2437 蜜蜂路线
2024-02-18 洛谷题单指南-递推与递归-P1928 外星密码