骑士精神问题详解:BFS暴力解法与A*算法优化

78 阅读5分钟

[SCOI2005] 骑士精神

题目描述

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

为了体现出骑士精神,他们必须以最少的步数完成任务。

输入格式

第一行有一个正整数 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* 算法优化最短路搜索过程。设 ,其中 为已经行走的步数, 为预估行走的步数,最优情况就是,有几个不同,就走几步,估价函数设计为与目标状态不同的位置数量。

解题流程

  1. 初始化:使用字符串存储棋盘状态。
  2. 估价函数:统计当前状态与目标状态的不同个数。
  3. 最短路计算:采用 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;
}

总结回顾

本题的关键在于:

  1. 将问题抽象为图上的最短路
  2. 使用 *A 算法**优化最短路的寻找。