目录
前言
A.建议
1.学习算法最重要的是理解算法的每一步,而不是记住算法。
2.建议读者学习算法的时候,自己手动一步一步地运行算法。
B.简介
八数码难题(也称为滑动拼图游戏或十五拼图游戏的一种变形)是一个经典的组合优化问题,它在一个3x3的网格上进行,网格上有8个数字块和一个空白格子。每个数字块可以在空格相邻的上下左右四个方向上进行移动,目标是通过一系列这样的移动操作,将最初乱序排列的数字块按照特定的顺序排列。
一 代码实现
在C语言中,我们可以用结构体表示状态,比如:
typedef struct {
int board[3][3]; // 用来存储3x3棋盘的状态
// 可能还包括其他信息,如父节点、移动步数等
} PuzzleState;
// 假设目标状态:
const int targetBoard[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 0} // 0代表空白格子
};
在实现解决方案时,可以使用多种搜索算法,如广度优先搜索(BFS)、深度优先搜索(DFS)、A*搜索等。以下是使用BFS的基本思路:
- 创建队列:用于存放待搜索的状态。
- 定义状态转换函数:根据当前状态,生成所有可能的下一状态(即一次移动之后的状态)。
- 检查目标状态:每当从队列中取出一个状态时,检查它是否为目标状态,如果是,则找到了解。
- 加入新状态:如果不是目标状态,则生成的所有合法的新状态加入队列,等待后续搜索。
对于C语言实现,你可能会有一个核心循环,像这样:
#include <queue>
#include <iostream> // 可能需要用于打印状态
// 假设PuzzleState的定义
struct PuzzleState {
// 这里定义谜题状态的数据成员,例如board是一个二维数组或类矩阵
int board[ROWS][COLS];
// 可能还有记录路径的额外信息,此处省略
};
// 假设Queue是一个基于std::queue的封装类
class Queue {
public:
std::queue<PuzzleState*> states;
// 初始化队列
Queue() {}
// 添加状态到队列末尾
void Enqueue(PuzzleState* state) {
states.push(state);
}
// 从队列头部移除并返回状态
PuzzleState* Dequeue() {
PuzzleState* front = states.front();
states.pop();
return front;
}
// 判断队列是否为空
bool IsEmpty() const {
return states.empty();
}
// 清理队列,这里假定我们需要删除队列中动态分配的状态
~Queue() {
while (!states.empty()) {
delete states.front();
states.pop();
}
}
};
// 目标状态比较函数
bool IsTargetState(const PuzzleState& currentState, const PuzzleState& targetState) {
// 实现比较逻辑,确定当前状态是否为目标状态
// ...
return /*比较结果*/ true; // 替换为实际比较逻辑
}
// 生成所有可能的下一个状态并加入队列
void GenerateNextStates(const PuzzleState& currentState, Queue* queue) {
// 根据谜题规则生成currentState的后续状态,并将其加入queue
// ...
for (const auto& nextState : generatedStates) {
PuzzleState* newState = new PuzzleState(nextState);
queue->Enqueue(newState);
}
}
int main() {
// 初始化队列
Queue queue;
// 初始化起始状态并加入队列
PuzzleState initial;
// ... 设置initial.board...
// 假设已设置好initial.board
queue.Enqueue(&initial);
while (!queue.IsEmpty()) {
PuzzleState* currentState = queue.Dequeue();
// 检查是否达到目标状态
if (IsTargetState(*currentState, targetBoard)) {
// 打印解题路径或返回解
std::cout << "找到解!\n";
//... 打印或处理解
break;
}
// 生成所有可能的下一个状态
GenerateNextStates(*currentState, &queue);
}
// 如果Queue类内部已经实现了析构函数来清理队列中的动态分配的状态,则无需额外清理
// 如果没有实现自动清理,这里需要手动释放内存,但这通常不建议在现代C++中这样做
// 因为在上述Queue类设计中,析构函数已经负责了这一工作
return 0;
}
注意,上述代码只是一个概念上的实现,实际应用中需要根据具体的谜题规则来填充IsTargetState
和GenerateNextStates
函数的内容,并确保适当管理和释放内存(如果使用了动态分配)。另外,在C++标准库中直接使用std::queue<PuzzleState>
而不是自定义Queue
类也是可行的,只需稍作调整即可。
二 时空复杂度
A.时间复杂度分析:
-
对于 广度优先搜索(BFS) 来说,如果每一步都均匀分布(即任何合法移动都能到达最终解),则时间复杂度理论上是最优的,约为,其中
b
是宽度(即每次扩展状态下最多能产生的新状态数量,在八数码问题中最大宽度是4,因为空白格子最多可以移动到相邻的4个位置),d
是解的步数。 -
若使用 深度优先搜索(DFS) ,在最坏情况下(如果没有剪枝策略,且状态空间巨大时),可能会导致指数级的时间复杂度。
-
对于 A*搜索 或 IDA*搜索 加上了启发式函数,若启发式函数是理想的Admissible(保守)和Consistent(一致),则能在最优情况下达到线性时间复杂度,但实际复杂度取决于启发式函数的好坏,一般会优于无信息搜索。
B.空间复杂度分析:
-
存储队列(或堆栈,对于DFS)中的中间状态会导致的空间复杂度为
O(bd)
,因为在最坏情况下,搜索树的每一层都有b
个状态,共搜索d
层。 -
如果使用哈希表来记录已经访问过的状态以避免重复,则空间复杂度取决于状态总数,对于八数码问题,共有
9!
种不同的排列(不考虑空白格子的位置),因此理论上哈希表空间复杂度可能高达O(9!)
,但在实际应用中,很多状态是重复的,所以实际占用的空间会小得多。