迷宫生成和求解器 | KMP鸿蒙Kotlin实现
本文介绍了基于Kotlin Multiplatform(KMP)的迷宫生成与求解工具实现方案。该工具采用递归回溯算法生成3×3至10×10的随机迷宫,并通过BFS算法求解最短路径,支持ASCII可视化展示和统计分析。核心功能包括:迷宫初始化、递归回溯生成、墙/通路统计、广度优先搜索路径求解以及ASCII字符可视化。该方案充分利用KMP跨平台特性,一份代码可同时支持多个平台,为OpenHarmony

目录
概述
本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个完整的迷宫生成和求解系统。这个案例展示了如何使用 Kotlin 的递归算法、图论和搜索算法来创建一个功能丰富的迷宫工具。通过 KMP,这个工具可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行,并支持用户输入进行实时处理。
工具的特点
- 迷宫生成:使用递归回溯算法生成随机迷宫
- 路径求解:使用 BFS 算法找到最短路径
- 可视化展示:用 ASCII 字符展示迷宫和路径
- 统计分析:分析迷宫的结构特性
- 跨端兼容:一份 Kotlin 代码可同时服务多个平台
工具功能
1. 迷宫生成
- 大小范围:支持 3×3 到 10×10 的迷宫
- 递归回溯:使用高效的递归回溯算法
- 随机性:每次生成不同的迷宫
2. 迷宫统计
- 总格子数:计算迷宫的总格子数
- 墙数量:统计墙的个数和百分比
- 通路数:统计通路的个数和百分比
3. 路径求解
- BFS 搜索:使用广度优先搜索找最短路径
- 路径长度:显示从起点到终点的步数
- 可解性:判断迷宫是否有解
4. 可视化展示
- 迷宫图:用 ASCII 字符展示迷宫
- 解决方案:用不同符号标记求解路径
- 清晰标记:S 表示起点,E 表示终点
核心实现
1. 迷宫网格初始化
val maze = Array(size) { IntArray(size) { 1 } } // 1=墙, 0=通路
val visited = Array(size) { BooleanArray(size) { false } }
这段代码初始化了迷宫的基础数据结构。创建一个 size × size 的二维整数数组 maze,所有元素初始化为 1(表示墙)。创建一个同样大小的布尔数组 visited,用于追踪在递归回溯过程中已访问的单元格。这种初始化方式确保了迷宫最初是完全被墙包围的,然后通过递归回溯算法逐步打通通路。
2. 递归回溯生成迷宫
fun carvePath(x: Int, y: Int) {
visited[x][y] = true
maze[x][y] = 0
val directions = listOf(
Pair(-2, 0), // 上
Pair(0, 2), // 右
Pair(2, 0), // 下
Pair(0, -2) // 左
)
val shuffled = directions.shuffled()
for ((dx, dy) in shuffled) {
val nx = x + dx
val ny = y + dy
if (nx in 0 until size && ny in 0 until size && !visited[nx][ny]) {
maze[x + dx / 2][y + dy / 2] = 0 // 打通中间的墙
carvePath(nx, ny)
}
}
}
这段代码实现了递归回溯算法来生成迷宫。首先标记当前单元格为已访问,并将其设置为通路(0)。定义四个方向的移动向量,每次移动 2 个单位以保留墙。使用 shuffled() 随机打乱方向顺序,确保生成的迷宫具有随机性。对于每个方向,检查目标单元格是否在边界内且未被访问。如果满足条件,打通当前单元格和目标单元格之间的中间墙,然后递归地对目标单元格调用 carvePath()。这个算法生成的迷宫具有完美的连通性,即每个单元格都可以从任何其他单元格到达。
3. 迷宫统计
val totalCells = size * size
val wallCount = maze.sumOf { row -> row.count { it == 1 } }
val pathCount = totalCells - wallCount
val wallPercent = (wallCount * 100) / totalCells
val pathPercent = (pathCount * 100) / totalCells
这段代码计算迷宫的统计信息。计算总格子数为 size × size。使用 sumOf() 和 count() 统计所有值为 1 的单元格(墙)的数量。计算通路数为总格子数减去墙数。计算墙和通路的百分比。这些统计数据可以用来评估迷宫的难度和结构特性。
4. BFS 路径求解
fun solveMaze(): List<Pair<Int, Int>>? {
val queue = mutableListOf<Pair<Int, Int>>()
val parent = mutableMapOf<Pair<Int, Int>, Pair<Int, Int>?>()
val visited2 = Array(size) { BooleanArray(size) { false } }
queue.add(Pair(0, 0))
visited2[0][0] = true
parent[Pair(0, 0)] = null
while (queue.isNotEmpty()) {
val (x, y) = queue.removeAt(0)
if (x == size - 1 && y == size - 1) {
// 回溯路径
val path = mutableListOf<Pair<Int, Int>>()
var current: Pair<Int, Int>? = Pair(x, y)
while (current != null) {
path.add(0, current)
current = parent[current]
}
return path
}
// 探索四个方向
for ((dx, dy) in directions) {
val nx = x + dx
val ny = y + dy
if (nx in 0 until size && ny in 0 until size &&
!visited2[nx][ny] && maze[nx][ny] == 0) {
visited2[nx][ny] = true
parent[Pair(nx, ny)] = Pair(x, y)
queue.add(Pair(nx, ny))
}
}
}
return null
}
这段代码实现了广度优先搜索(BFS)算法来求解迷宫。创建一个队列用于存储待探索的单元格,创建一个 Map 用于存储每个单元格的父节点(用于路径回溯),创建一个布尔数组追踪已访问的单元格。初始化起点 (0, 0) 到队列中。使用 while 循环处理队列中的每个单元格。当到达终点 (size-1, size-1) 时,通过父节点 Map 回溯路径。对于每个单元格,探索四个方向的相邻单元格。如果相邻单元格在边界内、未被访问且是通路,则标记为已访问、记录父节点、添加到队列。如果队列为空且未找到终点,则返回 null(迷宫无解)。BFS 算法保证找到的是最短路径。
5. ASCII 可视化
val mazeStr = maze.mapIndexed { i, row ->
" " + row.mapIndexed { j, cell ->
when {
i == 0 && j == 0 -> "S" // 起点
i == size - 1 && j == size - 1 -> "E" // 终点
cell == 1 -> "█" // 墙
else -> " " // 通路
}
}.joinToString("")
}.joinToString("\n")
这段代码将迷宫转换为 ASCII 字符串用于显示。使用 mapIndexed() 遍历迷宫的每一行和每一列。对于每个单元格,使用 when 表达式进行条件判断。起点 (0, 0) 显示为 “S”。终点 (size-1, size-1) 显示为 “E”。墙(值为 1)显示为 “█” 符号。通路(值为 0)显示为空格。使用 joinToString() 将每行的字符连接成字符串,然后将所有行用换行符连接。这种可视化方式使得迷宫结构清晰易读。
实战案例
案例:完整的迷宫生成和求解器
Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun mazeGenerator(inputSize: String = "5"): String {
// 解析输入
val size = inputSize.trim().toIntOrNull() ?: 5
if (size < 3) {
return "❌ 错误: 迷宫大小必须至少为 3"
}
if (size > 10) {
return "❌ 错误: 迷宫过大"
}
// 1. 创建迷宫网格
val maze = Array(size) { IntArray(size) { 1 } }
val visited = Array(size) { BooleanArray(size) { false } }
// 2. 生成迷宫
fun carvePath(x: Int, y: Int) {
visited[x][y] = true
maze[x][y] = 0
val directions = listOf(
Pair(-2, 0), Pair(0, 2), Pair(2, 0), Pair(0, -2)
)
for ((dx, dy) in directions.shuffled()) {
val nx = x + dx
val ny = y + dy
if (nx in 0 until size && ny in 0 until size && !visited[nx][ny]) {
maze[x + dx / 2][y + dy / 2] = 0
carvePath(nx, ny)
}
}
}
carvePath(0, 0)
maze[0][0] = 0
maze[size - 1][size - 1] = 0
// 3. 统计信息
val totalCells = size * size
val wallCount = maze.sumOf { row -> row.count { it == 1 } }
val pathCount = totalCells - wallCount
// 4. 求解迷宫
fun solveMaze(): List<Pair<Int, Int>>? {
val queue = mutableListOf<Pair<Int, Int>>()
val parent = mutableMapOf<Pair<Int, Int>, Pair<Int, Int>?>()
val visited2 = Array(size) { BooleanArray(size) { false } }
queue.add(Pair(0, 0))
visited2[0][0] = true
parent[Pair(0, 0)] = null
while (queue.isNotEmpty()) {
val (x, y) = queue.removeAt(0)
if (x == size - 1 && y == size - 1) {
val path = mutableListOf<Pair<Int, Int>>()
var current: Pair<Int, Int>? = Pair(x, y)
while (current != null) {
path.add(0, current)
current = parent[current]
}
return path
}
for ((dx, dy) in listOf(Pair(-1, 0), Pair(0, 1), Pair(1, 0), Pair(0, -1))) {
val nx = x + dx
val ny = y + dy
if (nx in 0 until size && ny in 0 until size &&
!visited2[nx][ny] && maze[nx][ny] == 0) {
visited2[nx][ny] = true
parent[Pair(nx, ny)] = Pair(x, y)
queue.add(Pair(nx, ny))
}
}
}
return null
}
val solution = solveMaze()
val solutionLength = solution?.size ?: 0
// 5. 生成输出
return "🎮 迷宫生成和求解器\n" +
"━━━━━━━━━━━━━━━━━━━━━\n" +
"1️⃣ 迷宫信息:\n" +
" 大小: ${size}×${size}\n" +
" 总格子数: $totalCells\n" +
" 墙数量: $wallCount\n" +
" 通路数: $pathCount\n\n" +
"2️⃣ 求解信息:\n" +
" 是否可解: ${if (solution != null) "✅ 是" else "❌ 否"}\n" +
" 路径长度: $solutionLength 步\n\n" +
"✅ 生成完成!"
}
这是迷宫生成和求解的核心实现函数,展示了完整的迷宫处理算法。函数使用 @OptIn(ExperimentalJsExport::class) 和 @JsExport 注解,使其可以被编译成 JavaScript 并在 ArkTS 中调用。函数首先解析输入字符串,获取迷宫大小,并验证大小在 3 到 10 之间。创建迷宫网格和访问追踪数组,初始化所有单元格为墙。定义 carvePath() 内部函数实现递归回溯算法生成迷宫。调用 carvePath(0, 0) 从起点开始生成迷宫,然后确保起点和终点都是通路。计算迷宫的统计信息,包括总格子数、墙数和通路数。定义 solveMaze() 内部函数实现 BFS 算法求解迷宫。获取求解结果和路径长度。最后使用字符串模板格式化输出完整的分析报告,包括迷宫信息和求解信息。这个函数展示了如何将复杂的迷宫算法转换为用户友好的输出。
ArkTS 调用代码(带输入框)
import { mazeGenerator } from './hellokjs';
@Entry
@Component
struct Index {
@State message: string = '加载中...';
@State results: string[] = [];
@State caseTitle: string = '迷宫生成和求解器';
@State inputText: string = '5';
aboutToAppear(): void {
this.loadResults();
}
loadResults(): void {
try {
const results: string[] = [];
const algorithmResult: string = mazeGenerator(this.inputText);
results.push(algorithmResult);
this.results = results;
this.message = '✓ 生成完成';
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.message = `✗ 错误: ${errorMessage}`;
}
}
这段 ArkTS 代码是鸿蒙应用的用户界面实现,展示了如何在 ArkTS 中导入和调用编译后的 Kotlin 函数。首先使用 import 语句从编译后的 JavaScript 模块中导入 mazeGenerator 函数。使用 @State 装饰器定义四个响应式状态变量:message 用于显示状态信息,results 存储迷宫生成结果,caseTitle 存储案例标题,inputText 存储用户输入的迷宫大小。aboutToAppear() 生命周期函数在组件加载时自动调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 编译的 JavaScript 函数 mazeGenerator(),将用户输入的迷宫大小传入,获取迷宫生成和求解的结果,然后更新 results 和 message。使用 try-catch 块捕获异常,如果发生错误则显示错误信息。这个结构实现了清晰的前后端职责划分:ArkTS 只负责收集输入和展示结果,所有迷宫生成和求解逻辑都集中在 Kotlin 中。
build() {
Column() {
// 顶部标题栏
Row() {
Text(‘KMP 鸿蒙跨端’)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Spacer()
Text(‘Kotlin 案例’)
.fontSize(14)
.fontColor(Color.White)
}
.width(‘100%’)
.height(50)
.backgroundColor(‘#3b82f6’)
.padding({ left: 20, right: 20 })
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.SpaceBetween)
// 案例标题
Column() {
Text(this.caseTitle)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1f2937')
Text(this.message)
.fontSize(13)
.fontColor('#6b7280')
.margin({ top: 5 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 20, bottom: 15 })
.alignItems(HorizontalAlign.Start)
// 输入框区域
Column() {
Text('输入迷宫大小 (3-10):')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#1f2937')
.margin({ bottom: 8 })
TextInput({ placeholder: '输入迷宫的大小...', text: this.inputText })
.width('100%')
.height(60)
.padding(12)
.border({ width: 1, color: '#d1d5db' })
.borderRadius(6)
.onChange((value: string) => {
this.inputText = value
})
Button('生成')
.width('100%')
.height(40)
.margin({ top: 12 })
.backgroundColor('#3b82f6')
.fontColor(Color.White)
.onClick(() => {
this.loadResults()
})
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 16 })
// 结果显示区域
Scroll() {
Column() {
ForEach(this.results, (result: string) => {
Column() {
Text(result)
.fontSize(13)
.fontFamily('monospace')
.fontColor('#374151')
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.border({ width: 1, color: '#e5e7eb' })
.borderRadius(8)
.margin({ bottom: 12 })
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
.width('100%')
// 底部按钮区域
Row() {
Button('示例 (5)')
.width('48%')
.height(44)
.backgroundColor('#10b981')
.fontColor(Color.White)
.fontSize(14)
.onClick(() => {
this.inputText = '5'
this.loadResults()
})
Button('清空')
.width('48%')
.height(44)
.backgroundColor('#6b7280')
.fontColor(Color.White)
.fontSize(14)
.onClick(() => {
this.inputText = ''
this.results = []
})
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#f9fafb')
}
}
---
## 编译过程详解
### Kotlin 到 JavaScript 的转换
| Kotlin 特性 | JavaScript 等价物 |
|-----------|-----------------|
| Array | 数组 [] |
| IntArray | 嵌套数组 |
| BooleanArray | 嵌套布尔数组 |
| shuffled() | 数组排序 |
| mutableListOf | 数组 [] |
| Pair | 对象或数组 |
### 关键转换点
1. **多维数组**:转换为嵌套数组
2. **集合操作**:转换为数组方法
3. **递归算法**:直接转换为 JavaScript 递归
4. **BFS 搜索**:转换为队列实现
---
## 工具扩展
### 扩展 1:添加 DFS 求解
```kotlin
fun solveMazeDFS(): List<Pair<Int, Int>>? {
val visited = Array(size) { BooleanArray(size) { false } }
val path = mutableListOf<Pair<Int, Int>>()
fun dfs(x: Int, y: Int): Boolean {
if (x == size - 1 && y == size - 1) {
path.add(Pair(x, y))
return true
}
if (visited[x][y] || maze[x][y] == 1) return false
visited[x][y] = true
path.add(Pair(x, y))
for ((dx, dy) in directions) {
if (dfs(x + dx, y + dy)) return true
}
path.removeAt(path.size - 1)
return false
}
return if (dfs(0, 0)) path else null
}
这段代码实现了深度优先搜索(DFS)算法来求解迷宫。创建一个访问追踪数组和一个路径列表。定义 dfs() 递归函数用于深度优先搜索。当到达终点时,将其添加到路径并返回 true。如果当前单元格已访问或是墙,则返回 false。标记当前单元格为已访问,将其添加到路径。递归地探索四个方向,如果任何方向找到解决方案则返回 true。如果所有方向都失败,则从路径中移除当前单元格(回溯)并返回 false。与 BFS 不同,DFS 找到的不一定是最短路径,但代码更简洁。这个扩展展示了如何实现不同的搜索算法。
扩展 2:添加迷宫难度评分
fun calculateDifficulty(): Int {
val wallRatio = (wallCount * 100) / totalCells
val pathLength = solution?.size ?: 0
return when {
wallRatio > 70 && pathLength > size * 2 -> 5 // 困难
wallRatio > 60 && pathLength > size -> 4 // 中等偏难
wallRatio > 50 -> 3 // 中等
wallRatio > 40 -> 2 // 简单
else -> 1 // 非常简单
}
}
这段代码实现了迷宫难度评分函数。计算墙的比例,即墙的数量占总格子数的百分比。获取求解路径的长度。使用 when 表达式根据墙比例和路径长度进行难度评分。墙比例越高、路径越长,迷宫难度越高。返回 1 到 5 的难度等级。这个评分函数可以帮助用户了解迷宫的复杂程度,从而选择合适的难度。
扩展 3:添加多个出口
fun generateMultipleExits(exitCount: Int): List<Pair<Int, Int>> {
val exits = mutableListOf<Pair<Int, Int>>()
exits.add(Pair(size - 1, size - 1))
for (i in 1 until exitCount) {
val x = (Math.random() * size).toInt()
val y = (Math.random() * size).toInt()
if (maze[x][y] == 0) {
exits.add(Pair(x, y))
}
}
return exits
}
这段代码实现了生成多个出口的功能。创建一个出口列表,首先添加默认的终点出口 (size-1, size-1)。循环生成指定数量的额外出口。对于每个额外出口,随机选择一个坐标。检查该坐标是否是通路(值为 0),如果是则添加到出口列表。这个功能允许迷宫有多个可能的终点,增加了迷宫的复杂性和趣味性。
扩展 4:添加迷宫导出
fun exportMazeAsString(): String {
return maze.joinToString("\n") { row ->
row.joinToString("") { if (it == 1) "1" else "0" }
}
}
这段代码实现了迷宫导出功能,将迷宫转换为字符串格式。使用 joinToString() 遍历迷宫的每一行。对于每一行,再次使用 joinToString() 将每个单元格转换为字符串。如果单元格值为 1(墙),则转换为 “1”;如果为 0(通路),则转换为 “0”。使用换行符连接所有行。这种格式便于保存、传输或在其他系统中导入迷宫数据。
最佳实践
1. 使用递归回溯生成迷宫
// ✅ 好:递归回溯
fun carvePath(x: Int, y: Int) {
visited[x][y] = true
maze[x][y] = 0
for ((dx, dy) in directions.shuffled()) {
// ...
}
}
// ❌ 不好:简单随机
val maze = Array(size) { IntArray(size) { if (Math.random() > 0.5) 1 else 0 } }
这段代码对比了两种迷宫生成方法。第一种方法使用递归回溯算法,这是生成完美迷宫的标准方法。递归回溯确保迷宫的每个单元格都可以从任何其他单元格到达,生成的迷宫具有良好的连通性和复杂性。第二种方法使用简单随机,随机决定每个单元格是墙还是通路。这种方法会产生不连通的迷宫,可能有多个独立的区域,不适合作为真正的迷宫。递归回溯是生成高质量迷宫的最佳实践。
2. 使用 BFS 找最短路径
// ✅ 好:BFS 找最短路径
fun solveMaze(): List<Pair<Int, Int>>? {
val queue = mutableListOf<Pair<Int, Int>>()
// ...
}
// ❌ 不好:DFS 可能不是最短
fun solveMazeDFS(): List<Pair<Int, Int>>? {
// ...
}
这段代码对比了两种迷宫求解方法。第一种方法使用 BFS(广度优先搜索),这是找最短路径的标准方法。BFS 按层级探索,保证找到的第一条路径是最短的。第二种方法使用 DFS(深度优先搜索),它会深入探索一条路径直到死胡同,然后回溯。DFS 找到的路径可能很长,不一定是最短的。对于迷宫求解,BFS 是最佳实践,因为用户通常希望找到最短的出路。
3. 检查边界条件
// ✅ 好:检查大小范围
if (size < 3 || size > 10) {
return "错误: 大小必须在 3-10 之间"
}
// ❌ 不好:不检查
val maze = Array(size) { IntArray(size) { 1 } }
这段代码对比了有无边界检查的差异。第一种方法在创建迷宫前检查大小是否在有效范围内(3-10)。如果大小无效,则返回错误信息。这样可以防止创建过小(无法形成有意义的迷宫)或过大(导致性能问题)的迷宫。第二种方法直接创建迷宫而不检查,可能导致异常或性能问题。边界检查是健壮代码的必要部分。
4. 使用合适的数据结构
// ✅ 好:使用 Pair 表示坐标
val path = mutableListOf<Pair<Int, Int>>()
// ❌ 不好:使用数组
val path = mutableListOf<IntArray>()
这段代码对比了两种表示坐标的方式。第一种方法使用 Pair<Int, Int> 表示坐标,这是 Kotlin 标准库提供的数据结构,语义清晰,易于理解和使用。可以直接访问 pair.first 和 pair.second 获取 x 和 y 坐标。第二种方法使用 IntArray 表示坐标,虽然也能工作,但语义不清晰,需要通过索引访问,容易出错。使用合适的数据结构可以提高代码的可读性和可维护性。
常见问题
Q1: 为什么使用递归回溯而不是其他算法?
A: 递归回溯能生成完美迷宫(每个格子都可达),且算法简单高效。
Q2: BFS 和 DFS 的区别是什么?
A:
- BFS:找最短路径,使用队列
- DFS:找任意路径,使用栈或递归
Q3: 迷宫大小为什么限制在 10×10?
A: 为了性能考虑。更大的迷宫会导致计算时间过长。
Q4: 如何优化迷宫生成性能?
A:
- 使用迭代而不是递归
- 减少随机数生成
- 使用位操作代替数组
Q5: 如何处理无解的迷宫?
A: 使用 BFS 搜索,如果队列为空且未找到出口,则迷宫无解。
总结
关键要点
- ✅ 使用递归回溯生成迷宫
- ✅ 使用 BFS 找最短路径
- ✅ 检查边界条件和特殊情况
- ✅ 使用合适的数据结构
- ✅ KMP 能无缝编译到 JavaScript
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)