前言

半年前我写了一篇《十几种排序算法的可视化效果,快来看看!👀》,还是很有意思的。这篇文章中的内容还被张风捷特烈张老师收录进了FlutterUnit:《FlutterUnit 周边 | 收录排序算法可视化》。今天让我们再来做一个有关寻路算法的可视化效果吧!

效果图:

用flutter实现五种寻路算法的可视化效果,快来看看!_初始化

代码下载:<https://www.alipan.com/s/3kvCHKTgaon>

迷宫地图的绘制

迷宫地图的生成有很多很多方法,常见的有随机生成算法、递归分割算法、深度优先搜索算法等,都可以用于迷宫地图的生成。这次我们使用深度优先遍历+广度优先遍历+随机队列去制作迷宫地图,这样会让地图有更多的随机性。

定义迷宫地图的数据类及初始化方法:

定义BlockModel类:

class BlockModel{
  int? rowSize;
  int? columnSize;

  int? startX;
  int? startY;

  int? endX;
  int? endY;

  int road = 1; //路
  int wall = 0; //墙

  //路径是否被访问过
  List<List<bool>> visitedList = [];

  //迷宫地图
  List<List<int>> blockList = [];

  //寻找到的正确路线
  List<List<bool>> pathList = [];

  //方向
  List<List<int>> direction = [
    [-1, 0],
    [0, -1],
    [1, 0],
    [0, 1]
  ];
}
初始化基本的地图数据:

设置迷宫的起点和终点位置,并给所有行坐标为奇数且列坐标为奇数的区块设置为路。其余位置设置为墙。

BlockModel(int r, int c) {
  if (r % 2 == 0 || c % 2 == 0) {
    throw "地图行数和列数不能为偶数";
  }
  
  rowSize = r;
  columnSize = c;

  startX = 1;
  startY = 0;

  endX = r - 2;
  endY = c - 1;
  blockList = [];
  visitedList = [];
  pathList = [];
  
  //初始化迷宫遍历的方向(上、左、右、下)顺序(迷宫趋势)
  //随机遍历顺序,提高迷宫生成的随机性(共12种可能性)
  List.generate(direction.length, (index) {
    int random = Random().nextInt(direction.length);
    List<int> temp = direction[random];
    direction[random] = direction[index];
    direction[index] = temp;
  });

  List.generate(r, (i) {
    List<bool> tempVisited = [];
    List<int> tempMazeList = [];
    List<bool> tempPath = [];
    List.generate(c, (j) {
      //行和列为奇数都设置为路,否则设置为墙
      if (i % 2 == 1 && j % 2 == 1) {
        tempMazeList.add(1); //设置为路
      } else {
        tempMazeList.add(0); //设置为墙
      }
      //初始化访问过的区块,现在所有都没有访问过
      tempVisited.add(false);
      tempPath.add(false);
    });
    visitedList.add(tempVisited);
    blockList.add(tempMazeList);
    pathList.add(tempPath);
  });

  blockList[startX!][startY!] = 1;
  blockList[endX!][endY!] = 1;
}
地图的生成

地图的创建需要一个随机队列的帮助,那让我们先来实现一个随机队列:

借助dart的LinkedList链表结合随机数来决定插入和移除元素的位置,从而实现随机队列。

class RandomQueue {
  LinkedList<Position> _queue = LinkedList<Position>();

  RandomQueue() {
    _queue = LinkedList<Position>();
  }

  void addRandomQueue(Position position) {
    if (Random().nextInt(100) < 50) {
      _queue.addFirst(position);// 插入到队列头部
    } else {
      _queue.add(position);// 插入到队列尾部
    }
  }

  ///返回随机队列中的一个元素
  Position removeRandomQueue() {
    if (_queue.isEmpty) {
      throw "数组为空";
    } else {
      if (Random().nextInt(100) < 50) {
        Position position = _queue.first;
        _queue.remove(position);
        return position;
      } else {
        Position position = _queue.last;
        _queue.remove(position);
        return position;
      }
    }
  }

  //返回随机队列元素数量
  int getSize() {
    return _queue.length;
  }

  //判断随机队列是否为空
  bool isEmpty() {
    return _queue.isEmpty;
  }
}

创建地图:

使用随机队列去生成地图,从指定的起始位置开始,随机选择下一个要访问的位置(访问的方向在初始化BlockModel时已经随机打乱),并在访问时标记该区块已访问。通过这种方式,可以生成具有一定随机性的地图结构。

/// 创建地图并使用随机队列生成地图结构
void createMap(int startX, int startY) {
  RandomQueue randomQueue = RandomQueue(); // 创建一个随机队列实例
  Position start = Position(startX, startY); // 创建起始位置
  randomQueue.addRandomQueue(start); // 将起始位置加入随机队列

  model.visitedList[startX][startY] = true; // 标记起始位置为已访问

  // 使用随机队列生成地图
  while (randomQueue.getSize() != 0) {
    Position position = randomQueue.removeRandomQueue(); // 移除队列中的一个位置

    // 生成四个方向的新位置
    List.generate(4, (i) {
      int newX = position.x! + model.direction[i][0] * 2;
      int newY = position.y! + model.direction[i][1] * 2;

      // 检查新位置是否在地图内且未被访问过
      if (model.isInMap(newX, newY) && !model.visitedList[newX][newY]) {
        // 将新位置加入随机队列,并记录为已访问
        randomQueue
            .addRandomQueue(Position(newX, newY, prePosition: position));
        model.visitedList[newX][newY] = true;

        // 设置新位置与当前位置之间的路径或道路
        setWithRoad(position.x! + model.direction[i][0],
            position.y! + model.direction[i][1]);
      }
    });
  }

  // 把visitedList全部设置为没有访问
  model.visitedList = model.visitedList
      .map((innerList) => innerList.map((element) => false).toList())
      .toList();
}
flutter布局实现

布局非常简单,通过Column结合Row,通过两层List.generate遍历地图数据,即可渲染完布局:

Widget board() {
  mapHeight = screenWidth.floor();
  cellSize = screenWidth.floor() ~/ rowSize;

  List<Row> rowList = [];
  List.generate(model.blockList.length, (i) {
    List<Container> columnList = [];
    List.generate(model.blockList[i].length, (j) {
      columnList.add(Container(
        width: cellSize.toDouble(),
        height: cellSize.toDouble(),
        decoration: BoxDecoration(
          color: getBoxColor(i, j),
        ),
      ));
    });

    rowList.add(Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: columnList,
    ));
  });

  return SizedBox(
    width: mapHeight.toDouble(),
    height: mapHeight.toDouble(),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: rowList,
    ),
  );
}

算法的实现

辅助函数

为了让算法更好的融入我们的地图寻路需求,还需要几个辅助函数:

///设置该区块为路
void setWithRoad(int x, int y) {
  setState(() {
    model.blockList[x][y] = model.road;
  });
}

///设置该区块为正确的路径
void setModelWithPath(int x, int y, bool isPath) {
  setState(() {
    if (model.isInMap(x, y)) {
      model.pathList[x][y] = isPath;
    }
  });
}

接下来让我们正式开始算法的实现!

算法的详细细节就不过多展开了,想要学习的朋友可以自行搜索,相关资料很多

BFS(广度优先搜索)

广度优先搜索是图的一种遍历策略。因为它的思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域,因此得名。一般可以用它做什么呢?一个最直观经典的例子就是走迷宫,我们从起点开始,找出到终点的最短路程,很多最短路径算法就是基于广度优先的思想成立的。

/// 广度优先搜索(BFS)寻找路径
  Future<bool> searchPathBFS(int startX, int startY) async {
    // 创建队列并添加起始位置
    Queue<List<int>> queue = Queue();
    queue.add([startX, startY]);

    // 初始化访问标记、路径记录和父节点记录
    model.visitedList = List.generate(
        model.columnSize!, (_) => List.filled(model.rowSize!, false));
    model.pathList = List.generate(
        model.columnSize!, (_) => List.filled(model.rowSize!, false));
    List<List<List<int>>> parent = List.generate(model.columnSize!,
        (_) => List.generate(model.rowSize!, (_) => [-1, -1]));

    // 开始广度优先搜索
    while (queue.isNotEmpty) {
      List<int> current = queue.removeFirst();
      int x = current[0];
      int y = current[1];

      // 检查当前位置是否越界
      if (!model.isInMap(x, y)) {
        throw "坐标越界";
      }

      // 如果已经访问过当前位置,则继续下一个循环
      if (model.visitedList[x][y]) continue;
      model.visitedList[x][y] = true; // 标记当前位置为已访问
      setState(() {});

      // 检查是否到达终点
      if (x == model.endX && y == model.endY) {
        print("找到路径了");

        // 回溯路径并记录到 path 列表中
        List<List<int>> path = [];
        int curX = x;
        int curY = y;
        while (curX != -1 && curY != -1) {
          path.add([curX, curY]);
          List<int> prev = parent[curX][curY];
          curX = prev[0];
          curY = prev[1];
        }

        // 清空原路径记录,并设置新的路径记录为找到的路径
        model.pathList = List.generate(
            model.columnSize!, (_) => List.filled(model.rowSize!, false));
        for (var point in path) {
          setModelWithPath(point[0], point[1], true); // 设置路径标记
          await Future.delayed(const Duration(
              milliseconds: 50));
        }
        return true; // 返回找到路径的结果
      }

      // 遍历当前位置的四个方向
      for (int i = 0; i < 4; i++) {
        int newX = x + model.direction[i][0];
        int newY = y + model.direction[i][1];

        // 检查新位置是否在地图范围内,并且是可通过的道路,并且未访问过
        if (model.isInMap(newX, newY) &&
            model.blockList[newX][newY] == model.road &&
            !model.visitedList[newX][newY]) {
          queue.add([newX, newY]); // 将新位置添加到队列中
          parent[newX][newY] = [x, y]; // 记录新位置的父节点
          setState(() {}); 
          await Future.delayed(const Duration(
              milliseconds: 50)); // 等待一段时间,以便显示搜索过程(如果在 Flutter 中)
        }
      }
    }

    return false; // 如果队列为空仍未找到路径,则返回 false
  }
DFS(深度优先搜索)

DFS 是一种递归或栈(堆栈)数据结构的算法,用于图的遍历。

从一个起始节点开始,尽可能深入图的分支,直到无法继续深入,然后回溯并探索其他分支。

通过标记已访问的节点来避免重复访问。

DFS最有意思的就是回溯这一步,回溯就是在搜索尝试过程中寻找问题的解,当发现不满足条件时,就回溯,尝试别的路径。强调的是此路不通,另寻他路。先打标记,记录路径;然后下一层,回到上一层,清除标记。

/// 深度优先搜索(DFS)寻找路径
Future<bool> searchPathDFS(int startX, int startY) async {
  // 初始化访问标记、路径记录和父节点记录
  model.visitedList = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, false));
  model.pathList = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, false));
  List<List<List<int>>> parent = List.generate(model.columnSize!,
          (_) => List.generate(model.rowSize!, (_) => [-1, -1]));

  // 定义DFS递归函数
  Future<bool> dfs(int x, int y) async {
    // 检查当前位置是否越界
    if (!model.isInMap(x, y)) {
      throw "坐标越界";
    }

    // 如果已经访问过当前位置,则返回 false
    if (model.visitedList[x][y]) return false;
    model.visitedList[x][y] = true; // 标记当前位置为已访问
    setState(() {}); // 更新界面状态(如果在 Flutter 中)

    // 检查是否到达终点
    if (x == model.endX && y == model.endY) {
      print("找到路径了");

      // 回溯路径并记录到 path 列表中
      List<List<int>> path = [];
      int curX = x;
      int curY = y;
      while (curX != -1 && curY != -1) {
        path.add([curX, curY]);
        List<int> prev = parent[curX][curY];
        curX = prev[0];
        curY = prev[1];
      }

      // 清空原路径记录,并设置新的路径记录为找到的路径
      model.pathList = List.generate(
          model.columnSize!, (_) => List.filled(model.rowSize!, false));
      for (var point in path) {
        setModelWithPath(point[0], point[1], true); // 设置路径标记
        await Future.delayed(const Duration(milliseconds: 50)); 
      }
      return true; // 返回找到路径的结果
    }

    // 遍历当前位置的四个方向
    for (int i = 0; i < 4; i++) {
      int newX = x + model.direction[i][0];
      int newY = y + model.direction[i][1];

      // 检查新位置是否在地图范围内,并且是可通过的道路,并且未访问过
      if (model.isInMap(newX, newY) &&
          model.blockList[newX][newY] == model.road &&
          !model.visitedList[newX][newY]) {
        parent[newX][newY] = [x, y]; // 记录新位置的父节点
        setState(() {}); // 更新界面状态(如果在 Flutter 中)
        await Future.delayed(const Duration(milliseconds: 50)); 
        if (await dfs(newX, newY)) {
          return true; // 如果找到路径,则返回 true
        }
      }
    }

    return false; // 如果当前位置无法继续搜索,则返回 false
  }

  // 调用DFS函数并返回最终结果
  return await dfs(startX, startY);
}
AStar算法(A\*)

A\*(AStar)算法是一种静态路网中求解最短路最有效的方法,俗称A星算法。这是一种在图形平面上,有多个节点的路径,求出最低通过成本的算法。常用于游戏中的NPC的移动计算,或线上游戏的BOT的移动计算上。

  • 和Dijkstra一样,AStar能用于搜索最短路径。
  • 和BFS一样,AStar能用启发式函数引导它自己。

左图为Astar算法效果图,右图为Dijkstra算法效果图。Astar算法与Dijkstra算法的效果差不多,但Astar算法访问的节点数明显比Dijkstra算法少得多,代表其速度更快,运行时间更短。

用flutter实现五种寻路算法的可视化效果,快来看看!_路径记录_02

\

(图片来源于网络)

实现这个算法我们需要编写一个Node类:

Node 类表示了AStar搜索算法中的节点。节点在A算法中的作用是记录搜索过程中的状态信息,包括当前位置的坐标、从起始节点到当前节点的路径长度(g值)、以及从当前节点到目标节点的估计距离(启发值h)。

class Node {
  int x, y, g; // 节点的坐标和当前路径长度g值
  double h; // 启发值h
  Node? parent; // 父节点

  Node(this.x, this.y, this.g, this.h, [this.parent]);

  double get f => g + h; // 计算节点的总代价f值
}

class PriorityQueue {
  final List<Node> _queue = []; // 优先队列使用的列表

  bool get isEmpty => _queue.isEmpty; // 判断队列是否为空

  void add(Node node) {
    _queue.add(node); // 添加节点到队列
    _queue.sort((a, b) => a.f.compareTo(b.f)); // 根据f值排序,f值小的在前面
  }

  Node removeFirst() {
    return _queue.removeAt(0); // 移除并返回队列中f值最小的节点
  }
}

编写AStar算法:

节点的估值算法我们使用曼哈顿距离用于估值,曼哈顿距离是平面上两点之间的距离,定义为从一个点沿着网格状的路径到另一个点的距离,沿路径只能朝着水平或垂直方向移动,而不能斜向移动。就是横向和纵向的距离之和。

具体计算公式如下:

heuristic(x,y)=∣x−model.endX∣+∣y−model.endY∣

/// AStar算法
Future<bool> searchPathAStar(int startX, int startY) async {
  // 创建优先队列作为开放列表
  PriorityQueue openList = PriorityQueue();
  openList.add(Node(startX, startY, 0, heuristic(startX, startY)));

  // 初始化路径记录和访问标记
  model.visitedList = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, false));
  model.pathList = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, false));

  while (!openList.isEmpty) {
    // 从开放列表中移除f值最小的节点作为当前节点
    Node current = openList.removeFirst();
    int x = current.x;
    int y = current.y;

    // 检查当前节点是否超出地图范围
    if (!model.isInMap(x, y)) {
      throw "坐标越界";
    }

    // 如果当前节点已经访问过,则继续下一个节点
    if (model.visitedList[x][y]) continue;
    model.visitedList[x][y] = true;
    setState(() {});

    // 检查是否到达终点
    if (x == model.endX && y == model.endY) {
      print("找到路径了");
      // 回溯路径
      List<List<int>> path = [];
      Node? curNode = current;
      while (curNode != null) {
        path.add([curNode.x, curNode.y]);
        curNode = curNode.parent;
      }
      // 将路径标记到pathList中,并显示路径动画
      model.pathList = List.generate(
          model.columnSize!, (_) => List.filled(model.rowSize!, false));
      for (var point in path) {
        setModelWithPath(point[0], point[1], true);
        await Future.delayed(const Duration(milliseconds: 50));
      }
      return true;
    }

    // 遍历当前节点的四个方向
    for (int i = 0; i < 4; i++) {
      int newX = x + model.direction[i][0];
      int newY = y + model.direction[i][1];

      // 如果新节点在地图范围内且是可通行的空地且未被访问过
      if (model.isInMap(newX, newY) &&
          model.blockList[newX][newY] == model.road &&
          !model.visitedList[newX][newY]) {
        int g = current.g + 1; // 更新新节点的g值
        double h = heuristic(newX, newY); // 计算新节点的启发值h
        openList.add(Node(newX, newY, g, h, current)); // 将新节点加入开放列表
        setState(() {});
        await Future.delayed(const Duration(milliseconds: 50)); // 延迟显示效果
      }
    }
  }

  return false; // 开放列表为空,未找到路径,搜索失败
}

// 估价函数(曼哈顿距离)
double heuristic(int x, int y) {
  return ((x - model.endX!).abs() + (y - model.endY!).abs()).toDouble();
}
BestFS算法(最佳优先算法)

主要运用贪心算法的思想,优先搜索离终点近的点。

/// BestFS算法
Future<bool> searchPathBestFS(int startX, int startY) async {
  // 初始化优先队列、起始节点和父节点数组
  PriorityQueue priorityQueue = PriorityQueue();

  Node startNode = Node(startX, startY, 0,
      euclideanDistance(startX, startY, model.endX!, model.endY!));
  priorityQueue.add(startNode);

  // 初始化 visitedList、pathList 和 parent
  // 初始化路径记录和访问标记
  model.visitedList = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, false));
  model.pathList = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, false));
  List<List<int>> parent = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, -1));

  while (!priorityQueue.isEmpty) {
    Node currentNode = priorityQueue.removeFirst();
    int x = currentNode.x;
    int y = currentNode.y;

    // 如果当前节点已访问过,则跳过
    if (model.visitedList[x][y]) continue;
    model.visitedList[x][y] = true;
    setState(() {});

    // 检查是否到达终点
    if (x == model.endX && y == model.endY) {
      print("找到路径了");

      // 回溯路径
      List<List<int>> path = [];
      int curX = x;
      int curY = y;
      while (curX != -1 && curY != -1) {
        path.add([curX, curY]);
        int prevX = parent[curX][curY] ~/ model.rowSize!;
        int prevY = parent[curX][curY] % model.rowSize!;
        curX = prevX;
        curY = prevY;
        print("curX:$curX,curY:$curY");
        // 防止出现死循环,检查是否回溯到起点
        if (curX == startX && curY == startY) {
          break;
        }
      }
      // path = path.reversed.toList();

      // 绘制路径到界面上
      model.pathList = List.generate(
          model.columnSize!, (_) => List.filled(model.rowSize!, false));
      for (var point in path) {
        setModelWithPath(point[0], point[1], true);
        await Future.delayed(const Duration(milliseconds: 50));
      }
      return true; // 找到路径并绘制完成,返回true
    }

    // 探索当前节点的邻居节点
    for (int i = 0; i < 4; i++) {
      int newX = x + model.direction[i][0];
      int newY = y + model.direction[i][1];

      // 如果邻居节点在地图范围内且是可通行的道路且未访问过
      if (model.isInMap(newX, newY) &&
          model.blockList[newX][newY] == model.road &&
          !model.visitedList[newX][newY]) {
        Node newNode = Node(newX, newY, currentNode.g + 1,
            euclideanDistance(newX, newY, model.endX!, model.endY!));
        priorityQueue.add(newNode);
        parent[newX][newY] = x * model.rowSize! + y; // 记录父节点
        setState(() {});
        await Future.delayed(const Duration(milliseconds: 50));
      }
    }
  }

  return false; // 未找到路径,返回false
}
Dijkstra算法

最初我学习到这个算法是在计算机网络中,关于网络路由的部分。它非常适合用于找到从起始节点到所有其他节点的最短路径。不过,它仅适用于非负权重的图,因为它依赖于贪婪策略来选择当前最短路径。

/// Dijkstra算法
Future<bool> searchPathDijkstra(int startX, int startY) async {
  // 初始化优先队列、距离数组和父节点数组
  PriorityQueue priorityQueue = PriorityQueue();
  List<List<int>> distance = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, -1));
  List<List<int>> parent = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, -1));

  model.visitedList = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, false));
  model.pathList = List.generate(
      model.columnSize!, (_) => List.filled(model.rowSize!, false));
  // 将起始节点加入优先队列并初始化距离
  priorityQueue.add(Node(startX, startY, 0, 0.0));
  distance[startX][startY] = 0;

  while (!priorityQueue.isEmpty) {
    // 从优先队列中取出当前节点
    Node currentNode = priorityQueue.removeFirst();
    int x = currentNode.x;
    int y = currentNode.y;
    int currentDistance = currentNode.g;

    // 如果当前节点已访问过,则跳过
    if (model.visitedList[x][y]) continue;
    model.visitedList[x][y] = true;
    setState(() {});

    // 检查是否到达终点
    if (x == model.endX && y == model.endY) {
      print("找到路径了");

      // 回溯路径
      List<List<int>> path = [];
      int curX = x;
      int curY = y;
      while (curX != -1 && curY != -1) {
        path.add([curX, curY]);
        int prevX = parent[curX][curY] ~/ model.rowSize!;
        int prevY = parent[curX][curY] % model.rowSize!;
        curX = prevX;
        curY = prevY;
        print("curX:$curX,curY:$curY");
        // 防止出现死循环,检查是否回溯到起点
        if (curX == startX && curY == startY) {
          break;
        }
      }
      // path = path.reversed.toList();

      // 绘制路径到界面上
      model.pathList = List.generate(
          model.columnSize!, (_) => List.filled(model.rowSize!, false));
      for (var point in path) {
        setModelWithPath(point[0], point[1], true);
        await Future.delayed(const Duration(milliseconds: 50));
      }
      return true; // 找到路径并绘制完成,返回true
    }

    // 探索当前节点的邻居节点
    for (int i = 0; i < 4; i++) {
      int newX = x + model.direction[i][0];
      int newY = y + model.direction[i][1];

      // 如果邻居节点在地图范围内且是可通行的道路且未访问过
      if (model.isInMap(newX, newY) &&
          model.blockList[newX][newY] == model.road &&
          !model.visitedList[newX][newY]) {
        int newDistance = currentDistance + 1; // 更新距离

        // 如果是第一次访问或找到更短的路径,则更新距离和父节点,并加入优先队列
        if (distance[newX][newY] == -1 ||
            newDistance < distance[newX][newY]) {
          distance[newX][newY] = newDistance;
          Node newNode =
              Node(newX, newY, newDistance, 0.0); // Dijkstra没有启发式函数,h设为0
          priorityQueue.add(newNode);
          parent[newX][newY] = x * model.rowSize! + y; // 记录父节点
          setState(() {});
          await Future.delayed(const Duration(milliseconds: 50));
        }
      }
    }
  }

  return false; // 未找到路径,返回false
}

都看到这里啦,给个赞吧~

关于我

Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 ,也可以通过掘金的新的私信功能联系到我。如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章~万一哪天我进步了呢?😝