使用深度优先搜索(DFS)、广度优先搜索(BFS)、A* 搜索算法求解 (n^2 -1) 数码难题,耗时与内存占用(时空复杂度)对比(附:(n^2 - 1) 数码问题控制台简易演示程序)

一、实验目的

通过编程实验,体会并理解人工智能领域常用的新型搜索算法的测试用例 —— (n^2 -1) 数码问题。

二、实验内容和要求

通过深度优先搜索(DFS)、宽度优先搜索(BFS)、A*搜索算法来求解 (n^2 -1) 数码难题,要求如下:

  1. 初始状态以及目标状态形如下图。
  2. 输出完整的从初始状态到目标状态的动作序列。
  3. 对比3种算法的时间、空间消耗。
    在这里插入图片描述

三、参考实验环境

CPU:				Intel Core i5-8400 @ 2.80 GHz / 3.80 GHz, 6C6T
	指令集:			MMX, SSE, SSE2, SSE3, SSE4.1, SSE4.2, EM64T, VT-x, AES, AVX, AVX2, FMA3
	功耗限制:		PL1 = 75 W, PL2 = 130 W
	步进:			Stepping A, Revision U0
操作系统:			Windows 10 Professional 64-bit
IDE:				Visual Studio 2019
编译器:				Microsoft Visual C++(MSVC) Compiler
	参数:			/std:c17 /std:c++latest /O2 /Oi /GL
	参数解释:		ISO C17 (2018) Standard (/std:c17)
					Preview - Features from the Latest C++ Working Draft (/std:c++latest) (启用 C++20 最新特性)
					Maximum Optimization (Favor Speed) (/O2)
					Enable Intrinsic Functions: Yes (/Oi)
					Whole Program Optimization: Yes (/GL)

四、实验原理

深度优先搜索(DFS)

DFS 是一种搜索树或图的算法。在搜索过程中,该算法总是先尽量向深处(离出发点更远的位置,如果是树,则是从树根开始,往远离根的方向)搜索,再回溯并搜索其它分支。
虽然一些问题不直接具有树或图的结构,但可将其所有的状态视为一棵解答树或图。在本例中,棋盘的不同状态都能看成树的一个节点,每走一步相当于访问一个邻接节点。树根代表棋盘的初始状态。

广度优先搜索(BFS)

BFS 也是一种搜索树或图的算法。在搜索过程中,该算法总是先尽量搜索兄弟节点,再进入更深层的节点继续搜索。

A*搜索算法

A* 搜索算法属于启发式搜索算法,常用于求解最短路(最低开销路径)等问题。通过引入适当的启发式算法,在给出近似最优解的情形下,往往能比保证给出最优解的算法(如:Dijkstra 算法)具有更快的执行速率和更优秀的空间消耗。不同的启发式算法可以令 A* 算法具有各种不同的表现。
理想情况下,A* 亦可以保证给出最优解。

8 数码问题可能有解的条件

首先,我们把去掉空格后的 8 个数看成排列。不难发现:
· 空格左右移动,8 个数构成的排列中,没有任何两个数发生了位置上的交换,逆序数不变。
· 空格上下移动,8 个数构成的排列中,发生 2 次对换,它能通过2次相邻对换等效实现,逆序数不变。
即:初态棋盘对应的排列为奇(偶)排列时,不论空格怎样移动,得到的新棋盘对应的排列依然为奇(偶)排列。
因此,当初态与终态对应的排列的奇偶性不同时,无解。
事实上,有如下充分必要条件:
8 数码问题无解 ⟺ 初态与终态对应的排列的奇偶性不同。
8 数码问题有解 ⟺ 初态与终态对应的排列的奇偶性相同。
篇幅所限,这里不予证明(其实是我不会)。
可以通过改动归并排序算法,在 O(n log ⁡n) 的时间复杂度内求得一个排列的逆序数,n 为排列的长度。算法的正确性证明详见:https://blog.csdn.net/COFACTOR/article/details/109005737
本命题可以推广到更大规模的 15 数码问题、24 数码问题等。

五、源代码

代码解析

输入棋盘的初始状态和目标状态,在验证输入的样例有解后,本代码将依次使用 DFS、BFS 和 A* 算法求解。
本代码使用的 A* 算法是在 BFS 的基础上修改得到的。BFS 总是先扩展深度最浅的状态节点,但 A* 维护一个优先队列,每次选出 f(x) = g(x) + h(x) 最小的节点先行扩展。其中,g(x) 代表从当前状态 x 对应的节点在解答树中的深度;本代码选用当前状态 x与目标状态的 Hamming 距离作为启发式函数 h(x)。
为了避免陷入死循环,本代码还会在搜索过程中通过哈希(使用自定义哈希仿函数的 std::unordered_set 来实现)来判定从根节点到准备扩展的节点的路径上是否有重复的节点。若是,则不再继续扩展此节点。

在 DFS 的过程中,没有显式构造解答树。在 BFS 的过程中,一边构造解答树(树节点为结构体 bfs_node),一边使用队列(std::queue)进行搜索。在 A* 搜索的过程中,同样一边构造解答树(树节点为结构体 astar_node,节点要记录启发式搜索需要的额外信息),一边使用优先队列(std::priority_queue)进行搜索。

代码特色

·支持更大规模的此类问题,例如 15 数码问题和 24 数码问题。可以通过修改代码中预留的常量来求解这些问题。
·支持为棋盘填入不同类型、不同大小的数;可以使用不同的数表示空格,默认为数 0。
  相比之下,许多 8 数码问题的题目或实现代码只能为空格以外的 8 格分别填入 1 ~ 8 这八个整数。
·采用 C++ 风格与 C++11 开始的新特性编写代码。包括但不限于:
	·函数模板(template<class _Ty>)。
	· nullptr 关键字;
	· STL 容器新增的成员函数 emplace、emplace_back;
	·新标准的数据结构,如 std::array、std::unordered_set;
	· C++ 风格的输入输出;
	· C++ 风格的类型别名,如 using board_data_type = unsigned;;
	· C++ 风格的类型转换,如 static_cast<>();
	·高精度计时库 <chrono> 用于进行三种算法的运行耗时的测量。
  增强代码运行性能的同时,也增强可读性。

源代码

#include <algorithm>
#include <array>
#include <chrono>
#include <iostream>
#include <queue>
#include <set>
#include <unordered_set>
#include <vector>

using namespace std;
using namespace std::chrono;

using board_data_type = unsigned;
using board_coord_type = unsigned;
using board_depth_type = size_t;
using board_heuristic_value_type = size_t;

const board_coord_type BOARD_ROW_COUNT = 3, BOARD_COLUMN_COUNT = 3;

using board_column_type = array<board_data_type, BOARD_COLUMN_COUNT>;
using board_type = array<board_column_type, BOARD_ROW_COUNT>;

const board_data_type blank_token = 0;

board_type start, goal;
board_coord_type xs, ys;

struct step {
   
	board_type board;
	board_coord_type x, y;
	step() {
   }
	step(const board_type& B, const board_coord_type X, const board_coord_type Y) : board(B), x(X), y(Y) {
   }
};

inline bool can_up(const board_coord_type x) {
   
	if (x > 0) return true;
	return false;
}

inline bool can_down(const board_coord_type x) {
   
	if (x < BOARD_ROW_COUNT - 1) return true;
	return false;
}

inline bool can_left(const board_coord_type y) {
   
	if (y > 0) return true;
	return false;
}

inline bool can_right(const board_coord_type y) {
   
	if (y < BOARD_COLUMN_COUNT - 1) return true;
	return false;
}

inline void up_b(board_type& board, board_coord_type& x, board_coord_type& y) {
   
	swap(board[x][y], board[x - 1][y]);
	--x;
}

inline void down_b(board_type& board, board_coord_type& x, board_coord_type& y) {
   
	swap(board[x][y], board[x + 1][y]);
	++x;
}

inline void left_b(board_type& board, board_coord_type& x, board_coord_type& y) {
   
	swap(board[x][y], board[x][y - 1]);
	--y;
}

inline void right_b(board_type& board, board_coord_type& x, board_coord_type& y) {
   
	swap(board[x][y], board[x][y + 1]);
	++y;
}

inline void up_b(step& s) {
   
	swap(s.board[s.x][s.y], s.board[s.x - 1][s.y]);
	--s.x;
}

inline void down_b(step& s) {
   
	swap(s.board[s.x][s.y], s.board[s.x + 1][s.y]);
	++s.x;
}

inline void left_b(step& s) {
   
	swap(s.board[s.x][s.y], s.board[s.x][s.y - 1]);
	--s.y;
}

inline void right_b(step& s) {
   
	swap(s.board[s.x][s.y], s.board[s.x][s.y + 1]);
	++s.y;
}

void print_solution(const vector<step>& solution) {
   
	cout << "----------------------------------------------------------------" << endl;
	for (board_coord_type i = 0; i < BOARD_ROW_COUNT; ++i) {
   
		for (board_coord_type j = 0; j < BOARD_COLUMN_COUNT; ++j) cout << solution.cbegin()->board[i][j];
		//cout << '\n';
		cout << ' ';
	}
	for (vector<step>::const_iterator h = solution.cbegin() + 1; h != solution.cend(); ++h) {
   
		if (h->x - (h - 1)->x == -1 && h->y - (h - 1)->y == 0) cout << "Up" << endl;
		else if (h->x - (h - 1)->x == 1 && h->y - (h - 1
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值