
1) 【一句话结论】
A*算法通过结合实际代价(g)与启发式估计(h)的评估函数f(n)=g(n)+h(n),高效搜索最短路径,适用于游戏路径规划,时间复杂度O(E+V log V),空间复杂度O(E+V),可通过剪枝、优先队列优化提升实时性。
2) 【原理/概念讲解】
老师口吻:A*算法的核心是“评估函数”f(n)=g(n)+h(n),其中g(n)是节点n到起点的已知实际代价(已走过的路径成本),h(n)是节点n到目标节点的启发式估计(对剩余路径的预测成本)。类比:规划从家到公司的路线,g是你已经走了多少公里(实际成本),h是你估计剩下的距离(启发式),总成本最低的路线优先探索,避免盲目搜索所有可能的路径。
3) 【对比与适用场景】
| 算法 | 定义 | 特性 | 使用场景 | 注意点 |
|---|---|---|---|---|
| Dijkstra | 计算所有节点到起点的最短路径 | 不考虑启发式,优先队列按g(n)排序 | 需要精确最短路径,无启发式需求 | 时间复杂度O(E log V) |
| A* | 结合g(n)和h(n)评估节点 | 启发式优化,优先队列按f(n)排序 | 需要高效搜索,有启发式信息 | h(n)需满足可采纳性(h(n)≤实际最小代价) |
| 贪心搜索 | 仅考虑当前节点的h(n) | 只选当前最优,可能陷入局部最优 | 实时性要求高,但路径可能非最优 | 无回溯机制 |
4) 【示例】
type Node struct {
x, y int
g int // 实际代价
h int // 启发式估计
f int // f = g + h
parent *Node // 父节点,用于回溯路径
}
type PriorityQueue []*Node
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].f < pq[j].f }
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *PriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*Node)
item.index = n
(*pq)[n] = item
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil
item.index = -1
*pq = old[0 : n-1]
return item
}
func AStar(start, goal *Node, grid [][]int) ([]*Node, int) {
openSet := make(PriorityQueue, 0)
closedSet := make(map[string]bool)
start.g = 0
start.h = heuristic(start, goal)
start.f = start.g + start.h
openSet = append(openSet, start)
for len(openSet) > 0 {
current := openSet[0]
if current.x == goal.x && current.y == goal.y {
path := make([]*Node, 0)
for current != nil {
path = append(path, current)
current = current.parent
}
reverse(path)
return path, current.g
}
openSet = openSet[1:]
closedSet[fmt.Sprintf("%d,%d", current.x, current.y)] = true
for _, neighbor := range getNeighbors(current, grid) {
if closedSet[fmt.Sprintf("%d,%d", neighbor.x, neighbor.y)] {
continue
}
tentativeG := current.g + 1
if tentativeG < neighbor.g {
neighbor.parent = current
neighbor.g = tentativeG
neighbor.h = heuristic(neighbor, goal)
neighbor.f = neighbor.g + neighbor.h
if !contains(openSet, neighbor) {
openSet = append(openSet, neighbor)
}
}
}
sort.Sort(sort.Reverse(sort.Interface)(openSet))
}
return nil, -1
}
func heuristic(a, b *Node) int {
return abs(a.x - b.x) + abs(a.y - b.y)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func getNeighbors(node *Node, grid [][]int) []*Node {
neighbors := make([]*Node, 0)
directions := [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
for _, dir := range directions {
nx, ny := node.x+dir[0], node.y+dir[1]
if nx >= 0 && nx < len(grid) && ny >= 0 && ny < len(grid[0]) && grid[nx][ny] != 1 {
neighbors = append(neighbors, &Node{x: nx, y: ny})
}
}
return neighbors
}
func contains(pq PriorityQueue, node *Node) bool {
for _, n := range pq {
if n.x == node.x && n.y == node.y {
return true
}
}
return false
}
func reverse(path []*Node) {
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
path[i], path[j] = path[j], path[i]
}
}
5) 【面试口播版答案】
“面试官您好,A*算法的核心是通过评估函数f(n)=g(n)+h(n)来优先搜索最有希望的节点,其中g是实际代价,h是启发式估计。在游戏路径规划中,我们用曼哈顿距离作为h,因为它是可采纳的。实现上,我们用优先队列(最小堆)存储待探索节点,按f值排序。时间复杂度方面,每次节点出队后,遍历其邻居节点,总复杂度是O(E+V log V),因为优先队列的插入和删除是log V级。空间复杂度是O(E+V),因为需要存储开放集和关闭集。为了适应实时性,我们可以做优化:比如剪枝,只探索必要的邻居(比如只向四个方向,不向对角线,减少邻居数);或者调整启发式函数,让h更接近实际,减少搜索范围;还有动态更新,当地图变化时,只重新计算受影响的路径,而不是全图重新搜索。”
6) 【追问清单】
7) 【常见坑/雷区】