Dijkstra 寻路算法
Dijkstra 是解决单源最短路径问题的算法,是贪婪算法的经典例子,是广度优先搜索算法,是一种发散式的搜索,计算源点(起点)到所有节点的最短路径,解决的是有权图中最短路径问题(注意:权值不能为负)。时间复杂度和空间复杂度都比较高。
优点:当目标不确定时,DijkStra算法是更好的选择。
示例:假如有N个目标,只需要找到一个路径最近的目标,DijkStra算法会从源点开始,一层一层不断扩大范围的搜索,则最先被搜索到的目标即为路径最近的最优选择。
如果使用 AStar,JPS算法等,则需要遍历搜索 N 个目标,最终在N个路径中比较查找最近路径。会存在大量重复计算,降低效率
下面是一个Dijkstra寻路的动画展示
下图为一个有向赋权图 G=(V, E)
图中各节点到其他节点的权值,未连接的节点之间距离认为无穷大 ∞
A | B | C | D | E | F | |
---|---|---|---|---|---|---|
A | 0 | 3 | 5 | 2 | ∞ | ∞ |
B | 0 | 1 | ∞ | ∞ | ∞ | |
C | 0 | ∞ | 1 | 2 | ||
D | 0 | 4 | 6 | |||
E | 0 | ∞ | ||||
F | 0 |
表格读取方式为从横向到数列节点的权值(代价)
A 横向到 A、B、C、D、E、F
A(0) A -> A 权值/代价为 0
B(3) A ->B 权值/代价为 3
C(5) A ->C 权值/代价为 5
D(2) A ->D 权值/代价为 2
E(∞) A ->E 权值/代价为 ∞, A 不能直接到达 E 所以初始时记 A 到 E 的代价无穷大
F(∞) A ->F 权值/代价为 ∞,A 不能直接到达 F 所以初始时记 A 到 F 的代价无穷大
起始时所有节点{A,B,C,D,E,F} 都为未知的 Known = false
以A为源点:所以A为已知的,设置 A.Known = true
逻辑图如下
初始化
S={}
U={A(cost=0, know = true), B(cost=∞, know = false), C(cost=∞, know = false), D(cost=∞, know = false), E(cost=∞, know = false), F(cost=∞, know = false)}
如上A(cost=0, know = true) 解读为 从起点到 A 的代价为 0,A 为已知的
B(cost=∞, know = false) 解读为 从起点到 B 的代价为 ∞,B为未知的
下面简写为 A(0, true) ,B(∞, false)
S={}
U={A(0,true), B(∞,false), C(∞, false), D(∞,false), E(∞, false), F(∞, false)}
注:S 是已经计算出最短路径的节点集合
U 是未计算出最短路径的节点集合
代码核心逻辑如下
public class Node
{
// 节点名
public string nodeName;
// 节点标记:已知/未知
public bool know = false;
// 节点邻居
public List<Node> neighbourList = new List<Node>();
// 节点到邻居的代价
public List<int> neighbourCostList = new List<int>();
// 从源点到当前节点的代价(默认值为int.MaxValue)
public int cost = int.MaxValue;
public Node(string nodeName)
{
this.nodeName = nodeName;
}
}
class Dijkstra
{
public Dijkstra()
{
// 配置节点
Node nodeA = new Node("A");
Node nodeB = new Node("B");
Node nodeC = new Node("C");
Node nodeD = new Node("D");
Node nodeE = new Node("E");
Node nodeF = new Node("F");
// A 为起点,所以已知节点设置cost = 0
nodeA.cost = 0;
nodeA.know = true;
// 将节点 B 加入到节点 A 的邻居中
nodeA.neighbourList.Add(nodeB);
// 添加节点 A 到 节点 B 的代价(3)
nodeA.neighbourCostList.Add(3);
// 将节点 C 加入到节点 A 的邻居中
nodeA.neighbourList.Add(nodeC);
// 添加节点 A 到 节点 C 的代价(5)
nodeA.neighbourCostList.Add(5);
// 将节点 D 加入到节点 A 的邻居中
nodeA.neighbourList.Add(nodeD);
// 添加节点 A 到 节点 D 的代价(2)
nodeA.neighbourCostList.Add(2);
nodeB.neighbourList.Add(nodeC);
nodeB.neighbourCostList.Add(1);
nodeC.neighbourList.Add(nodeE);
nodeC.neighbourCostList.Add(1);
nodeC.neighbourList.Add(nodeF);
nodeC.neighbourCostList.Add(2);
nodeD.neighbourList.Add(nodeE);
nodeD.neighbourCostList.Add(4);
nodeD.neighbourList.Add(nodeF);
nodeD.neighbourCostList.Add(6);
List<Node> nodeList = new List<Node>();
nodeList.Add(nodeA);
nodeList.Add(nodeB);
nodeList.Add(nodeC);
nodeList.Add(nodeD);
nodeList.Add(nodeE);
nodeList.Add(nodeF);
UpdateGraph(nodeList);
}
public void UpdateGraph(List<Node> nodeList)
{
// 根据权值排序,这里可以将List换成小根堆
nodeList.Sort((a, b) =>
{
return a.cost - b.cost;
});
List<Node> closedList = new List<Node>();
while (nodeList.Count > 0)
{
Node node = null;
// 获取所有已知节点(know=true)中权值最小的节点
for (int i = 0; i < nodeList.Count; ++i)
{
Node temp = nodeList[i];
if (!temp.know)
{
continue;
}
node = temp;
nodeList.RemoveAt(i);
break;
}
// 如果没有已知节点,则退出
if (null == node)
{
break;
}
// 将已知节点加入到 closedList
closedList.Add(node);
// 遍历节点 node 的所有邻居节点
for (int i = 0; i < node.neighbourList.Count; ++i)
{
Node neighbour = node.neighbourList[i];
int cost = node.cost + node.neighbourCostList[i];
// 更新邻居节点的权值
neighbour.cost = neighbour.cost < cost ? neighbour.cost : cost;
// 将邻居节点设置为已知节点(know=true)
neighbour.know = true;
}
// 重新根据权值排序,这里可以换成小根堆
nodeList.Sort((a, b) =>
{
return a.cost - b.cost;
});
}
// 遍历closedList打印每个节点的权值
for (int i = 0; i < closedList.Count; ++i)
{
Node node = closedList[i];
Console.WriteLine(node.nodeName + " " + node.cost);
}
}
}
堆优化:
将上方的 openList 替换为 最小堆,下面简称 堆
逻辑实现:
1.将起点加入 堆,调整堆
2.获取堆顶节点N (即当前代价最小的节点),从堆中删除节点N,并对堆进行调整
3.遍历节点N 的相邻节点
(1) 如果该节点在堆里或已访问过,且当前计算距离 < 之前计算的距离,则更新距离,并调整堆
(2) 如果该节点不在堆里,加入堆,更新小根堆
4.如果节点 N 是终点,结束算法,否则重复步骤 2、3
下面是实现的代码动画展示
蓝色小球位置为搜索过的节点