1. 算法训练营实战:从Floyd到A*的路径规划探索
最近在代码随想录算法训练营的第六十天,遇到了两道非常有意思的题目:97号题"小明逛公园"和127号题"骑士的攻击"。这两道题看似场景不同,实则都涉及图论中的经典算法应用。作为算法工程师,我发现很多实际问题都可以抽象为图论问题,而选择合适的算法往往能事半功倍。
"小明逛公园"这道题描述的是在公园地图上寻找最优游览路径的场景,这让我联想到实际生活中的导航系统。而"骑士的攻击"则是棋盘上棋子移动范围的经典问题,在游戏开发中很常见。两道题分别适合用Floyd算法和A*算法来解决,正好可以对比这两种算法的适用场景和性能特点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 97号题:公园路径规划与Floyd算法实战
2.1 问题场景建模
小明逛公园的问题可以这样描述:给定一个公园的地图,表示为带权无向图,其中节点代表景点,边代表路径,权重代表路径长度。要求找出从入口到出口,且经过若干指定景点的最短路径。
这实际上是一个典型的"必经点最短路径"问题。我们需要先理清几个关键点:
- 图的表示方式(邻接矩阵或邻接表)
- 必经点的处理策略
- 路径长度的计算方式
2.2 Floyd算法核心思想
Floyd-Warshall算法是一种计算图中所有顶点对之间最短路径的动态规划算法。它的核心思想是逐步优化路径估计:
code复制let dist be a |V| × |V| array of minimum distances initialized to ∞
for each edge (u,v)
dist[u][v] ← w(u,v) // 边的权重
for each vertex v
dist[v][v] ← 0
for k from 1 to |V|
for i from 1 to |V|
for j from 1 to |V|
if dist[i][j] > dist[i][k] + dist[k][j]
dist[i][j] ← dist[i][k] + dist[k][j]
这个三重循环的结构看似简单,却巧妙地利用了动态规划的思想。k代表"中转站",算法逐步考虑通过更多中转站来优化路径。
2.3 必经点处理的技巧
对于必须经过的景点,我们可以采用以下策略:
- 先用Floyd算法计算全图所有点对的最短距离
- 然后在这些必经点上进行排列组合,计算不同顺序下的总路径长度
- 选择总长度最小的那个顺序
这里有个优化技巧:当必经点较多时(比如超过5个),全排列会变得非常耗时。此时可以考虑使用动态规划的状态压缩技巧,或者启发式算法如遗传算法来近似求解。
2.4 代码实现与优化
以下是基于Python的实现框架:
python复制def floyd_warshall(graph):
n = len(graph)
dist = [[float('inf')] * n for _ in range(n)]
# 初始化
for i in range(n):
dist[i][i] = 0
for u in range(n):
for v, w in graph[u]:
dist[u][v] = w
# Floyd核心算法
for k in range(n):
for i in range(n):
for j in range(n):
if dist[i][j] > dist[i][k] + dist[k][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
def find_optimal_path(dist, must_visit):
from itertools import permutations
min_length = float('inf')
best_path = None
# 尝试所有必经点的排列组合
for perm in permutations(must_visit):
total = dist[0][perm[0]] # 起点到第一个必经点
for i in range(len(perm)-1):
total += dist[perm[i]][perm[i+1]]
total += dist[perm[-1]][n-1] # 最后一个必经点到终点
if total < min_length:
min_length = total
best_path = [0] + list(perm) + [n-1]
return best_path, min_length
在实际应用中,当图比较大时(节点数超过1000),Floyd算法会变得不太适用,因为它的时间复杂度是O(n^3)。这时可以考虑使用Dijkstra算法的变种,或者A*算法。
3. 127号题:棋盘游戏与A*算法应用
3.1 问题重述与分析
"骑士的攻击"问题描述的是:在一个8×8的国际象棋棋盘上,给定骑士的初始位置和目标位置,计算骑士最少需要多少步才能到达目标位置。
骑士的移动方式是独特的"日"字形移动(横向2格纵向1格,或横向1格纵向2格)。这实际上是一个典型的单源最短路径问题,但由于棋盘的特殊结构和骑士的移动方式,我们可以采用更高效的算法。
3.2 A*算法原理
A*算法是一种启发式搜索算法,结合了Dijkstra算法的完备性和贪心算法的高效性。它的核心是评估函数:
f(n) = g(n) + h(n)
其中:
- g(n)是从起点到节点n的实际代价
- h(n)是从节点n到目标的估计代价(启发式函数)
对于棋盘问题,常用的启发式函数有:
- 曼哈顿距离除以3(因为骑士每步最多缩短3单位曼哈顿距离)
- 切比雪夫距离的最大值除以2
- 欧几里得距离
3.3 启发式函数的选择
在骑士移动问题中,我发现以下启发式函数效果很好:
python复制def heuristic(a, b):
# a和b是(x,y)坐标
dx = abs(a[0] - b[0])
dy = abs(a[1] - b[1])
return (dx + dy) / 3
这个启发式函数是可接受的(不会高估实际代价),且计算简单。在实际测试中,它能让A*算法比普通BFS快3-5倍。
3.4 实现细节与优化
完整的A*算法实现需要注意以下几点:
- 优先队列的实现:Python中可以使用heapq模块
- 已访问节点的记录:可以用字典存储已访问节点及其g值
- 路径重建:需要记录每个节点的父节点
以下是关键代码片段:
python复制from heapq import heappush, heappop
def a_star(start, goal):
open_set = []
heappush(open_set, (0 + heuristic(start, goal), 0, start))
came_from = {}
g_score = {start: 0}
while open_set:
_, current_g, current = heappop(open_set)
if current == goal:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
return path[::-1]
for move in [(1,2),(2,1),(-1,2),(-2,1),(1,-2),(2,-1),(-1,-2),(-2,-1)]:
neighbor = (current[0]+move[0], current[1]+move[1])
# 检查是否在棋盘内
if 0 <= neighbor[0] < 8 and 0 <= neighbor[1] < 8:
tentative_g = current_g + 1 # 每步代价为1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + heuristic(neighbor, goal)
heappush(open_set, (f_score, tentative_g, neighbor))
return None # 无路径
在实际测试中,A算法对于8×8棋盘上的任意两点,通常能在O(1)时间内找到解,因为棋盘大小固定。但对于更大的棋盘,A的优势会更加明显。
4. 算法对比与工程实践思考
4.1 Floyd与A*的适用场景对比
通过这两道题的实践,我总结了这两种算法的适用场景:
| 特性 | Floyd算法 | A*算法 |
|---|---|---|
| 时间复杂度 | O(n^3) | O(b^d) |
| 空间复杂度 | O(n^2) | O(b^d) |
| 最佳适用场景 | 稠密图,需要所有点对最短路径 | 单源最短路径,有启发式信息 |
| 预处理 | 需要 | 不需要 |
| 动态图适应性 | 差 | 好 |
| 实现难度 | 简单 | 中等 |
在工程实践中,选择算法时需要综合考虑:
- 图的规模和稀疏程度
- 查询的频率和模式
- 是否需要实时更新图结构
- 是否有好的启发式函数可用
4.2 实际应用中的优化技巧
在实现这些算法时,我积累了一些实用技巧:
- Floyd算法的空间优化:可以原地更新矩阵,节省空间
- A*算法的启发式调整:当发现算法运行缓慢时,可以适当调整启发式函数的权重
- 预处理与缓存:对于固定图结构,可以预处理结果并缓存
- 并行化:Floyd算法的k循环可以部分并行化
例如,对于Floyd算法,我们可以这样优化空间:
python复制def floyd_optimized(graph):
n = len(graph)
dist = [row[:] for row in graph] # 直接使用输入的邻接矩阵
for k in range(n):
for i in range(n):
if dist[i][k] == float('inf'):
continue
for j in range(n):
if dist[k][j] != float('inf') and dist[i][j] > dist[i][k] + dist[k][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
4.3 边界条件与异常处理
在实际编码中,特别需要注意以下边界条件:
- 图中存在负权边时,Floyd算法可能不正确
- 图不连通时,某些点对之间没有路径
- 启发式函数不可接受时,A*可能找不到最优解
- 浮点数精度问题可能导致比较错误
对于骑士移动问题,还需要考虑:
- 棋盘边界检查
- 起始点和目标点相同的情况
- 不可达情况的处理
5. 从算法题到实际工程的思考
这两道算法题虽然场景简单,但背后反映的工程问题却很普遍。比如:
- 公园路径规划可以扩展到城市交通导航
- 骑士移动问题可以推广到游戏AI中的单位移动
- 算法选择直接影响系统性能和用户体验
在实际工程中,我们往往需要在理论最优和工程实用之间做权衡。例如:
- 对于实时性要求高的场景,可能选择非最优但更快的算法
- 对于数据量大的场景,需要考虑分布式算法
- 对于动态变化的环境,需要增量式算法
我在实现这两道题时,最大的收获是认识到算法不仅仅是解题工具,更是解决实际问题的思维框架。理解算法的本质和适用条件,比记住算法实现更重要。
