人工智能学习栈记——迷宫问题(Java)

迷宫问题是一类经典的搜索策略问题。首先创建一个10*10的迷宫,生成障碍物,寻找从入口到出口的方法。

Cell

我们首先定义存放迷宫的二维数组,为了显示其路径,我们定义了一个枚举类Cell.java

public enum Cell {
	EMPTY(" "),
	START("S"),
	GOAL("G"),
	BLOCK("X"),
	PATH("*");

	private String code; // 用于表示Cell的字符

	Cell(String code) {
		this.code = code;
	}

	@Override
	public String toString() {
		return code; // 返回Cell的字符
	}
}

其中,空格代表空,“S”表示开始,“G”表示终点,“X”表示障碍,“*”表示走过的路径

MazeLocation

随后,定义表示在迷宫的某一个位置的类MazeLocation,用于取当前位置、向各个方向行走的事件。

该类的equals方法用于判断输入,hashCode由于判断走过的路径是否重复,并构造Getter方法和重写toString方法。

import java.util.Objects;

/**
 * 表示迷宫中的一个位置
 */
public class MazeLocation {
	private int rowN;
	private int colN;

	public MazeLocation(int rowN, int colN) {
		super();
		this.rowN = rowN;
		this.colN = colN;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) return true;
		if (obj == null) return false;
		if (getClass() != obj.getClass()) return false;
		MazeLocation other = (MazeLocation) obj;
		return colN == other.colN && rowN == other.rowN;
	}

	@Override
	public int hashCode() {
		return Objects.hash(rowN, colN);
	}

	public int getRowN() {
		return rowN;
	}

	public int getColN() {
		return colN;
	}

	@Override
	public String toString() {
		return "MazeLocation [rowN=" + rowN + ", colN=" + colN + "]";
	}
}

Maze

接下来,定义随机迷宫,需要有以下的参数,行,列,障碍密集度,起始位置,终点位置,一个存储迷宫的二维数组。

int rowCount;
int colCount;
double separseness; // 至少有20%的空白
MazeLocation start;
MazeLocation goal;

Cell[][] grids;

定义构造方法:

public Maze() {
	this(9,9,0.1,new MazeLocation(0,0),new MazeLocation(8,8));
}

public Maze(int rowCount, int colCount, double separseness, MazeLocation start, MazeLocation goal) {
	super();
	this.rowCount = rowCount;
	this.colCount = colCount;
	this.separseness = separseness;
	grids = new Cell[rowCount][colCount];
	this.start = start;
	this.goal = goal;
	initMaze();
	fillRandomBlock();
	fillStartAndGoal();
}

在构造方法中,我们需要初始化迷宫(initMaze),随机填充障碍(fillRandomBlock)和填充开始结束为止(fillStartAndGoal),下面定义这些私有方法

private void initMaze() {
	for (int i = 0; i < rowCount; i++) {
		for (int j = 0; j < colCount; j++) {
			grids[i][j] = Cell.EMPTY;
		}
	}
}

private void fillRandomBlock() { // 初始化迷宫
	for (int i = 0; i < rowCount; i++) {
		for (int j = 0; j < colCount; j++) {
			if (Math.random() < separseness) {
				grids[i][j] = Cell.BLOCK;
			}
		}
	}
}

private void fillStartAndGoal() {
	int sRowN = start.getRowN(), sColN = start.getColN();
	int gRowN = goal.getRowN(), gColN = goal.getColN();

	grids[sRowN][sColN] = Cell.START;
	grids[gRowN][gColN] = Cell.GOAL;
}

初始化迷宫,当生成的随机数小于设定的障碍密集度时就设定障碍。

接下来定义fillStartAndGoal方法里需要的Getter方法和重写toString方法,以及调用测试,标记路径、清除重复走的路径的方法。

public int getRowCount() {
	return rowCount;
}

public int getColCount() {
	return colCount;
}

public boolean testGoal(MazeLocation loc) {
	return loc.equals(goal);
}

public void mark(List<MazeLocation> mls) {
	for (MazeLocation loc : mls) { // 标记路径
		grids[loc.getRowN()][loc.getColN()] = Cell.PATH;
	}
	// 标记起点和终点
	grids[start.getRowN()][start.getColN()] = Cell.START;
	grids[goal.getRowN()][goal.getColN()] = Cell.GOAL;
}

public void clear(List<MazeLocation> mls) {
	for (int i = 0; i < rowCount; i++) {
		for (int j = 0; j < colCount; j++) {
			if (grids[i][j] == Cell.PATH) {
				grids[i][j] = Cell.EMPTY;
			}
		}
	}
}

@Override
public String toString() {
	StringBuilder sb = new StringBuilder();
	for (int i = 0; i < rowCount; i++) {
		for (int j = 0; j < colCount; j++) {
			sb.append(grids[i][j]);
			sb.append(" ");
		}
		sb.append("\n");
	}
	return sb.toString();
}

随后,定义行走的方法,我们用一个列表来记录行走的路径。

public List<MazeLocation> successor(MazeLocation loc) {
	List<MazeLocation> result = new ArrayList<>();
	int rowN = loc.getRowN(), colN = loc.getColN();
	// 右
	if (colN + 1 < colCount && grids[rowN][colN + 1] != Cell.BLOCK) {
		result.add(new MazeLocation(rowN, colN + 1));
	}
	// 左
	if (colN - 1 >= 0 && grids[rowN][colN - 1] != Cell.BLOCK) {
		result.add(new MazeLocation(rowN, colN - 1));
	}
	// 上
	if (rowN - 1 >= 0 && grids[rowN - 1][colN] != Cell.BLOCK) {
		result.add(new MazeLocation(rowN - 1, colN));
	}
	// 下
	if (rowN + 1 < rowCount && grids[rowN + 1][colN] != Cell.BLOCK) {
		result.add(new MazeLocation(rowN + 1, colN));
	}
	return result;
}

Maze.java 完整代码如下:

import java.util.*;

public class Maze {
	int rowCount;
	int colCount;
	double separseness; // 至少有20%的空白
	MazeLocation start;
	MazeLocation goal;

	Cell[][] grids;

	public Maze() {
		this(9,9,0.1,new MazeLocation(0,0),new MazeLocation(8,8));
	}

	public Maze(int rowCount, int colCount, double separseness, MazeLocation start, MazeLocation goal) {
		super();
		this.rowCount = rowCount;
		this.colCount = colCount;
		this.separseness = separseness;
		grids = new Cell[rowCount][colCount];
		this.start = start;
		this.goal = goal;
		initMaze();
		fillRandomBlock();
		fillStartAndGoal();
	}

	public int getRowCount() {
		return rowCount;
	}

	public int getColCount() {
		return colCount;
	}

	private void initMaze() {
		for (int i = 0; i < rowCount; i++) {
			for (int j = 0; j < colCount; j++) {
				grids[i][j] = Cell.EMPTY;
			}
		}
	}

	private void fillRandomBlock() { // 初始化迷宫
		for (int i = 0; i < rowCount; i++) {
			for (int j = 0; j < colCount; j++) {
				if (Math.random() < separseness) {
					grids[i][j] = Cell.BLOCK;
				}
			}
		}
	}

	private void fillStartAndGoal() {
		int sRowN = start.getRowN(), sColN = start.getColN();
		int gRowN = goal.getRowN(), gColN = goal.getColN();

		grids[sRowN][sColN] = Cell.START;
		grids[gRowN][gColN] = Cell.GOAL;
	}

	public List<MazeLocation> successor(MazeLocation loc) {
		List<MazeLocation> result = new ArrayList<>();
		int rowN = loc.getRowN(), colN = loc.getColN();
		// 右
		if (colN + 1 < colCount && grids[rowN][colN + 1] != Cell.BLOCK) {
			result.add(new MazeLocation(rowN, colN + 1));
		}
		// 左
		if (colN - 1 >= 0 && grids[rowN][colN - 1] != Cell.BLOCK) {
			result.add(new MazeLocation(rowN, colN - 1));
		}
		// 上
		if (rowN - 1 >= 0 && grids[rowN - 1][colN] != Cell.BLOCK) {
			result.add(new MazeLocation(rowN - 1, colN));
		}
		// 下
		if (rowN + 1 < rowCount && grids[rowN + 1][colN] != Cell.BLOCK) {
			result.add(new MazeLocation(rowN + 1, colN));
		}
		return result;
	}

	public boolean testGoal(MazeLocation loc) {
		return loc.equals(goal);
	}

	public void mark(List<MazeLocation> mls) {
		for (MazeLocation loc : mls) { // 标记路径
			grids[loc.getRowN()][loc.getColN()] = Cell.PATH;
		}
		// 标记起点和终点
		grids[start.getRowN()][start.getColN()] = Cell.START;
		grids[goal.getRowN()][goal.getColN()] = Cell.GOAL;
	}

	public void clear(List<MazeLocation> mls) {
		for (int i = 0; i < rowCount; i++) {
			for (int j = 0; j < colCount; j++) {
				if (grids[i][j] == Cell.PATH) {
					grids[i][j] = Cell.EMPTY;
				}
			}
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < rowCount; i++) {
			for (int j = 0; j < colCount; j++) {
				sb.append(grids[i][j]);
				sb.append(" ");
			}
			sb.append("\n");
		}
		return sb.toString();
	}
}

至此,迷宫的构建全部完成

深度优先搜索

我们首先使用深度优先搜索(DFS)算法探索迷宫。深度优先搜索的主要思路是,先一直探索一个结点的某一个子结点,直到没有子结点为止,如果当前结点不是终点,则回溯继续探索其他子结点。

我们知道,DFS使用的是堆栈的数据结构。当该结点有子结点就入栈,直到没有为止。

因此,我们现需要定义一个结点类

Node

public class Node<T> {
	Node<T> parent;
	T currentNode;

	public Node(T currentNode, Node<T> parent) {
		super();
		this.parent = parent;
		this.currentNode = currentNode;
	}

	public Node<T> getParent() {
		return parent;
	}

	public T getCurrentNode() {
		return currentNode;
	}
}

Node类是一个泛型类,用于存放迷宫的某一个位置。

BFS

我们会定义一个GenerateSearcher类,存放各种搜过策略的方法。(代码最后给出)

下面展示bfs的具体方法

// 深度优先搜索
public static <T> Node<T> dfs(Function<T, List<T>> successors, Predicate<T> goalTest, T start) {
	Node<T> sn = new Node<>(start, null);
	Stack<Node<T>> frontier = new Stack<>();
	frontier.push(sn);
	Set<T> explored = new HashSet<>();
	explored.add(start);

	while (!frontier.isEmpty()) {
		sn = frontier.pop();
		T location = sn.getCurrentNode();
		if (goalTest.test(location)) {
			break;
		}

		for (T child : successors.apply(location)) {
			if (explored.contains(child)) {
				continue;
			}
			Node<T> newNode = new Node<>(child, sn);
			explored.add(child);
			frontier.push(newNode);
		}
	}
	return sn;
}

DFS 算法的主循环在 frontier 栈不为空时运行。在循环内部,方法从栈中弹出一个节点 (sn = frontier.pop()) 并获取其当前状态 (T location = sn.getCurrentNode())。然后检查该状态是否满足目标条件,如果满足则退出循环。

如果当前节点不是目标,方法通过应用 successors 函数来获取后继节点列表。然后遍历这些后继节点。对于每个后继节点,检查该节点是否已经被探索过。如果没有,则为该后继节点创建一个新的 Node 对象,并将当前节点作为其父节点 (new Node<>(child, sn))。然后将该新节点添加到 explored 集合和 frontier 栈中。

最后,一旦循环退出,方法返回表示目标状态的节点或最后一个被探索的节点。

生成单元测试

下面是一个单元测试:

@Test
public void testDFS() {
	Maze maze = new Maze(10, 10, 0.2, new MazeLocation(0, 0), new MazeLocation(9, 9));
	// MazeLocation goal = new MazeLocation(9, 9); // 终点
	MazeLocation start = new MazeLocation(0, 0); // 起点

	Node<MazeLocation> node = GenericSearcher.dfs(maze::successor, maze::testGoal, start); // 深度优先搜索
	List<MazeLocation> path = GenericSearcher.nodePath(node); // 获得路径

	maze.mark(path); // 标记路径
	System.out.println(maze); // 输出迷宫
}

运行结果

显然dfs不是最短路径,他会绕远路。

广(宽)度优先搜索

广(宽)度优先搜索(BFS)则是一步一步探索该结点的所有子结点。其用到的数据结构则是队列,因此,需要调用Java.util.Quene。

// 广度优先搜索
public static <T> Node<T> bfs(Function<T, List<T>> successors, Predicate<T> goalTest, T start) {
	Node<T> sn = new Node<>(start, null);
	Queue<Node<T>> frontier = new LinkedList<>(); // 构造一个空队列
	frontier.add(sn); // 将初始节点加入队列
	Set<T> explored = new HashSet<>(); // 用于记录已经探索过的节点
	explored.add(start);

	while (!frontier.isEmpty()) {
		sn = frontier.poll();
		T location = sn.getCurrentNode(); // 取出队列的头节点
		if (goalTest.test(location)) { // 判断是否为目标节点,是则退出
			break;
		}

		for (T child : successors.apply(location)) { // 遍历当前节点的所有子节点
			if (explored.contains(child)) { // 如果子节点已经探索过,则跳过
				continue;
			}
			Node<T> newNode = new Node<>(child, sn);
			explored.add(child); // 标记为已探索
			frontier.add(newNode); // 将子节点加入队列
		}
	}
	return sn;
}

生成单元测试

@Test
public void testBFS() {
	Maze maze = new Maze(10, 10, 0.2, new MazeLocation(0, 0), new MazeLocation(9, 9));
	// MazeLocation goal = new MazeLocation(9, 9);
	MazeLocation start = new MazeLocation(0, 0);

	Node<MazeLocation> node = GenericSearcher.bfs(maze::successor, maze::testGoal, start);
	List<MazeLocation> path = GenericSearcher.nodePath(node);

	maze.mark(path);
	System.out.println(maze);
}

测试结果

这种方法相比于深度优先搜索,路径较优

GenericSearcher

最后给出存储两种搜索策略的GenericSearcher类

import java.util.*;
import java.util.function.*;

public class GenericSearcher {
	// 深度优先搜索
	public static <T> Node<T> dfs(Function<T, List<T>> successors, Predicate<T> goalTest, T start) {
		Node<T> sn = new Node<>(start, null);
		Stack<Node<T>> frontier = new Stack<>();
		frontier.push(sn);
		Set<T> explored = new HashSet<>();
		explored.add(start);

		while (!frontier.isEmpty()) {
			sn = frontier.pop();
			T location = sn.getCurrentNode();
			if (goalTest.test(location)) {
				break;
			}

			for (T child : successors.apply(location)) {
				if (explored.contains(child)) {
					continue;
				}
				Node<T> newNode = new Node<>(child, sn);
				explored.add(child);
				frontier.push(newNode);
			}
		}
		return sn;
	}

	// 获得路径
	public static <T> List<T> nodePath(Node<T> node) {
		List<T> result = new ArrayList<>();

		Node<T> parent = node.getParent();
		T currentState = node.getCurrentNode();
		result.add(currentState);

		while (parent != null) {
			currentState = parent.getCurrentNode();
			result.add(currentState);
			parent = parent.getParent();
		}

		return result;
	}

	// 广度优先搜索
	public static <T> Node<T> bfs(Function<T, List<T>> successors, Predicate<T> goalTest, T start) {
		Node<T> sn = new Node<>(start, null);
		Queue<Node<T>> frontier = new LinkedList<>(); // 构造一个空队列
		frontier.add(sn); // 将初始节点加入队列
		Set<T> explored = new HashSet<>(); // 用于记录已经探索过的节点
		explored.add(start);

		while (!frontier.isEmpty()) {
			sn = frontier.poll();
			T location = sn.getCurrentNode(); // 取出队列的头节点
			if (goalTest.test(location)) { // 判断是否为目标节点,是则退出
				break;
			}

			for (T child : successors.apply(location)) { // 遍历当前节点的所有子节点
				if (explored.contains(child)) { // 如果子节点已经探索过,则跳过
					continue;
				}
				Node<T> newNode = new Node<>(child, sn);
				explored.add(child); // 标记为已探索
				frontier.add(newNode); // 将子节点加入队列
			}
		}
		return sn;
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值