boxmoe_header_banner_img

Hello! 欢迎来到悠悠畅享网!

文章导读

Java双向搜索路径算法实现与常见错误解析


avatar
作者 2025年10月10日 9

Java双向搜索路径算法实现与常见错误解析

本文深入探讨了Java中双向路径搜索算法的实现,旨在通过两个方向同时搜索来提高效率。我们将分析一个常见的实现错误,即使用单个父子映射来记录双向路径,并提供一个修正后的策略,包括使用独立的父节点映射和正确的路径重构方法,以确保算法的正确性和完整性。

双向搜索算法概述

双向搜索(bidirectional search)是一种图搜索算法,旨在寻找两个节点(起点和终点)之间的最短路径。与传统的单向搜索(如bfs或dfs)不同,双向搜索同时从起点和终点开始进行搜索,当两个搜索前沿相遇时,算法终止。这种方法通常可以显著减少需要探索的节点数量,因为搜索空间以两个方向的“半球”形式扩展,其交点通常比单个“全球”的面积小得多,从而提高搜索效率。

其核心思想是:

  1. 从起点S开始一个前向搜索(Forward Search)。
  2. 从终点T开始一个后向搜索(Backward Search)。
  3. 当两个搜索过程在某个节点M相遇时,即M同时被前向搜索和后向搜索访问到,则找到了一条从S到T的路径。这条路径由S到M的前向路径段和M到T的后向路径段(反向)组成。

初始实现的问题分析

在提供的java实现中,尝试使用双向搜索来构建路径。代码片段如下:

public BidirectionalSearch buildSearchTree(Vertex start, Vertex end) {     // ... 参数校验 ...      searchTreeParentByChild.clear(); // 单个map      Queue<Vertex> unvisitedVertexQueueFromStart = new ArrayDeque<>();     Queue<Vertex> unvisitedVertexQueueFromEnd = new ArrayDeque<>();      unvisitedVertexQueueFromStart.add(start);     unvisitedVertexQueueFromEnd.add(end);      searchTreeParentByChild.put(start, null); // 前向搜索的起点     while (!unvisitedVertexQueueFromStart.isEmpty() && !unvisitedVertexQueueFromEnd.isEmpty()) {         // 前向搜索部分         var curStart = unvisitedVertexQueueFromStart.poll();         for (var e : curStart.edges()) {             if (!searchTreeParentByChild.containsKey(e.to())) {                 searchTreeParentByChild.put(e.to(), curStart); // 记录前向路径:e.to()的父节点是curStart                 unvisitedVertexQueueFromStart.add(e.to());             }         }         var intersection = curStart.edges().stream()                 .map(Edge::to)                 .filter(unvisitedVertexQueueFromEnd::contains) // 检查交集                 .findAny();         if (intersection.isPresent())             return this;          // 后向搜索部分         var curEnd = unvisitedVertexQueueFromEnd.poll();         for (var e : curEnd.edges()) {             if (!searchTreeParentByChild.containsValue(e.to())) { // 错误:这里应该是containsKey                 searchTreeParentByChild.put(curEnd, e.to()); // 错误:记录后向路径的方式                 unvisitedVertexQueueFromEnd.add(e.to());             }         }         intersection = curEnd.edges().stream()                 .map(Edge::to)                 .filter(unvisitedVertexQueueFromStart::contains) // 检查交集                 .findAny();         if (intersection.isPresent())             return this;     }     return this; }

该实现存在两个主要问题:

  1. 单一父子映射的滥用: searchTreeParentByChild 被用于同时记录前向搜索和后向搜索的父子关系。

    立即学习Java免费学习笔记(深入)”;

    • 前向搜索 searchTreeParentByChild.put(e.to(), curStart); 记录的是 子节点 -> 父节点 的关系(从start到e.to())。
    • 后向搜索 searchTreeParentByChild.put(curEnd, e.to()); 记录的是 curEnd -> e.to()。如果e.to()是curEnd的邻居,且我们希望构建从end到curEnd的路径,那么e.to()应该是curEnd的父节点。但这里的键值对是curEnd作为键,e.to()作为值,这实际上是在说curEnd是e.to()的父节点,与前向搜索的语义相同,但方向相反。更严重的是,它会覆盖或混淆前向搜索已经记录的父子关系,导致路径无法正确重构。
    • searchTreeParentByChild.containsValue(e.to()) 这里的检查也是错误的,应该检查containsKey(e.to())来判断e.to()是否已经被后向搜索访问过。
  2. 路径重构缺失: 即使找到了交点,当前代码也只是简单地 return this;,并没有提供任何机制来从 searchTreeParentByChild 中提取完整的路径。由于映射被混用,即便尝试提取,也只会得到不完整的或错误的结果。

    Java双向搜索路径算法实现与常见错误解析

    纳米搜索

    纳米搜索:360推出的新一代AI搜索引擎

    Java双向搜索路径算法实现与常见错误解析30

    查看详情 Java双向搜索路径算法实现与常见错误解析

修正后的双向搜索实现策略

为了正确实现双向搜索并重构完整路径,我们需要:

  1. 使用两个独立的父节点映射:

    • Map<Vertex, Vertex> parentFromStart; 用于记录从start出发的前向搜索路径中,每个节点的前一个节点。
    • Map<Vertex, Vertex> parentFromEnd; 用于记录从end出发的后向搜索路径中,每个节点的前一个节点。
  2. 正确的交集判断: 在任一搜索步骤中,如果发现当前访问到的节点已经被另一个方向的搜索访问过,则找到了交点。

  3. 路径重构: 一旦找到交点,结合两个父节点映射来构建完整路径。

示例代码:修正后的 buildSearchTree 方法

 import java.util.*;  // 假设 Vertex 和 Edge 类已定义如下: class Vertex {     String name;     List<Edge> edges; // 出边列表,对于无向图,通常双向添加     // 构造函数、equals、hashCode、toString 等      public Vertex(String name) {         this.name = name;         this.edges = new ArrayList<>();     }      public List<Edge> edges() {         return edges;     }      public void addEdge(Edge edge) {         this.edges.add(edge);     }      @Override     public boolean equals(Object o) {         if (this == o) return true;         if (o == null || getClass() != o.getClass()) return false;         Vertex vertex = (Vertex) o;         return Objects.equals(name, vertex.name);     }      @Override     public int hashCode() {         return Objects.hash(name);     }      @Override     public String toString() {         return name;     } }  class Edge {     Vertex from;     Vertex to;     int weight;     // 构造函数、getter等      public Edge(Vertex from, Vertex to, int weight) {         this.from = from;         this.to = to;         this.weight = weight;     }      public Vertex from() {         return from;     }      public Vertex to() {         return to;     }      public int weight() {         return weight;     } }  // 假设 Graph 类已定义 class Graph {     Set<Vertex> vertices;     // 构造函数、添加节点、添加边等      public Graph() {         this.vertices = new HashSet<>();     }      public void addVertex(Vertex v) {         vertices.add(v);     }      public Set<Vertex> vertices() {         return vertices;     } }  public class BidirectionalSearcher {      private final Graph graph;      public BidirectionalSearcher(Graph graph) {         this.graph = graph;     }      /**      * 执行双向搜索并返回从起点到终点的路径。      *      * @param start 路径起点      * @param end   路径终点      * @return 包含路径上所有顶点的列表,如果不存在路径则返回空列表。      * @throws IllegalArgumentException 如果起点或终点不在图中。      */     public List<Vertex> findPath(Vertex start, Vertex end) {         if (!graph.vertices().containsAll(List.of(start, end))) {             throw new IllegalArgumentException("起点或终点不在图中。");         }         if (start.equals(end)) {             return List.of(start); // 起点和终点相同,路径就是自身         }          // 记录前向搜索的父节点:child -> parent         Map<Vertex, Vertex> parentFromStart = new HashMap<>();         // 记录后向搜索的父节点:child -> parent         Map<Vertex, Vertex> parentFromEnd = new HashMap<>();          // 前向搜索队列         Queue<Vertex> queueFromStart = new ArrayDeque<>();         // 后向搜索队列         Queue<Vertex> queueFromEnd = new ArrayDeque<>();          // 初始化前向搜索         queueFromStart.add(start);         parentFromStart.put(start, null); // 起点没有父节点          // 初始化后向搜索         queueFromEnd.add(end);         parentFromEnd.put(end, null);     // 终点没有父节点          Vertex meetingPoint = null; // 记录相遇点          while (!queueFromStart.isEmpty() && !queueFromEnd.isEmpty() && meetingPoint == null) {             // 执行前向搜索一步             meetingPoint = searchStep(queueFromStart, parentFromStart, parentFromEnd);             if (meetingPoint != null) break;              // 执行后向搜索一步             meetingPoint = searchStep(queueFromEnd, parentFromEnd, parentFromStart);             if (meetingPoint != null) break;         }          if (meetingPoint == null) {             return List.of(); // 未找到路径         }          // 重构路径         return reconstructPath(start, end, meetingPoint, parentFromStart, parentFromEnd);     }      /**      * 单步搜索逻辑,用于前向或后向搜索。      *      * @param currentQueue 当前搜索方向的队列      * @param currentParents 当前搜索方向的父节点映射 (child -> parent)      * @param otherParents 另一个搜索方向的父节点映射 (child -> parent)      * @return 如果找到交点则返回交点Vertex,否则返回null      */     private Vertex searchStep(Queue<Vertex> currentQueue,                               Map<Vertex, Vertex> currentParents,                               Map<Vertex, Vertex> otherParents) {         if (currentQueue.isEmpty()) {             return null;         }          Vertex current = currentQueue.poll();          // 遍历当前节点的邻居         for (Edge edge : current.edges()) {             Vertex neighbor = edge.to(); // 假设边是 from -> to              // 如果邻居未被当前搜索方向访问过             if (!currentParents.containsKey(neighbor)) {                 currentParents.put(neighbor, current); // 记录父节点                 currentQueue.add(neighbor);                  // 检查是否与另一个搜索方向相遇                 if (otherParents.containsKey(neighbor)) {                     return neighbor; // 找到交点                 }             }         }         return null;     }      /**      * 从两个父节点映射和交点重构完整路径。      *      * @param start 起点      * @param end 终点      * @param meetingPoint 相遇点      * @param parentFromStart 前向搜索的父节点映射      * @param parentFromEnd 后向搜索的父节点映射      * @return 完整的路径列表      */     private List<Vertex> reconstructPath(Vertex start, Vertex end, Vertex meetingPoint,                                          Map<Vertex, Vertex> parentFromStart,                                          Map<Vertex, Vertex> parentFromEnd) {         List<Vertex> path = new LinkedList<>(); // 使用 LinkedList 便于在头部



评论(已关闭)

评论已关闭

text=ZqhQzanResources