js实现八码数问题
问题描述
八数码问题,在3×3的方格棋盘上,摆放着1到8这八个数码,有1个方格是空的,用数字0代表。要求对空格执行空格左移、空格右移、空格上移和空格下移这四个操作使得棋盘从初始状态到目标状态。例如:
2 8 3
1 0 4
7 6 5
初试状态
1 2 3
8 0 4
7 6 5
目标状态
请任选一种盲目搜索算法(深度优先搜索或宽度优先搜索)或任选一种启发式搜索方法(A 算法或A* 算法)编程求解八数码问题(初始状态任选),并对实验结果进行分析,得出合理的结论。
实验原理
实验环境:JavaScript编程语言
算法实现原理:八码数问题(也称为滑块谜题)是一个经典的人工智能问题,目标是从一个初始状态通过有限步数达到目标状态。广度优先搜索(BFS)是一种逐层扩展搜索的算法,适用于求解最短路径或最少步数问题。在八码数问题中,BFS会探索所有可能的下一步状态,直到找到目标状态。
程序框图
实验步骤
- 定义数据结构和辅助函数
-
定义状态表示方法(如数组)。
-
实现检查两个状态是否相等的函数。
-
实现生成下一个状态的函数。
- 实现BFS算法
- 初始化队列和访问集合。
- 将初始状态入队并标记为已访问。
- 开始BFS循环,直到队列为空或找到目标状态。
- 在每次循环中,出队当前状态,生成所有可能的下一个状态,并检查每个新状态。
- 测试和验证
- 使用不同的初始状态和目标状态进行测试。
- 验证算法的正确性和效率。
基本数据结构分析和实现
-
状态表示:使用数组表示八码数状态,其中0表示空格。
-
队列:用于存储待处理的状态。
-
集合:用于存储已访问的状态,以避免重复处理。
-
代码如下:
class Node {
constructor(state, parent, action, depth) {
this.state = state;
this.parent = parent;
this.action = action;
this.depth = depth;
}
}
function isGoal(state, goalState) {
return state.join('') === goalState.join('');
}
function getStateIndex(state) {
return state.join(',');
}
function getNeighbors(state) {
const neighbors = [];
const zeroIndex = state.indexOf(0);
const x = Math.floor(zeroIndex / 3);
const y = zeroIndex % 3;
const directions = [
{ dx: -1, dy: 0 }, { dx: 1, dy: 0 },
{ dx: 0, dy: -1 }, { dx: 0, dy: 1 }
];
for (const { dx, dy } of directions) {
const newX = x + dx;
const newY = y + dy;
if (newX >= 0 && newX < 3 && newY >= 0 && newY < 3) {
const newIndex = newX * 3 + newY;
const newState = [...state];
[newState[zeroIndex], newState[newIndex]] = [newState[newIndex], newState[zeroIndex]];
neighbors.push(newState);
}
}
return neighbors;
}
编写程序的各个子模块
模块1:初始化
- 建立时间:实验开始时。
- 功能:初始化队列、访问集合和初始状态。
- 输入参数:初始状态和目标状态。
- 输出参数:无。
- 与其它模块联系:调用生成节点和BFS算法模块。
function initialize(initialState, goalState) {
const startNode = new Node(initialState, null, null, 0);
const queue = [startNode];
const visited = new Set([getStateIndex(initialState)]);
return { queue, visited, goalState };
}
模块2:BFS算法
- 建立时间:实验进行时。
- 功能:执行广度优先搜索。
- 输入参数:初始化模块的输出。
- 输出参数:解路径或失败信息。
- 与其它模块联系:调用检查目标和生成邻居模块。
function bfs(initialState, goalState) {
const { queue, visited, goalState } = initialize(initialState, goalState);
while (queue.length > 0) {
const currentNode = queue.shift();
if (isGoal(currentNode.state, goalState)) {
return constructPath(currentNode);
}
const neighbors = getNeighbors(currentNode.state);
for (const neighbor of neighbors) {
const neighborIndex = getStateIndex(neighbor);
if (!visited.has(neighborIndex)) {
const newNode = new Node(neighbor, currentNode, null, currentNode.depth + 1);
queue.push(newNode);
visited.add(neighborIndex);
}
}
}
return null; // 无解
}
模块3:构造路径
- 建立时间:找到目标状态时。
- 功能:从目标节点回溯构造解路径。
- 输入参数:目标节点。
- 输出参数:解路径。
- 与其它模块联系:被BFS算法模块调用。
function constructPath(node) {
const path = [];
while (node !== null) {
path.unshift(node.state);
node = node.parent;
}
return path;
}
完整源码
class Node {
constructor(state, parent, action, depth) {
//设置节点的状态,state 是一个数组,表示当前的状态
this.state = state;
//设置节点的父节点,parent 是另一个 Node 实例,表示当前节点是从哪个节点扩展而来的
this.parent = parent;
//设置导致当前状态的动作或移动,action 可以是任何描述动作的对象或值
this.action = action;
//设置节点的深度,depth 是一个整数,表示从初始节点到当前节点的步数
this.depth = depth;
}
}
//判断是否是目标状态
function isGoal(state, goalState) {
// 将 state 和 goalState 数组转换为字符串,并比较它们是否相等。如果相等,说明当前状态是目标状态。
return state.join("") === goalState.join("");
}
// 将 state 数组的元素用逗号连接成一个字符串,作为该状态的唯一索引或标识。
function getStateIndex(state) {
return state.join(",");
}
//获取当前节点的邻居节点
function getNeighbors(state) {
// 初始化一个空数组,用于存储当前状态的所有邻居状态(即可以通过一步移动到达的状态)
const neighbors = [];
// 找到状态数组中值为 0 的元素的索引
const zeroIndex = state.indexOf(0);
// 计算 0 所在位置的行、列坐标
const x = Math.floor(zeroIndex / 3);
const y = zeroIndex % 3;
// 定义一个包含四个方向的数组,每个方向都是一个对象,表示在网格中可以移动的方向
const directions = [
//上
{ dx: -1, dy: 0 },
//下
{ dx: 1, dy: 0 },
//左
{ dx: 0, dy: -1 },
//右
{ dx: 0, dy: 1 }
];
// 遍历每个方向
for (const { dx, dy } of directions) {
// 计算新位置的行、列坐标
const newX = x + dx;
const newY = y + dy;
// 检查新位置是否在网格内
if (newX >= 0 && newX < 3 && newY >= 0 && newY < 3) {
const newIndex = newX * 3 + newY;
const newState = [...state];
// 交换 0 和新位置上的元素,生成新的状态
[newState[zeroIndex], newState[newIndex]] = [newState[newIndex], newState[zeroIndex]];
// 将新的状态添加到邻居数组中
neighbors.push(newState);
}
}
// 返回包含所有邻居状态的数组
return neighbors;
}
//初始化
function init(initialState, goalState) {
const startNode = new Node(initialState, null, null, 0);
const queue = [startNode];
创建一个Set集合,用于存储已经访问过的状态
const visited = new Set([getStateIndex(initialState)]);
// 返回一个对象,该对象包含队列、已访问集合和目标状态
return { queue, visited, goalState };
}
//算法:广度优先搜索
function bfs(start, goal) {
调用init函数来初始化搜索所需的数据结构,包括队列、已访问集合和目标状态
const { queue, visited, goalState } = init(start, goal);
// 当队列不为空时,继续搜索
while (queue.length > 0) {
// 从队列头部取出一个节点作为当前节点
const currentNode = queue.shift();
// 检查当前节点的状态是否为目标状态
if (isGoal(currentNode.state, goal)) {
// 如果是目标状态,构造该路径,并返回该路径
return constructPath(currentNode);
}
// 获取当前节点状态的所有邻居状态
const neighbors = getNeighbors(currentNode.state);
//遍历每一个邻居状态
for (const neighbor of neighbors) {
const neighborIndex = getStateIndex(neighbor);
// 检查邻居状态是否已经被访问过
if (!visited.has(neighborIndex)) {
// 如果邻居状态未被访问过,则创建一个新的节点来表示该邻居状态
// 新节点的状态为邻居状态,父节点为当前节点,动作为null(因为这里没有具体动作信息),深度为当前节点深度加1
const newNode = new Node(neighbor, currentNode, null, currentNode.depth + 1);
// 将新节点加入队列,以便后续访问
queue.push(newNode);
//标记为已访问
visited.add(neighborIndex);
}
}
}
return null; // 无解
}
//构造路径
function constructPath(node) {
// 初始化一个空数组path,用于存储从初始状态到目标状态的路径
const path = [];
// 当节点不为null时,继续循环
while (node !== null) {
// 将当前节点的状态添加到path数组的开头 从目标状态开始,回溯到初始状态,所以路径应该是反向的
path.unshift(node.state);
// 将当前节点设置为它的父节点 这样我们就可以继续回溯到上一个状态
node = node.parent;
}
return path;
}
//转换为3*3输出
function changeFn(step) {
for (let i = 0; i < 3; i++) {
let row = "";
for (let j = 0; j < 3; j++) {
const index = i * 3 + j;
row += " " + step[index] + " ";
}
console.log(row);
}
}
//输出
function outPutStep(start, goal) {
const result = bfs(start, goal);
if (result) {
console.log("一共走了", result.length - 1, "步");
result.forEach((step, index) => {
if (index == 0) {
console.log("初始状态为:");
changeFn(step);
} else {
console.log("====================第" + index + "步");
changeFn(step);
}
});
}
}
// 案例
const start = [2, 8, 3, 1, 0, 4, 7, 6, 5];
const goal = [1, 2, 3, 8, 0, 4, 7, 6, 5];
outPutStep(start, goal);
运行结果
初始状态:
[1, 2, 3,
4, 0, 5,
6, 7, 8]
目标状态:
[1, 2, 3,
4, 5, 6,
7, 8, 0]
运行结果:
实验分析与讨论
- 算法效率:BFS算法在解决八码数问题时,由于需要探索所有可能的下一步状态,因此空间和时间复杂度都较高。对于较复杂的初始状态,可能需要较长的运行时间和较大的存储空间。
- 解的存在性:BFS算法能够保证找到最短路径,但如果初始状态与目标状态之间不存在解路径,算法将遍历所有可能的状态,这在实际应用中可能不可行。
- 优化方向:可以考虑使用启发式搜索算法(如A*算法)来优化搜索过程,通过引入评估函数来指导搜索方向,减少不必要的探索。
结论
本实验使用JavaScript实现了广度优先搜索(BFS)算法来解决八码数问题。通过定义适当的数据结构和辅助函数,以及实现BFS算法和路径构造函数,成功找 到了从初始状态到目标状态的解路径。实验结果表明,BFS算法能够正确解决八码数问题,但存在效率较低的问题。未来可以考虑引入启发式搜索算法来优化搜索过程。