版权声明:版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/shaotaiban1097/article/details/83051981
参考博文地址: https://blog.csdn.net/csdnsevenn/article/details/82782947
参考博文中是以漫画为思路的Java代码实现,非常好理解。
本文是以参考上述博文思路,以C#代码实现八数码问题的求解,代码中加入了详细的注释以便于理解。
★ 测试时发现本文C#代码的BFS算法在求解步数超过20步以上的问题时,速度很慢。
不确定是否代码编写的有问题,还需改进。
定义类EightDigtal存储八数码问题的基本常量、变量和操作方法
using System;
using System.Collections.Generic;
namespace EightDigital {
/// <summary>
/// 八数码问题初始化及操作方法实现 2018年9月25日13:51:47
/// </summary>
public class EightDigital {
#region 定义基本常量、变量
/* 定义九宫格的最终状态 为了方便 直接使用string类型常量 */
public const string STATUS = "123456780";
/* 定义移动的方向 */
public const int LEFT = 1;
public const int RIGHT = 2;
public const int UP = 3;
public const int DOWN = 4;
/* 定义九宫格 3 * 3 的二维数组表示 */
public int[,] arr;
/* 定义 x y 变量 存储 0 在二维数组中的位置 */
public int x, y;
#endregion
#region 定义算法中使用的变量
/* 定义集合 存储当前移动的路径 -- DFS使用 */
public List<int?> movePath = new List<int?>();
/* 定义集合(队列) 存储每一步搜索的对象 也是下一步要搜索的状态集合 -- BFS使用 */
public List<BFSSearchItem> searchItemList = new List<BFSSearchItem>();
/* 定义集合 存储已经搜索过的九宫格状态 */
public List<string> statusList = new List<string>();
#endregion
#region 构造函数初始化 搜索空格位置
/* 构造函数:初始化对象时遍历二维数组 搜索 0 所在的位置并对 x y 赋值 */
public EightDigital(int[,] arr) {
this.arr = arr;
GetPositionOfSpace();
}
/* 返回空格(即 0)所在的 X Y 坐标 */
private void GetPositionOfSpace() {
for (int i = 0; i < arr.GetLength(0); i++) {
for (int j = 0; j < arr.GetLength(1); j++) {
if (arr[i, j] == 0) {
x = i;
y = j;
}
}
}
}
#endregion
#region 移动、回退操作
/// <summary>
/// 移动空格位置
/// </summary>
/// <param name="direction">移动的方向</param>
/// <returns></returns>
public bool Move(int direction) {
/* 定义局部变量 canMove 存储指定方向是否可以进行移动操作 */
bool canMove = false;
/* 判断是否可以向指定方向移动 如可以 则继续 如不可以 则终止 */
switch (direction) {
case LEFT:
canMove = y > 0;
break;
case RIGHT:
canMove = y < 2;
break;
case UP:
canMove = x > 0;
break;
case DOWN:
canMove = x < 2;
break;
default:
break;
}
/* 如果不满足移动条件 则直接终止跳出方法 */
if (!canMove) {
return false;
}
/* 执行移动操作 */
switch (direction) {
case LEFT:
arr[x, y] = arr[x, y] ^ arr[x, y - 1];
arr[x, y - 1] = arr[x, y] ^ arr[x, y - 1];
arr[x, y] = arr[x, y] ^ arr[x, y - 1];
/* 移动成功后 重新定位 0 的位置 */
y = y - 1;
break;
case RIGHT:
arr[x, y] = arr[x, y] ^ arr[x, y + 1];
arr[x, y + 1] = arr[x, y] ^ arr[x, y + 1];
arr[x, y] = arr[x, y] ^ arr[x, y + 1];
/* 移动成功后 重新定位 0 的位置 */
y = y + 1;
break;
case UP:
arr[x, y] = arr[x, y] ^ arr[x - 1, y];
arr[x - 1, y] = arr[x, y] ^ arr[x - 1, y];
arr[x, y] = arr[x, y] ^ arr[x - 1, y];
/* 移动成功后 重新定位 0 的位置 */
x = x - 1;
break;
case DOWN:
arr[x, y] = arr[x, y] ^ arr[x + 1, y];
arr[x + 1, y] = arr[x, y] ^ arr[x + 1, y];
arr[x, y] = arr[x, y] ^ arr[x + 1, y];
/* 移动成功后 重新定位 0 的位置 */
x = x + 1;
break;
default:
break;
}
/* 移动操作完成后 将本次移动方向追加到路径集合中 */
movePath.Add(direction);
return true;
}
/// <summary>
/// 回退空格位置 当移动后九宫格的状态与之前出现过的状态重复或不合理时 用于回溯操作
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
public bool BackMove(int direction) {
switch (direction) {
case LEFT:
Move(RIGHT);
break;
case RIGHT:
Move(LEFT);
break;
case UP:
Move(DOWN);
break;
case DOWN:
Move(UP);
break;
default:
break;
}
/* 回退成功后将上次移动的方向从路径集合中移除 */
movePath.RemoveAt(movePath.Count - 1);
return true;
}
#endregion
#region 获取九宫格状态、当前移动路径操作
/// <summary>
/// 获取当前九宫格的数字状态 依次按照顺序强转为int型 方便与常量STATUS的比较和存储
/// </summary>
/// <returns></returns>
public string GetCurrentStatus() {
string num = string.Empty;
for (int i = 0; i < arr.GetLength(0); i++) {
for (int j = 0; j < arr.GetLength(1); j++) {
num += arr[i, j].ToString();
}
}
return num;
}
/// <summary>
/// 恢复当前九宫格的状态到int类型的二维数组 并重新定位 0 的坐标
/// </summary>
/// <param name="currentStatus"></param>
public void RecoverStatus(string currentStatus) {
/* 恢复九宫格数组 */
for (int i = 0; i < currentStatus.Length; i++) {
/* 注意:currentStatus[i] 是一个字符串 需要将其转成int 否则 会自动转成对应的ASCII码 那样的话 "1" 可能就会被转为49 */
arr[i / 3, i % 3] = Convert.ToInt32(currentStatus[i].ToString());
}
/* 重定位空格位置 */
GetPositionOfSpace();
}
/// <summary>
/// 输出当前九宫格的字符状态
/// </summary>
public void PrintCurrentStatus() {
for (int i = 0; i < arr.GetLength(0); i++) {
for (int j = 0; j < arr.GetLength(1); j++) {
Console.Write(arr[i, j].ToString() + "\t");
}
Console.WriteLine();
}
}
/// <summary>
/// 输出当前路径
/// </summary>
public void PrintCurrentPath() {
for (int i = 0; i < movePath.Count; i++) {
Console.Write(ConvertDirectionToString(movePath[i]) + " ");
}
}
/// <summary>
/// 方向转换成汉字形式显示
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
public string ConvertDirectionToString(int? direction) {
switch (direction) {
case LEFT:
return "左";
case RIGHT:
return "右";
case UP:
return "上";
case DOWN:
return "下";
default:
return null;
}
}
#endregion
#region BFS 广度优先中获取某一状态形成的路径
/// <summary>
/// 根据指定的状态结点包含的信息(当前状态、上一次移动方向、上一结点)逆向追溯其形成的路径
/// </summary>
/// <param name="item"></param>
public void GetRoute(BFSSearchItem item) {
/* 清空 movePath 准备存储最终路径 (因movePath是两种算法共用存储路径的,所以有可能在移动过程中存储到一些无效路径) */
movePath.Clear();
/* 追溯过程直到当前结点无上一结点 即当前结点为首结点 为止 */
while (null != item.LASTITEM) {
/* 路径集合中在倒序插入方向 */
movePath.Insert(0, item.DIRECTION);
/* 追溯上一结点 */
item = item.LASTITEM;
}
}
#endregion
#region 调用算法 返回执行结果
public bool ExcuteDFS() {
/* 记录初始状态 */
statusList.Add(GetCurrentStatus());
/* 分别向四个方向开始搜索 */
return DFSSearch(LEFT) || DFSSearch(RIGHT) || DFSSearch(UP) || DFSSearch(DOWN);
}
public bool ExcuteBFS() {
/* 记录初始状态 */
statusList.Add(GetCurrentStatus());
/* 初始状态加入搜索队列 准备搜索 */
searchItemList.Add(new BFSSearchItem(GetCurrentStatus(), null, null));
return BFSSearch();
}
#endregion
#region 算法实现
#region 深度优先算法 Depth First Search
/* 深度优先算法的特点:
适合获取所有结果
占用内存空间少
代码量少
常常与递归结合使用
堆栈内存开销大
效率相对较慢
*/
/* 实现基本思想:暴力搜索 + 剪枝 + 回溯 + 递归 */
/* 实现基本原理:
* 1、利用递归算法 向任一方向持续搜索 直到该方向无法走通或者为已搜索过的状态
* 2、在某一点 对四个方向进行搜索 如果四个方向均无法通过或均为已搜索过的状态 回溯到上一状态 改变方向继续搜索
* 3、当回溯到起始状态依然无有效可通过路径供选择时 认为该题目无解
* 4、当某次搜索完成后状态与指定状态相同时 则认为搜索成功 算法结束
* */
/// <summary>
/// 深度优先算法实现
/// </summary>
/// <param name="direction">移动方向</param>
/// <returns></returns>
public bool DFSSearch(int direction) {
/* 因某些路径可能十分复杂无法确定是否可以走通 最终导致内存溢出 所以新增判断 如果路径 > 100 则放弃该路径 */
//if (movePath.Count > 100) { return false; }
/* 根据传入的方向 执行移动操作(包含是否可移动的判断 */
/* 如果移动失败(包含无法移动)结束本次操作 返回false */
if (!Move(direction)) {
return false;
}
/* 移动成功后 获取当前九宫格状态 */
string currentStatus = GetCurrentStatus();
/* 判断当前九宫格状态 如果为最终状态 则结束搜索 */
if (STATUS == currentStatus) {
return true;
}
/* 判断当前九宫格状态 如果为已经搜索过的状态 则回溯到上一步 并结束本次查找 */
if (statusList.Contains(currentStatus)) {
BackMove(direction);
return false;
}
/* 若上述条件都不满足 则说明该状态是一个新状态 将其追加到已搜索状态集合中 */
statusList.Add(currentStatus);
/* 递归调用DFS算法 继续向移动后空格所在位置的四个方向搜索 */
bool search = DFSSearch(LEFT) || DFSSearch(RIGHT) || DFSSearch(UP) || DFSSearch(DOWN);
/* 如果四个方向都搜索失败 则回溯到上一步 并删除该路径 */
if (!search) {
BackMove(direction);
return false;
}
return true;
}
#endregion
#region 广度优先算法 Breadth First Search
/* 广度优先算法的特点:
适合获取最优解
占用内存空间较大
代码较为复杂
效率相对较高
*/
/* 实现基本思想:多方向并行搜索 + 队列存储 */
/* 实现基本原理:
* 1、定义一个链表结点 存储九宫格当前状态、上次移动的方向、移动之前的结点。(单向链表的结构)
* 2、定义一个队列 存储搜索的结果。同时搜索某一点的四个方向 将未重复的搜索结果状态加入到队列中存储
* 3、将当前状态、上次移动的方向、移动之前的项次对象 以结点的形式存储到链表中
* 4、循环搜索队列中所有存储的状态的下一步(包括四个方向)直到出现最终胜利状态 输出对应链表中的路径
* 备注:这里搜索队列中存储的状态需要将存储的string类型状态恢复为int的二维数组
* 5、当队列为空 但依然未搜索到最终状态时 认为此题无解
* */
/// <summary>
/// 广度优先算法代码实现
/// </summary>
public bool BFSSearch() {
/* 如果队列中还存在未搜索的状态 */
while (searchItemList.Count > 0) {
/* 从队首开始搜索 */
BFSSearchItem item = searchItemList[0];
/* 从队列中移除本次取出的结点 */
searchItemList.Remove(item); //searchItemList.RemoveAt(0);
/* 如果该结点的当前状态为最终状态 则获取到达该状态的路径 并结束搜索 */
if (item.STATUS == STATUS) {
/* 获取移动的路径到movePath集合中 */
GetRoute(item);
/* 搜索成功 结束 */
return true;
}
/* 如果当前结点非最终状态 恢复该状态为数组形式(包含重定位空格位置) */
RecoverStatus(item.STATUS);
/* 分别向4个方向继续搜索 将搜索结果继续保存 */
for (int i = 1; i <= 4; i++) // 1:LEFT 2:RIGHT 3:UP 4:DOWN
{
/* 移动操作 如果不满足移动条件或者移动失败 结束本次移动 继续其他方向的搜索 */
if (!Move(i)) { continue; }
/* 移动成功后 获取当前九宫格状态 */
string currentStatus = GetCurrentStatus();
/* 如果该状态已经搜索过 */
if (statusList.Contains(currentStatus)) {
/* 回退到上一步 */
if (!BackMove(i)) { throw new Exception("回退操作异常"); }
/* 放弃该状态在当前方向的搜索 继续搜索其他方向 */
continue;
}
/* 如果该状态未被搜索过 则加入待搜索的队列 并且同时标记该状态已搜索(待搜索中也属于已经搜索过) */
searchItemList.Add(new BFSSearchItem(currentStatus, i, item));
statusList.Add(currentStatus);
/* 恢复为本次移动之前的状态 尝试向其他方向移动 */
BackMove(i);
}
}
/* 当队列中所待搜索状态都已经搜索完成 仍未找到出口时 认为该题无解 结束算法 */
return false;
}
#endregion
#endregion
}
/// <summary>
/// BFS 算法存储每一步信息的链表结点类
/// </summary>
public class BFSSearchItem {
/// <summary>
/// 当前九宫格状态
/// </summary>
public string STATUS { get; set; }
/// <summary>
/// 上一次移动的方向
/// </summary>
public int? DIRECTION { get; set; }
/// <summary>
/// 上一次移动的结点对象
/// </summary>
public BFSSearchItem LASTITEM { get; set; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="status"></param>
/// <param name="dir"></param>
/// <param name="item"></param>
public BFSSearchItem(string status, int? dir, BFSSearchItem item) {
STATUS = status;
DIRECTION = dir;
LASTITEM = item;
}
}
}
定义主程序类。初始化并测试八数码问题求解
using System;
namespace EightDigital {
/// <summary>
/// 测试类 2018年9月25日13:51:32
/// </summary>
public class Program {
/* 题目(八数码问题):
有 1 - 8 八个数字,放在 3 * 3 的九宫格里面,留下一个空格(代码实现时,我们将空格表示为 0 )
3 4 1
5 6 0
8 2 7
空格可以和相邻的数字进行交换,如移动成
1 2 3
4 5 6
7 8 0
则移动完成 */
static void Main(string[] args) {
int[,] arr = { { 3, 4, 1 }, { 5, 6, 0 }, { 8, 2, 7 } };
//int[,] arr = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 0, 8 } };
//int[,] arr = { { 4, 5, 1 }, { 8, 3, 0 }, { 2, 7, 6 } };//左 左 上 右 下 左 下 右 右 上 左 左 下 右 上 上 右 下 左 左 上 右 下 右 下
//int[,] arr = { { 3, 4, 1 }, { 5, 0, 6 }, { 8, 2, 7 } };
EightDigital e = new EightDigital(arr);
//bool result = e.ExcuteDFS();
#region 算法分析 DFS算法的缺陷 --> BFS 算法的引入
/* 这里可以很明显的看出本次案例中 DFS 算法的弊端:即堆栈内存开销大、非最优解 */
/* 上面简单的例子 可以一眼看出 0 向右移动一次即可成功变为最终状态 */
/* 而运行 DFS 算法后 发现也确实输出为: True 右 */
/* 但是 若将 DFS 的搜索方向优先级改为 DFSSearch(LEFT) || DFSSearch(RIGHT) || DFSSearch(UP) || DFSSearch(DOWN) */
/* 则会发现 输出结果为:True 左 右 上 右 左 右 左 上 左 左 右 下 右 左 右 左 上 左 左 右 下 右 左 右 左 上 左
左 右 下 右 …………
/* 先不言其正确与否 即使其真的可以走通 但也可以确定的是 绝非最优路径 */
/* 不仅如此 在搜索过程中 也肯定会有很多类似的情况最终因算法选择了"错误"的路径而一路错误的搜索下去 误入歧途 */
/* 最终导致情况越来越复杂 甚至出现内存溢出等异常而终止 */
/* DFS 算法的这种情况就类似与现实生活中的"钻牛角尖" 它会在一个方向一直搜索下去直到该方向走不通为止 */
/* 如上题。因为算法认为左侧是可以走通且不违反规则 所以就会向左走 然后继续的一路遍历下去 殊不知如果第一步朝右 就已经胜利了 */
/* 我们要避免 DFS 算法带来的这种"歧途"效果 */
/* 首先 考虑到的应该就是 -- 是否可以每次对四个方向进行并行的搜索?然后择优? */
/* 如此思路 就牵扯到了 BFS 广度优先算法 */
#endregion
bool result = e.ExcuteBFS();
Console.WriteLine(result.ToString());
e.PrintCurrentPath();
//e.PrintCurrentStatus();
Console.ReadLine();
}
}
}