至今,我偶尔会遇到一些寻路的寻求,虽然是偶尔,但时间久了,也就多了。
我对寻路是没有太多研究的,所以就去B站搜了一下,搜到一个很棒的dikjstra教程。
我照着这个教程,用ts写了一下这个逻辑,分享给大家。
整体代码如下:
scss
/* 节点关系图
* id 唯一标志符,与其key值一致
* adjacency 邻点
* distance 当前点到起点的路径距离
* prev 前面点
*/
type MapData = {
id: number
adjacency: number[]
distance: number
prev: undefined | MapData
}
const map: Map<number, MapData> = new Map([
[0, { id: 0, adjacency: [1, 7], distance: Infinity, prev: undefined }],
[1, { id: 1, adjacency: [0, 2, 7], distance: Infinity, prev: undefined }],
[2, { id: 2, adjacency: [1, 3, 5, 8], distance: Infinity, prev: undefined }],
[3, { id: 3, adjacency: [2, 4, 5], distance: Infinity, prev: undefined }],
[4, { id: 4, adjacency: [3, 5], distance: Infinity, prev: undefined }],
[5, { id: 5, adjacency: [2, 3, 4, 6], distance: Infinity, prev: undefined }],
[6, { id: 6, adjacency: [5, 7, 8], distance: Infinity, prev: undefined }],
[7, { id: 7, adjacency: [0, 1, 6, 8], distance: Infinity, prev: undefined }],
[8, { id: 8, adjacency: [2, 6, 7], distance: Infinity, prev: undefined }],
])
/* 线段长度图
* key用'-'符号将两个自个升序的id链接起来。
*/
const segmentLengthMap: { [k: string]: number } = {
'0-1': 4,
'0-7': 8,
'1-2': 8,
'1-7': 11,
'2-3': 7,
'2-5': 4,
'2-8': 2,
'3-4': 9,
'3-5': 14,
'4-5': 10,
'5-6': 2,
'6-7': 1,
'6-8': 6,
'7-8': 7,
}
let start = 0
const end = 4
// 标记节点集合
const mark = new Set<number>()
// 未标记节点集合
let unmark = new Set<number>()
// 最优路径
let optimal: number[] = []
// 初始化
init()
console.log(optimal)
/* 初始化
* 定义起点start
* 对起点做标记,将起点添加到最优路径mark中
* 起点的路径距离默认为0
* 起点的前面点默认为undefined
* 让未标记点集合unmark等于map中除start之外的所有点
* 寻路
* 回溯
*/
function init(n: number = 0) {
start = n
const startNode = map.get(n)
if (!startNode) {
console.error('起点未找到')
return
}
mark.add(n)
startNode.distance = 0
unmark = new Set([...map.values()].map((ele) => ele.id))
unmark.delete(n)
explorer(startNode)
optimal = backtrack(map.get(end))
}
/* 寻路 */
function explorer(markNode: MapData) {
/* 更新当前标记点的邻点的距离与前面点 */
for (let n of markNode.adjacency) {
// 若标记点集合中包含此点,则跳过此点
if (mark.has(n)) {
continue
}
// 根据id获取当前邻点
const curNode = map.get(n)
if (!curNode) {
console.error('当前邻点未找到')
continue
}
// 获取当前邻点过当前标记点后的路径距离
const dist = getDistance2(curNode, markNode)
/* 若dist小于当前邻点的已记录过的路径距离,则:
* 更新当前邻点的路径距离为dist
* 前面点为当前标记点
*/
if (dist < curNode.distance) {
curNode.distance = dist
curNode.prev = markNode
}
}
/* 从未标记点集合unmark中寻找到start的路径距离最短的点 */
// 基于路径距离对unmark进行升序排序,然后取第一个点
const nearest = [...unmark].sort((a, b) => {
const [an, bn] = [map.get(a), map.get(b)]
if (!an || !bn) {
console.error('节点不存在')
return 0
}
return getDistance1(an) - getDistance1(bn)
})[0]
/* 标记nearest,将其添加到标记点集合mark中,并从unmark中删除 */
const nearestNode = map.get(nearest)
if (!nearestNode) {
console.error('最近点获取失败')
return
}
mark.add(nearest)
unmark.delete(nearest)
/* 若unmark中还有没标记完的点,继续寻路 */
if (unmark.size) {
explorer(nearestNode)
}
}
/* 获取node到起点start的路径距离 */
function getDistance1(node: MapData): number {
// 当前节点的前面点
const { prev } = node
if (!prev) {
// 没有前面点,具备此情况的点除了起点还有所有未探索过的点
return Infinity
}
return getDistance2(node, prev)
}
/* 获取node过prev后,到起点start的路径距离 */
function getDistance2(node: MapData, prev: MapData): number {
/* 获取当前点与前一个点的距离
* 基于两个节点的id拼成segmentLengthMap中的key
*/
const [n, p] = [node.id, prev.id]
const key = [n, p].sort().join('-')
const d = segmentLengthMap[key]
if (d === undefined) {
console.error('节点距离未找到')
return Infinity
}
/*
* 若前面点是起点,返回d
* 否则,进行线段距离的累加
*/
if (p === start) {
return d
} else {
const ppNode = prev.prev
if (!ppNode) {
console.error('前面点的前一个点未找到')
return Infinity
}
return d + getDistance2(prev, ppNode)
}
}
/* 从结束点回溯路径 */
function backtrack(node: MapData | undefined): number[] {
if (!node) {
console.error('回溯点的前面点未找到')
return []
}
const { id } = node
if (id === start) {
return [id]
}
return [node.id, ...backtrack(node.prev)]
}
最终,console.log(optimal) 输出的结果就是:[4, 5, 6, 7, 0]
这与dikjstra教程里的结果是一致的。
根据这个教程内容,做个笔记。
已知:
- 节点关系map,即每个节点的邻点
- 节点距离segmentLengthMap,存在联系的两个节点间的距离
- 起点start为0
- 终点end为4
注:上图的节点距离并非通过两点间的距离公式算出来的,所以我们不能通过线段长度来区分距离的大小。
求:起点到终点的最短路径
解:
1.对起点做标记,将起点添加到标记点集合mark中。
起点的路径距离默认为0,路径距离就是节点到起点的路径距离,所以起点到起点的默认距离为0。
起点的前面点默认为undefined,因为起点就是最源头的点,没有前面点。
2.让未标记点集合unmark等于map中除start之外的所有点。
3.遍历当前标记点(第一个标记点就是起点)的邻点。
若邻点在标记点集合mark中,跳过此点;
否则,若当前邻点过当前标记点的路径距离小于当前节点的已记录过的路径距离,则:
- 更新当前邻点的路径距离为当前邻点过当前标记点的路径距离。
- 更新当前邻点的前面点为当前标记点。
4.遍历未标记点集合unmark,从中找出路径最短的点nearest。
对nearest点做标记,将其添加到mark中,并从unmark中删除。
5.若unmark不为空,以nearest点为标记点,重复3,4,5步骤。
当unmark为空的时候,所有的节点就都完成了标记。
6.对终点进行回溯,寻找其前面点的前面点的前面点的前面点......,直到找到其源头的点-起点。
把上面的一堆前面点按序连在一起,就是最短路径了。