97. 小明逛公园,Floyd 算法,127. 骑士的攻击,A * 算法
97. 小明逛公园
Floyd 算法
dijkstra, bellman_ford 是求单个起点到单个终点的最短路径,dijkstra无法解决负权边的问题, bellman_ford解决了负权边的问题,但二者都是基于单起点和单终点。
而Floyd 算法旨在解决多个起点到多个终点的最短路径问题,且Floyd 算法对边的权值正负没有要求,都可以处理。Floyd 算法采用的是动态规划的思想,那涉及到动态规划就需要用到动态规划五部曲了。
- 1. dp定义
dp[i][j][k] 表示 节点i 到 节点j 以[1...k] 集合中的一个节点为中间节点的最短距离。
k 是指取 区间[1,k] 中的一个节点作为中间节点;节点i 到 节点j 的最短路径中 一定是经过很多节点,那么这个集合用[1...k] 来表示。
- 2. dp初始化
刚开始进来时,每条边的关系进来时都不需要经过中间节点,因此输入的边的关系用dp[i][j][0]进行初始化。
- 3. dp递推公式
当到达节点k时,此时有两种状态,一种是经过节点k,另外一种是不经过中间节点k。
- 经过节点k,那此时dp[i][j][k] = dp[i][k][k-1] + dp[k][j][k-1]
- 不经过中间节点k,那此时dp[i][j][k] = dp[i][j][k-1]
求的是最小距离,因此dp[i][j][k] = min( dp[i][j][k-1] + dp[k][j][k-1], dp[i][j][k-1])
补充:节点1 到 节点9 的最短距离 是不是可以由 节点1 到节点5的最短距离 + 节点5到节点9的最短距离组成呢?即 grid[1][9] = grid[1][5] + grid[5][9]。
- 4. 遍历顺序
由于初始化得到的是一个平面,且递推公式是往上去进行递推的,因此需要三个循环去进行递推,k处于外循环,i,j构成内循环。即先平面再高度的方式去进行递推。
- 5. 打印dp
Code:
if __name__ == "__main__":scenery_size, road_size = map(int, input().split())length = scenery_size + 1# 1. dp数组定义dp = [ [[float('inf')]*length for _ in range(length)] for _ in range(length) ] ##先构造一个二维数组,再外封遍历从而构成三维数组# 2. dp初始化for _ in range(road_size):u, v, w = map(int, input().split())dp[u][v][0] = w dp[v][u][0] = w ## 无向图Q = int(input())plan = []for _ in range(Q):start, end = map(int, input().split())plan.append([start,end])# 3. 递推公式# dp[i][j][k] = min(dp[i][j][k-1], dp[i][k][k-1]+dp[k][j][k-1])# 4.遍历顺序for k in range(1, length):for i in range(1,length):for j in range(1,length):dp[i][j][k] = min(dp[i][j][k-1], dp[i][k][k-1]+dp[k][j][k-1])# 5. 打印dp数组for i in range(len(plan)): ## 结果输出start = plan[i][0]end = plan[i][1]if dp[start][end][scenery_size] != float('inf'):print(dp[start][end][scenery_size])else:print(-1)
其他:
- dp = [ [[float('inf')]*length for _ in range(length)] for _ in range(length) ]
先构造 length 个list, 然后每个list里面存储的是一个二维矩阵。
- Floyd的算法是基于点的角度去进行计算的,因此适合于稠密图(边多),不适合稀疏图(边少)。也就说当你图中有1万个点,但只有一条边时,此时用这个算法就不合适。(计算时间复杂度O(n^3), n是节点数目)
127. 骑士的攻击
A * 算法
关键:启发式函数,来确定广搜的方向。
A * 算法其实就是个改进版的广搜,其与BFS的区别如下。BFS是每次都往四面八方去进行搜索,而A*会计算当前离终点的距离去判断要往那个方向进行广搜,从而极大减少了广搜的次数。
由于每次BFS都是选取dis最小的一个元素进行下一次BFS,因此需要用到小顶堆。
dis = cur_dis + remaining_dis(通过引入这个变量来明确遍历的方向)
- cur_dis: 表示从起点出发到当前节点时已走过的步数。(题目要求是输出步数,而不是距离)
- remaining_dis:当前节点 到 终点的距离。
- dis 与 cur_dis 呈正相关,当dis有最小时,此时cur_dis有最小。
如何计算、保存 cur_dis,并通过小顶堆对dis进行排序,通过堆顶元素来指导BFS的方向是这道题的关键。
Code
from collections import deque
import heapq
import mathdirections = [[2,1],[-2,1],[1,2],[-1,2],[2,-1],[-2,-1],[1,-2],[-1,-2]]def caculate_dis(point_1, point_2): ## 计算当前点到终点的距离return ((point_1[0] - point_2[0]) ** 2 + (point_1[1] - point_2[1]) ** 2) ** 0.5 ### **2 表示平方, ** 0.5 表示sqrt(),即开根def A_star(start, end):dis = -1length = 1001q = [ (caculate_dis(start, end), start) ] ## 队列内存储一个元组,元组包含了距离信息和起点位置。在小顶堆中会优先对元组中靠左的元素进行排序heapq.heappush(q, (caculate_dis(start, end), start))step = {start: 0} ## 一个字典,来记录从起点到当前节点所走过的距离。数组不能作为key值while q: ## q是一个队列,跟BFS的思路一样dis, cur = heapq.heappop(q) ## 推出小顶堆的堆顶if cur == end: ## 当前节点抵达终点return step[cur]for x_move, y_move in directions:new_x = cur[0] + x_movenew_y = cur[1] + y_moveif new_x < 1 or new_x > 1000 or new_y < 1 or new_y > 1000:continuenew = (new_x, new_y)step_new = step[cur] + 1 #计算起点到当前节点所走过的距离,每走一次加一次if step_new < step.get(new, float('inf')): ## 不存在new这个key时,输出inf。 如果从起点出发有走更少的距离抵达目前这个点,则不更新step[new] = step_new # 记录从起点到当前节点所走过的距离heapq.heappush(q, (caculate_dis(new, end)+step[new], new))if __name__ == "__main__":test_num = int(input())for _ in range(test_num):start_x, start_y, end_x, end_y = map(int, input().split())start = (start_x, start_y)end = (end_x, end_y)min_List = A_star(start, end)print(min_List)
from collections import defaultdict
import math
import heapqdirections = [ [2,1], [2,-1],[1,2], [1, -2],[-2,1], [-2,-1],[-1,2], [-1,-2],]def caculate_dis(cur, end): ## 这里不能改精度,会影响搜索dis = math.sqrt((cur[0] - end[0])**2 + (cur[1] - end[1])**2)return disdef A_star(start, end):queue = [(caculate_dis(start,end), start)]heapq.heappush(queue, (caculate_dis(start,end), start))step = {}step[start] = 0while queue:dis, cur = heapq.heappop(queue)if cur == end:return disfor x_move, y_move in directions:new_x = cur[0] + x_movenew_y = cur[1] + y_moveif new_x < 1 or new_x > 1000 or new_y < 1 or new_y > 1000:continuenew = (new_x, new_y)step_new = step[cur] + 1if step_new < step.get(new, float('inf')):step[new] = step_newheapq.heappush(queue, ((caculate_dis(new, end)+step_new),new))## 浮点数+整数 = 浮点数,后续堆result结果进行int转换就行if __name__ == "__main__":test_num = int(input())for _ in range(test_num):start_x, start_y, end_x, end_y = map(int, input().split())start = (start_x, start_y)end = (end_x, end_y)min_result = A_star(start, end)print(int(min_result))
注意:
- 为什么需要这个判断 if step_new < step.get(new, float('inf'))
因为走到同一个节点下所用的步数可以步数,但我么要求的是所用步数最少的。如上,只有红色箭头是所花步数是最少的。
- 另外由于A*算法具有一定的方向性(小顶堆+dis实现),因此每次BFS只会选取一个更靠近终点的点进行下一次BFS,故不会存在因为重复遍历而出现死循环的问题。
缺点:
- 小顶堆内维护了很多元组,但实际用到的时候只是使用到了堆顶。如果在一次路径搜索中,大量不需要访问的节点都在队列里,会造成空间的过度消耗。
- A*算法只擅长给出明确的目标 然后找到最短路径。如果给出 多个可能的目标,然后在这多个目标中 选择最近的目标,则A * 就不擅长了。