最佳优先搜索(Best-First Search)与广度优先搜索(Breadth-First Search)

2,678 阅读5分钟

图搜索算法在计算机科学中占有重要地位,特别是在路径规划和问题求解领域。最佳优先搜索(Best-First Search, BFS)和广度优先搜索(Breadth-First Search, BFS)是两种常用的图搜索算法。虽然它们的简称相同,但实现细节和应用场景各异。本文将详细介绍这两种算法及其变种,并提供Kotlin代码实现。

一、最佳优先搜索(Best-First Search)

最佳优先搜索是一类通过优先考虑某些节点而进行搜索的算法。它的目的是尽快找到解,通常通过某种评价函数来决定扩展节点的优先级。最佳优先搜索的两个常见变种是启发式搜索(Greedy Best-First Search)和A*算法(A-Star Search)。

启发式搜索(Greedy Best-First Search)

原理

启发式搜索是一种贪心算法,只考虑启发式函数h(n)h(n),即从当前节点到目标节点的估计代价。每次选择启发式代价最小的节点扩展。

执行步骤
  1. 初始化:

    • 将起始节点加入优先队列,代价为hh
    • 初始化已访问节点集合为空。
  2. 选择节点:

    • 从优先队列中取出hh值最小的节点作为当前节点。
  3. 检查目标:

    • 如果当前节点是目标节点,返回路径。
  4. 扩展节点:

    • 扩展当前节点的邻接节点,计算hh值,并加入优先队列。
  5. 重复步骤2-4,直到找到目标节点或优先队列为空。

Kotlin代码实现

import java.util.PriorityQueue

data class Node(val name: String, val h: Int) : Comparable<Node> {
    override fun compareTo(other: Node) = this.h.compareTo(other.h)
}

fun greedyBestFirstSearch(
    graph: Map<String, List<Pair<String, Int>>>,
    start: String,
    goal: String,
    heuristic: Map<String, Int>
): List<String> {
    val openList = PriorityQueue<Node>()
    val cameFrom = mutableMapOf<String, String>()
    
    openList.add(Node(start, heuristic[start] ?: 0))
    
    while (openList.isNotEmpty()) {
        val current = openList.poll()
        if (current.name == goal) {
            val path = mutableListOf<String>()
            var temp = goal
            while (temp != start) {
                path.add(temp)
                temp = cameFrom[temp] ?: start
            }
            path.add(start)
            return path.reversed()
        }
        
        for ((neighbor, _) in graph[current.name] ?: emptyList()) {
            val neighborNode = Node(neighbor, heuristic[neighbor] ?: 0)
            if (!cameFrom.containsKey(neighbor)) {
                cameFrom[neighbor] = current.name
                openList.add(neighborNode)
            }
        }
    }
    return emptyList()
}

A*算法(A-Star Search)

原理

A*算法是最佳优先搜索的改进版,它结合了启发式函数h(n)h(n)和从起点到当前节点的实际代价g(n)g(n)。 评价函数f(n)=g(n)+h(n)f(n) = g(n) + h(n)决定了节点的优先级。

执行步骤
  1. 初始化:

    • 将起始节点加入优先队列,f=g+hf = g + h
    • 初始化已访问节点集合为空。
  2. 选择节点:

    • 从优先队列中取出ff值最小的节点作为当前节点。
  3. 检查目标:

    • 如果当前节点是目标节点,返回路径。
  4. 扩展节点:

    • 扩展当前节点的邻接节点,计算gg值和ff值,并更新优先队列。
  5. 重复步骤2-4,直到找到目标节点或优先队列为空。

Kotlin代码实现

import java.util.PriorityQueue

data class AStarNode(val name: String, val g: Int, val h: Int) : Comparable<AStarNode> {
    val f: Int get() = g + h
    override fun compareTo(other: AStarNode) = this.f.compareTo(other.f)
}

fun aStarSearch(
    graph: Map<String, List<Pair<String, Int>>>,
    start: String,
    goal: String,
    heuristic: Map<String, Int>
): List<String> {
    val openList = PriorityQueue<AStarNode>()
    val cameFrom = mutableMapOf<String, String>()
    val gScores = mutableMapOf<String, Int>().apply { put(start, 0) }
    
    openList.add(AStarNode(start, 0, heuristic[start] ?: 0))
    
    while (openList.isNotEmpty()) {
        val current = openList.poll()
        if (current.name == goal) {
            val path = mutableListOf<String>()
            var temp = goal
            while (temp != start) {
                path.add(temp)
                temp = cameFrom[temp] ?: start
            }
            path.add(start)
            return path.reversed()
        }
        
        for ((neighbor, cost) in graph[current.name] ?: emptyList()) {
            val tentativeG = current.g + cost
            if (tentativeG < gScores.getOrDefault(neighbor, Int.MAX_VALUE)) {
                cameFrom[neighbor] = current.name
                gScores[neighbor] = tentativeG
                openList.add(AStarNode(neighbor, tentativeG, heuristic[neighbor] ?: 0))
            }
        }
    }
    return emptyList()
}

二、广度优先搜索(Breadth-First Search)

原理

广度优先搜索是一种遍历图或树的算法,它按层次逐层访问节点。BFS使用队列数据结构,确保每次访问距离起始节点最短的节点。BFS特别适用于无权图中的最短路径问题。

执行步骤
  1. 初始化:

    • 将起始节点加入队列,并标记为已访问。
  2. 出队列:

    • 从队列中取出一个节点作为当前节点。
  3. 访问邻接节点:

    • 对于当前节点的每一个未访问的邻接节点,将其加入队列并标记为已访问。
  4. 重复步骤2-3,直到队列为空。

Kotlin代码实现

import java.util.ArrayDeque

fun breadthFirstSearch(graph: Map<String, List<String>>, start: String): List<String> {
    val visited = mutableSetOf<String>()
    val queue = ArrayDeque<String>()
    val order = mutableListOf<String>()
    
    queue.add(start)
    visited.add(start)
    
    while (queue.isNotEmpty()) {
        val current = queue.removeFirst()
        order.add(current)
        
        for (neighbor in graph[current] ?: emptyList()) {
            if (neighbor !in visited) {
                visited.add(neighbor)
                queue.add(neighbor)
            }
        }
    }
    
    return order
}

三、区别及应用场景

区别

1. 搜索策略:
  • 启发式搜索:只考虑启发式代价h(n)h(n),不考虑实际代价g(n)g(n),可能不保证最优解。
  • A*算法:同时考虑g(n)g(n)h(n)h(n),能保证最优解(若h(n)h(n)是可接受的)。
  • 广度优先搜索:无偏向的遍历算法,保证在无权图中找到最短路径。
2. 数据结构:
  • 启发式搜索和A*算法:使用优先队列。
  • 广度优先搜索:使用普通队列。
3. 复杂度:
  • 启发式搜索和A*算法:时间复杂度取决于启发式函数的质量和具体实现。
  • 广度优先搜索:时间复杂度为O(V + E),空间复杂度与节点和边的数量有关。

应用场景

1. 启发式搜索:
  • 适用于启发式函数能较好估计的场景,如迷宫求解、路径规划等。
2. A*算法:
  • 广泛用于路径规划、游戏AI、导航等领域,尤其是在需要最优解的情况下。
3. 广度优先搜索:
  • 适用于无权图中的最短路径问题,如社交网络分析、网络爬虫等。

四、总结

最佳优先搜索和广度优先搜索是图搜索中的基本算法,它们各有优缺点和适用场景。启发式搜索是一种简单的贪心算法,A*算法在此基础上保证了最优解,而广度优先搜索则在无权图中有较好的性能和效果。了解和掌握这些算法的原理和实现,有助于在实际应用中选择