5 X 5 方阵引出的寻路算法 之 路径遍历(完结)

本文通过5X5方阵的一笔画问题引入,探讨了遍历所有可能路线的寻路算法。算法基于图的深度遍历,使用栈和历史路线集合记录路径。在无路可走时回退并记录路线,直至遍历完所有可能路线。文章还提到了用React实现算法的可视化,并提供了部分代码和可视化效果链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  此篇文章源自对一个有趣问题的思考,在我的另一篇博文《一个有趣的 5 X 5 方阵一笔画问题》中有详细介绍。在已知其结论的情况下,作为程序员的我,还是想利用该问题当做出发点,写一个可以遍历所有“可能路线”的寻路算法,当做学习“图”相关算法的练习。如果对那个原始问题有兴趣,点击上面的文章链接,出门右转便是。

一、问题回顾

  还是要简单描述一下问题:有一个 5 X 5 的点方阵,如下图,要想用一笔画将所有的蓝色点连起来,是否有可行路线。需要满足3点要求:
  1、笔画必须水平或垂直。
  2、笔画不可以超出方阵边界之外,
  3、每个点只经过一次,且不可以经过黄色点。
  这个问题的数学证明已经在开头的文章中给出,在此不做赘述。这里只考虑它的算法实现。
image.png
  在思考算法之前,需要先对问题进行抽象分析,我们可以将上面的问题理解成下图这样 :
image.png

二、抽象分析

  由于题目已经被数学证明没有一条路线能够满足题目要求,所以算法的目的不是为了找到所谓符合要求的路线,而是遍历从出发点向网格中所有点可形成的所有路线。可以假设上面的网格是一个地图,小明同学按照地图在网格中行走,目的是找出从出发点到所有点的所有路线。小明先在地图中规定坐标,网格底边作为x轴,左侧的竖边作为y轴,那么红色五角星坐标就是(1,4)。小明从原点开始出发,刚开始时地图中所有的交叉点都被标记“未走过”标识。小明随机选择下一个点,假如依次经过(0,0)、(1,0)、(1,1)、(2,1)、(2,2)、(3,2)、(4,2)、(4,1)、(4,0)、(3,0)、(3,1),每往前走一个点,他都会在地图的相应位置标记已经走过。走到(3,1)时,由于四周的(2,1)、(3,2)、(4,1)都已经走到过,所以往前是无路可走的,只能逐步回退,回退之前小明为了下次不再重新走这个线路,他将这个线路记到了本子上,并标记本路线为历史路线,然后回退,每回退一步小明都会在离开的那个位置将“已走过”的标记涂抹掉并改为“未走过”标识,因为这样当下次从其他路线走时还可以走那个位置。当回退到(3,0)时,(3,1)位置就会由标记“已走过”改为标记“未走过”的状态,并发现临近的(2,0)还没有走过,于是不再回退,转向(2,0)。到达(2,0)时,又出现了无路可走的情况,于是记录当下的路线,然后回退并标记地图。当回退到(3,0)时,虽然会发现(3,1)是“未走过”状态,但由于如果走(3,1)的话就会发现这条路线在笔记本的历史路线中已经出现过,所以依然不能走(3,1),继续回退,按照这个原则一直走下去,直到遍历完从起点出发到达地图上所有点的所有路线,小明最终一定会回到起点。此时他笔记本中的历史路线列表,就是从起点达地图上所有点的所有路线。

三、算法分析

  考虑到算法的实现,这显然是利用了数据结构中的“图”。每一次路线尝试,都是对图的深度遍历,算法要做的事情,无非就是随机选择起点,然后不断地深度遍历新的路线。每往前走一步,就将新位置放进栈。这里要注意的是,针对每一次尝试,当走到无路可走时,并不意味着24个点全部已经遍历到了,因为根据规则,无路可走的原因可能是当下点的四周位置(下面统称为“邻居”)已经全部走过了,即它们已经在栈中了,或者是走到了边角的位置。那么此时无路可走该怎么办呢?是重新从起点开始呢,还是怎样。理论上都可行,不过考虑到效率,即让算法尽量少做重复的事情,我选择在无路可走时,将当下的路线(一个由点序列组成的数组)做记录(存入一个集合中,这个集合记录着所有历史路线),然后回退并将离开的位置从栈弹出,每次回退都将当下路线记录为历史路线,直到周围存在“没有在栈中的点”时不再回退,而是转向新的位置。此时,最近一次回退的那个历史路线决定了下次不会再走这个路线的方向。(假如连续回退了N步,那么最近一次回退的那个历史路线则是临近前N-1次回退历史路线的子集)。不断以上面的方式进行下去,由于“地图”是有限的,当所有可能路线全部遍历过时,最终一定会回到起点。此时历史路线集合列表,记录了从起点达地图上所有点的所有路线。

四、算法描述:

  1、输入:一个二维数组M[26][5]。(第一维度角标1到4分别代表上右下左四个方向,第二维度角标1到25分别代表图的每个点,一次从地图的左上到右下为1到25。数组中的每个值,比如M[20][1]=15 表示地图编号为20的点的上边是编号为15的点,M[20][2]=0表示地图编号为20的点的右方没有通路。其中M[i][0]=0,M[0][j]=0)
  2、初始化:定义栈Q = {1} 和 集合H = {} .(Q中记录了当下路径中依次走过的所有点,H记录着所有的历史路径。)
  3、重复以下动作,直到 Q = {}:
  根据Q的栈顶元素d,和邻接表M找出d的所有临近点元素,从其中随机选择一个元素,且Q中不存在该元素,若找到,将该元素放入Q中。若找不到,则将Q的副本存入H中,并将d从Q中弹出。
  4、输出:H。

五、算法实现

  到这里,对原始问题的抽象思考过程和算法的理论分析过程已经描述完毕。下面就是用实际的代码实现该算法并做到可视化。如果不考虑可视化,那么随便一个编程语言都可以很方便的实现它,我之前用java写过,将结果输出在控制台那种。不过感觉没啥意思,还是想要可视化的感觉。当时很长一段时间我都不知道如何将它可视化,直到我遇到了React,深入了解后,发现用React来完成该算法的可视化,真是再合适不过了。
  由于React是面向组件开发的模式,并且可以很容易根据状态来自动渲染页面,所以可视化部分的设计,就变成对原始“地图”的拆解和状态的定义。我设计的最基础的组件和整体样式如下图这样:

Snipaste20190720173848.jpg

  每个位置(点)都有上下左右四个方向,点与点之间,如果存在连线,则将对应方向的线条用CSS渲染成“block”,其他渲染成“none”.这个过程在定义好CSS样式后,根据数据状态,React会自动帮助界面渲染,不需要反复用js写渲染逻辑。
  最终效果请点击这个 程序链接1程序链接2 查看。以下附带部分代码。另外关于实现过程中对性能的优化,我已经做了一些努力,以后也会不断对其优化,欢迎有兴趣的朋友提出高见。

import React, {
    Component } from 'react';
import './AppDemo.css';
import Grid from './Grid';

class AppDemo extends Component {
   
    constructor(proprs) {
   
        super(proprs);
        var width = 5;
        var height = 5;
        var matrix = this.init(width,height);
        var x = 50;
        var start = Math.floor(Math.random() * width * height + 1);
        this.state = {
   
            matrix: matrix,
            start: start,
            arr: [start],
            historyPath: [],
            width: width,
            height: height,
            timeID: 0,
            speed: 5,
            random: true,
            x: x,
            time: 0
        }
    }
    init(width,height){
   
        var matrix = [[0, 1, 2, 3, 4]];
        for (var numb = 1; numb <= width * height; numb++) {
   
            var up = numb > width ? numb - width : 0;
            var right = (numb % width) !== 0 ? numb + 1 : 0;
            var down = numb <= width * (height - 1) ? numb + width : 0;
            var left = ((numb - 1) % width) !== 0 ? numb - 1 : 0;
            var arr = [numb];
            arr.push(up);
            arr.push(right);
            arr.push(down);
            arr.push(left);
            matrix.push(arr);
        }
        return matrix;
    }
    handle() {
   
        //var beginTime1=0;
        //var beginTime2=0;
        //beginTime1 = new Date().getTime();
        var nowRow = this.state.arr[this.state.arr.length - 1];//获取当下的位置编号
        var arr = this.state.arr;//路径编号
        var matrix = this.state.matrix;//矩阵存储结构
        var historyPath = this.state.historyPath;//历史路径
        if (arr.length > 0) {
   //如果路径长度>0
            var next = false;//默认找不到路径
            var ran = 1
            if (
### 方阵遍历算法的实现 方阵遍历是指按照特定规则访问矩阵中的每一个元素。常见的遍历方式包括按行/列遍历、对角线遍历以及旋转后的遍历等。 #### 1. 按行列遍历 最简单的方阵遍历方法是逐行或逐列读取矩阵中的元素。以下是基于Go语言的示例代码: ```go package main import "fmt" func traverseMatrix(matrix [][]int) { for i := 0; i < len(matrix); i++ { // 行索引 for j := 0; j < len(matrix[i]); j++ { // 列索引 fmt.Printf("%d ", matrix[i][j]) } fmt.Println() } } func main() { matrix := [][]int{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, } traverseMatrix(matrix) } ``` 上述代码实现了对方阵的逐行打印功能[^1]。 --- #### 2. 对角线遍历 对于更复杂的遍历需求,比如沿对角线方向依次访问矩阵中的元素,则可以参考以下逻辑。给定一个 `m x n` 的矩阵,可以通过交替改变遍历的方向来完成对角线遍历[^3]。 下面是Python版本的一个简单实现: ```python def findDiagonalOrder(mat): if not mat or not mat[0]: return [] m, n = len(mat), len(mat[0]) # 获取矩阵维度 result = [] # 存储最终结果 row, col = 0, 0 # 当前位置指针 direction = 1 # 移动方向 (1表示向上右移, -1表示向下左移) for _ in range(m * n): # 总共需要移动 m*n 步 result.append(mat[row][col]) # 计算下一步的位置 new_row = row + (-direction if direction == 1 else direction) new_col = col + (direction if direction == 1 else -direction) # 如果越界则调整方向并重新计算起始点 if new_row >= m or new_row < 0 or new_col >= n or new_col < 0: if direction == 1: # 上升过程中遇到边界 if col == n - 1: # 右侧边界 -> 向下走一步 row += 1 else: # 转向右侧一格 col += 1 else: # 下降过程中遇到边界 if row == m - 1: # 底部边界 -> 向右走一步 col += 1 else: # 转向下侧一格 row += 1 direction *= -1 # 改变方向 else: row, col = new_row, new_col # 更新当前位置 return result ``` 该函数能够处理任意矩形形状的输入,并返回其对角线遍历的结果。 --- #### 3. 顺时针旋转后再遍历 如果先将方阵顺时针旋转一定角度再进行遍历,那么可以在原基础上增加一层预处理操作——即执行一次旋转变换。下面展示的是针对N×N阶方阵的旋转与后续遍历过程: ```go // RotateSquareMatrix rotates a square matrix by 90 degrees clockwise. func RotateSquareMatrix(matrix [][]int) [][]int { n := len(matrix) rotated := make([][]int, n) for i := range rotated { rotated[i] = make([]int, n) } for i := 0; i < n; i++ { for j := 0; j < n; j++ { rotated[j][n-i-1] = matrix[i][j] } } return rotated } func TraverseRotatedMatrix(matrix [][]int) { rotated := RotateSquareMatrix(matrix) for _, row := range rotated { for _, val := range row { fmt.Print(val, " ") } fmt.Println() } } ``` 通过调用以上两个辅助函数即可轻松实现目标效果。 --- #### §相关问题§ 1. 如何优化大规模稀疏矩阵上的遍历效率? 2. 是否存在其他形式的有效路径用于遍历非正方形矩阵? 3. 在实际应用中,哪些场景会频繁涉及矩阵变换运算? 4. 基于邻接表而非邻接矩阵的情况下如何高效实施DFS/BFS搜索策略[^2]? 5. 给定一个三维立方体数据结构,怎样定义一种合理的遍历模式覆盖所有单元格?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大象与工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值