[SCOI2005] 骑士精神
题目描述
在一个 5×5 的棋盘上有 12 个白色的骑士和 12 个黑色的骑士,且有一个空位。在任何时候一个骑士都能按照骑士的走法(它可以走到和它横坐标相差为 1,纵坐标相差为 2 或者横坐标相差为 2,纵坐标相差为 1 的格子)移动到空位上。
给定一个初始的棋盘,怎样才能经过移动变成如下目标棋盘:

为了体现出骑士精神,他们必须以最少的步数完成任务。
输入格式
第一行有一个正整数 T(T ≤10),表示一共有 T 组数据。
接下来有 T 个 5 ×5 的矩阵,0 表示白色骑士,1 表示黑色骑士,* 表示空位。两组数据之间没有空行。
输出格式
对于每组数据都输出一行。如果能在 15 步以内(包括 15 步)到达目标状态,则输出步数,否则输出 -1。
样例 #1
样例输入 #1
2
10110
01*11
10111
01001
00000
01011
110*1
01110
01010
00100
样例输出 #1
7
-1
题目分析
题意简述
题目要求从初始的棋盘变成目标棋盘的最少步数,若在 步以内(包括 步)无法到达则输出 。
模型抽象
从初始状态到目标状态的最少步数问题。
初步思考
将状态抽象为结点,从一个状态 A 变为另一个状态 B 的过程可视为结点 A 到结点 B 建立一条有向边。那么从初始状态到目标状态的最少步数,就变成了从起点到终点的最短路,且是一个无权最短路。
暴力解法:对于无权最短路问题可以考虑采用 BFS 来进行处理。
对于 的棋盘状态可以考虑压缩为一维的字符串用于描述状态。骑士移动到空位,则可以反过来考虑,从空位进行移动。
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 1e5 + 5;
string st;
string ed="111110111100*110000100000";//目标状态,对应5*5的布局
map<string,int> ans;//ans[状态]=从起始状态到当前状态的最少步数
//骑士移动的8个方向,dx和dy分别对应行和列的变化
int dx[]={-1,-2,-2,-1, 1, 2, 2, 1};
int dy[]={-2,-1, 1, 2, 2, 1,-1,-2};
int bfs(string st){
ans.clear();//多测要清空
ans[st]=0;
queue<string> q;
q.push(st);//初始状态入队
while(!q.empty()){
string u=q.front();
q.pop();
if(ans[u]>15) break;//超过15步停止
if(u==ed) break;//找到目标停止搜索
int pos=u.find('*');//找到空位
int x=pos/5,y=pos%5;
for(int i=0;i<8;i++){//遍历八个方向
int nx=x+dx[i],ny=y+dy[i];
if(nx<0||nx>4||ny<0||ny>4) continue;
string v=u;
swap(v[pos],v[nx*5+ny]);//求出新状态
if(ans.count(v)) continue;
ans[v]=ans[u]+1;//计算到达新状态的最少步数
q.push(v);//状态入队
}
}
//返回结果,注意无法到达返回-1
if(ans.count(ed)) return ans[ed];
else return -1;
}
int main(){
int T;
cin>>T;
while(T--){
st="";
for(int i=1;i<=5;i++){
for(int j=1;j<=5;j++){
char c;
cin>>c;
st+=c;
}
}
cout<<bfs(st)<<"\n";
}
return 0;
}
完成后可发现状态较多,对于不存在的情况需要遍历完所有的状态,过于复杂。
优化方向:本题需要高效求解最短路,可以考虑使用 A* 算法。
优化思考
考虑以 A* 算法优化最短路搜索过程。设 ,其中 为已经行走的步数, 为预估行走的步数,最优情况就是,有几个不同,就走几步,估价函数设计为与目标状态不同的位置数量。
解题流程
- 初始化:使用字符串存储棋盘状态。
- 估价函数:统计当前状态与目标状态的不同个数。
- 最短路计算:采用 A* 算法用于高效求解最短路径问题。
代码实现
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 1e5 + 5;
string st;
string ed="111110111100*110000100000";//目标状态,对应5*5的布局
map<string,int> ans;//ans[状态]=从起始状态到当前状态的最少步数
//骑士移动的8个方向,dx和dy分别对应行和列的变化
int dx[]={-1,-2,-2,-1, 1, 2, 2, 1};
int dy[]={-2,-1, 1, 2, 2, 1,-1,-2};
int h(string s){//估价函数,当前状态到目标状态大概最少还用几步
int cnt=0;
for(int i=0;i<25;i++){
if(s[i]!=ed[i]) cnt++;
}
return cnt;
}
struct node{
string v;//状态
int steps;//最少步数
friend bool operator<(const node &A,const node &B){
return A.steps+h(A.v)>B.steps+h(B.v);
}
};
int bfs(string st){
ans.clear();//多测要清空
ans[st]=0;
priority_queue<node> q;
q.push({st,0});//初始状态入队
while(!q.empty()){
node cur=q.top();
q.pop();
string u=cur.v;
if(u==ed) return cur.steps;//找到目标停止搜索
if(cur.steps>15) return -1;//超过15步停止,注意无法到达返回-1
int pos=u.find('*');//找到空位
int x=pos/5,y=pos%5;
for(int i=0;i<8;i++){//遍历八个方向
int nx=x+dx[i],ny=y+dy[i];
if(nx<0||nx>4||ny<0||ny>4) continue;
string v=u;
swap(v[pos],v[nx*5+ny]);//求出新状态
//if(ans.count(v)) continue;
//计算到达新状态的最少步数
int steps=ans[u]+1;
if(!ans.count(v) || steps<ans[v]){
ans[v]=steps;
q.push({v,steps});
}
}
}
}
int main(){
int T;
cin>>T;
while(T--){
st="";
for(int i=1;i<=5;i++){
for(int j=1;j<=5;j++){
char c;
cin>>c;
st+=c;
}
}
cout<<bfs(st)<<"\n";
}
return 0;
}
总结回顾
本题的关键在于:
- 将问题抽象为图上的最短路。
- 使用 *A 算法**优化最短路的寻找。