1. 项目概述
"代码随想录算法训练营第六十天"这个标题背后隐藏着两个经典的算法题目:97号题目"小明逛公园"和127号题目"骑士的攻击"。作为算法训练营的收官之作,这两个题目分别代表了图论和搜索算法中的典型问题,考察学员对Floyd算法和A*启发式搜索等高级算法的掌握程度。
在算法竞赛和面试中,这类题目经常出现。它们不仅考察基础编码能力,更考验解题者对算法原理的深入理解和灵活运用。通过这两个题目,我们可以系统性地复习图的最短路径问题和启发式搜索算法,这些都是算法工程师必须掌握的核心技能。
2. 题目解析与算法选择
2.1 97号题目:小明逛公园
这是一个典型的图论问题,可以抽象为在有向图中寻找特定条件下的最短路径。题目描述通常是:公园有N个景点,由M条有向路径连接,每条路径有相应的距离。小明想从入口出发,经过若干景点后回到入口,且总距离不超过K,问有多少种不同的游览路线。
这类问题最适合使用Floyd算法来解决。Floyd-Warshall算法是一种计算图中所有顶点对之间最短路径的动态规划算法,其时间复杂度为O(N^3),适合处理节点数不多(通常N≤100)的情况。
Floyd算法的核心思想是:
for k from 1 to N for i from 1 to N for j from 1 to N dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])这个三重循环逐步优化每对节点之间的最短距离,考虑了通过中间节点k的所有可能路径。
2.2 127号题目:骑士的攻击
这是一个棋盘类问题,通常描述为:在一个N×N的棋盘上,骑士从起始位置出发,每次按照国际象棋中骑士的走法(日字形移动),问最少需要多少步可以到达目标位置,或者计算骑士在k步内可以攻击到的所有位置。
对于这类问题,A启发式搜索算法是最佳选择。A算法结合了Dijkstra算法的准确性(保证找到最短路径)和贪心算法的高效性(通过启发式函数引导搜索方向),其核心公式为: f(n) = g(n) + h(n) 其中g(n)是从起点到节点n的实际代价,h(n)是从节点n到目标的预估代价。
对于骑士移动问题,常用的启发式函数有:
- 曼哈顿距离除以3(因为骑士每步最多缩短3单位曼哈顿距离)
- 切比雪夫距离的最大值除以2
3. 算法实现细节
3.1 Floyd算法的实现要点
实现Floyd算法时需要注意以下几个关键点:
- 初始化距离矩阵:
dist = [[float('inf')] * N for _ in range(N)] for i in range(N): dist[i][i] = 0 # 节点到自身的距离为0 for u, v, w in edges: dist[u][v] = w # 初始化直接相连的边- 动态规划更新:
for k in range(N): for i in range(N): for j in range(N): if dist[i][k] + dist[k][j] < dist[i][j]: dist[i][j] = dist[i][k] + dist[k][j]- 检测负权环: 在算法结束后,检查是否存在dist[i][i] < 0的节点,如果存在说明图中包含负权环。
注意:Floyd算法可以处理负权边,但不能处理含有负权环的图(此时最短路径无意义)。
3.2 A*算法的实现要点
实现A*算法时需要关注:
- 启发式函数的设计:
def heuristic(a, b): # 使用切比雪夫距离 dx = abs(a[0] - b[0]) dy = abs(a[1] - b[1]) return max(dx, dy) / 2- 优先队列的使用:
import heapq def a_star(start, goal): open_set = [] heapq.heappush(open_set, (0 + heuristic(start, goal), 0, start)) # 其他初始化代码...- 邻居节点的生成(针对骑士移动):
def get_neighbors(pos): x, y = pos moves = [(1,2),(2,1),(-1,2),(-2,1), (1,-2),(2,-1),(-1,-2),(-2,-1)] neighbors = [] for dx, dy in moves: nx, ny = x + dx, y + dy if 0 <= nx < N and 0 <= ny < N: neighbors.append((nx, ny)) return neighbors4. 优化技巧与常见问题
4.1 Floyd算法的优化
- 空间优化:可以使用单个二维数组,原地更新距离矩阵。
- 提前终止:如果只关心特定节点对的距离,可以在发现目标距离满足条件时提前终止。
- 并行化:最内层循环可以并行执行,因为每个dist[i][j]的计算是独立的。
常见问题:
- 忘记初始化对角线距离为0
- 混淆节点编号是0-based还是1-based
- 没有处理无穷大的情况导致整数溢出
4.2 A*算法的优化
- 启发式函数的选择:不同的启发式函数对性能影响很大,需要根据具体问题选择。
- 关闭集的管理:使用高效的数据结构(如哈希表)来存储已访问节点。
- 双向搜索:同时从起点和终点开始搜索,可以显著减少搜索空间。
常见问题:
- 启发式函数不满足可采纳性(高估实际代价)
- 忘记处理重复访问节点的情况
- 优先队列中存储的信息不足,无法正确回溯路径
5. 实际应用场景
5.1 Floyd算法的应用
- 交通网络规划:计算城市之间最短路径
- 网络路由:确定数据包传输的最佳路径
- 社交网络分析:计算用户之间的关系紧密度
- 游戏开发:NPC的路径寻找
5.2 A*算法的应用
- 游戏AI:角色寻路(如RTS游戏中的单位移动)
- 机器人导航:自动驾驶车辆的路径规划
- 物流配送:快递员的最优送货路线
- 拼图游戏:自动求解器
6. 代码实现示例
6.1 小明逛公园的Floyd实现
def count_park_routes(N, edges, K): # 初始化距离矩阵 dist = [[float('inf')] * N for _ in range(N)] for i in range(N): dist[i][i] = 0 for u, v, w in edges: dist[u][v] = w # Floyd算法 for k in range(N): for i in range(N): for j in range(N): if dist[i][k] + dist[k][j] < dist[i][j]: dist[i][j] = dist[i][k] + dist[k][j] # 统计满足条件的路径数量 count = 0 # 这里需要根据具体题目要求实现路径统计逻辑 # 可能是DFS/BFS遍历所有可能路径 return count6.2 骑士攻击的A*实现
from heapq import heappop, heappush def knight_attack(N, start, target): def heuristic(pos): dx = abs(pos[0] - target[0]) dy = abs(pos[1] - target[1]) return max(dx, dy) / 2 moves = [(1,2),(2,1),(-1,2),(-2,1), (1,-2),(2,-1),(-1,-2),(-2,-1)] open_set = [(heuristic(start), 0, start)] closed_set = set() while open_set: _, g, pos = heappop(open_set) if pos == target: return g if pos in closed_set: continue closed_set.add(pos) for dx, dy in moves: x, y = pos[0] + dx, pos[1] + dy if 0 <= x < N and 0 <= y < N: new_pos = (x, y) heappush(open_set, (g + 1 + heuristic(new_pos), g + 1, new_pos)) return -1 # 无法到达7. 性能分析与比较
7.1 Floyd算法复杂度
- 时间复杂度:O(N^3)
- 空间复杂度:O(N^2)
- 适用场景:稠密图,需要所有节点对的最短路径
- 限制:节点数不宜过大(通常N≤500)
7.2 A*算法复杂度
- 时间复杂度:取决于启发式函数质量,最好情况O(b^d),其中b是分支因子,d是解深度
- 空间复杂度:O(b^d)(需要存储所有待探索节点)
- 适用场景:单源最短路径,特别是知道目标位置的情况
- 限制:需要设计良好的启发式函数
8. 进阶思考与扩展
- 对于"小明逛公园"问题,如果限制必须访问某些特定景点,可以结合状态压缩DP来扩展Floyd算法。
- "骑士的攻击"问题可以扩展到三维棋盘,或者考虑不同的移动规则。
- 在实际工程中,可以结合多种算法:先用A*找到大致路径,再用局部搜索优化细节。
- 对于大规模图,可以考虑分层策略或近似算法来提高性能。
我在实际编码中发现,Floyd算法的三重循环顺序非常重要,必须是k在最外层。曾经因为调换循环顺序导致错误,调试了很久才发现。而A*算法的性能高度依赖启发式函数的设计,有时候一个简单的启发式函数改进可以让搜索效率提升10倍以上。