本文属于《算法图解》系列。
一 狄克斯特拉算法
前一章,我们使用了广度优先的算法来查找两点之间的最短距离,那时的“最短距离”是指路径最少,在狄克斯特拉算法中,你给每段都分配了一个数字或权重,因此狄克斯特拉算法找出的是总权重最小的路径。
狄克斯特拉算法包含4个步骤。
(1) 找出最便宜的节点,即可在最短时间内前往的节点。
(2) 对于该节点的邻居,检查是否有前往它们的更短路径,如果有,就更新其开销。
(3) 重复这个过程,直到对图中的每个节点都这样做了。
(4) 计算最终路径。
你可能觉得看起来简单,但是如果从百度百科上看数学原理,还是很复杂的。
下面来看看如何对下面的图使用这种算法。
第一步:找出最便宜的节点。你站在起点,不知道该前往节点A还是前往节点B。前往这两个节点都要多长时间呢?
前往节点A需要6分钟,而前往节点B需要2分钟。由于你还不知道前往终点需要多长时间,因此你假设为无穷大,节点B是最近的,2分钟。
第二步:计算经节点B前往其各个邻居所需的时间。
直接前往节点A需要6分钟。但经由节点B前往节点A只需5分钟。
对于节点B的邻居,如果找到前往它的更短路径,就更新其开销。在这里,你找到了:
前往节点A的更短路径(时间从6分钟缩短到5分钟);
前往终点的更短路径(时间从无穷大缩短到7分钟)。
第三步:重复!
重复第一步:找出可在最短时间内前往的节点。你对节点B执行了第二步,除节点B外,可在最短时间内前往的节点是节点A。
重复第二步:更新节点A的所有邻居的开销。
你发现前往终点的时间为6分钟!
你对每个节点都运行了狄克斯特拉算法(无需对终点这样做)。现在,你知道:
前往节点B需要2分钟;
前往节点A需要5分钟;
前往终点需要6分钟。
那么最终路径为:
术语
狄克斯特拉算法用于每条边都有关联数字的图,这些数字称为权重(weight)。
带权重的图称为加权图(weighted graph),不带权重的图称为非加权图(unweighted graph)。
图还可能有环,而 环类似下面这样。
在无向图中,每条边都是一个环。狄克斯特拉算法只适用于有向无环图(directed acyclic graph,DAG)。
二 负权边
作者还介绍了一个以物换物,最小成本换到目标的例子,乐谱换钢琴
前面使用的都是术语最短路径的字面意思:计算两点或两人之间的最短路径。但希望这个示例让你明白:最短路径指的并不一定是物理距离,也可能是让某种度量指标最小。在这个示例中,最短路径指的是想要额外支付的费用最少。
如果有负权边,就不能使用狄克斯特拉算法。
三 实现
要编写解决这个问题的代码,需要三个散列表。
好吧,又一次见证了Python的强大。可能书上作者的几行代码,用Java就得写一坨。
下面表示边对应的权重。
开销表的代码如下
存储对应父节点关系,(找路径使用)
基础准备做好了,看下算法步骤:
好了,到此为止,已经介绍完了,我试着用Java 写一版。明显不如Python简洁。我觉得永Java的二维数组来标识,不是那么直观的表示出散列表的内容。所以跟上一章类似,还是用hashmap实现。
/**
*
* @author bohu83
*
*/
public class DijkstraTest {
static HashMap<String,Integer> start = new HashMap<String,Integer>();
static HashMap<String,Integer> a = new HashMap<String,Integer>();
static HashMap<String,Integer> b = new HashMap<String,Integer>();
//这里对应图上graph
static HashMap<String,HashMap<String,Integer>> allMap = new HashMap<String,HashMap<String,Integer>>();
static{
//准备数据
start.put("a", 6);
start.put("b", 2);
a.put("end", 1);
b.put("a", 3);
b.put("end", 5);
allMap.put("a", a);
allMap.put("b", b);
allMap.put("start", start);
}
private static void handle(String startKey,String target,HashMap<String,HashMap<String,Integer>> all) {
//存放到各个节点所需要消耗的时间
HashMap<String,Integer> costMap = new HashMap<String,Integer>();
//到各个节点对应的父节点
HashMap<String,String> parentMap = new HashMap<String,String>();
//存放已处理过的节点key,已处理过的不重复处理
List<String> hasHandleList = new ArrayList<String>();
//首先获取开始节点相邻节点信息
HashMap<String,Integer> start = all.get(startKey);
//添加起点到各个相邻节点所需耗费的时间等信息
for(String key:start.keySet()) {
int cost = start.get(key);
costMap.put(key, cost);
parentMap.put(key,startKey);
}
costMap.put("start", Integer.MAX_VALUE);
costMap.put("end", Integer.MAX_VALUE);
//选择最"便宜"的节点,这边即耗费时间最低的
String lowestCostKey = getLowestCostKey(costMap,hasHandleList);
while (lowestCostKey!=null ){
//获取neighbor
HashMap<String,Integer> nodeMap = all.get(lowestCostKey);
if(nodeMap != null){
for(String key : nodeMap.keySet()) {
//获取该节点要花费的时间
int hasCost = costMap.get(lowestCostKey);
//获取该到另一节点所需要花费的时间
int cost = nodeMap.get(key);
//计算从最初的起点到该节点所需花费的总时间
cost = hasCost + cost;
//获取原本耗费的时间
int oldCost = costMap.get(key);
if (cost < oldCost) {
//新方案到该节点耗费的时间更少
//更新到达该节点的父节点和消费时间对应的散列表
costMap.put(key,cost);
parentMap.put(key,lowestCostKey);
System.out.println("更新节点:"+key + ",parent:-->"+lowestCostKey+",cost:" +oldCost + " --> " + cost);
}
}
}
//添加该节点到已处理结束的列表中
hasHandleList.add(lowestCostKey);
//再次获取下一个最便宜的节点
lowestCostKey = getLowestCostKey(costMap,hasHandleList);
}
if(parentMap.containsKey(target)) {
System.out.print("到目标节点"+target+"最低耗费:"+costMap.get(target));
List<String> pathList = new ArrayList<String>();
String parentKey = parentMap.get(target);
while (parentKey!=null) {
pathList.add(0, parentKey);
parentKey = parentMap.get(parentKey);
}
pathList.add(target);
String path="";
for(String key:pathList) {
path = path + key + " --> ";
}
System.out.print("路线为"+path);
} else {
System.out.print("不存在到达"+target+"的路径");
}
}
private static String getLowestCostKey(HashMap<String, Integer> costMap, List<String> hasHandleList) {
int mini = Integer.MAX_VALUE;
String miniKey = null;
for(String key : costMap.keySet()) {
if(!hasHandleList.contains(key)) {
int cost = costMap.get(key);
if(mini > cost) {
mini = cost;
miniKey = key;
}
}
}
System.out.println("lowest cost key="+miniKey);
return miniKey;
}
public static void main(String[] args) {
handle("start","end",allMap);
}
}
运行结果如下:
lowest cost key=b
更新节点:a,parent:-->b,cost:6 --> 5
更新节点:end,parent:-->b,cost:2147483647 --> 7
lowest cost key=a
更新节点:end,parent:-->a,cost:7 --> 6
lowest cost key=end
lowest cost key=null
到目标节点end最低耗费:6路线为start --> b --> a --> end -->
其实自己动手写一下,更容易理解。为了便于理解,贴一下书上的图。
找出开销最低的节点。
获取该节点的开销和邻居。
每个节点都有开销。开销指的是从起点前往该节点需要多长时间。在这里,你计算从起点出 发,经节点B前往节点A(而不是直接前往节点A)需要多长时间。
找到了一条前往节点A的更短路径!因此更新节点A的开销。
下一个邻居是终点节点。 经节点B前往终点需要多长时间呢?
需要7分钟。终点原来的开销为无穷大,比7分钟长。
设置终点节点的开销和父节点。
你更新了节点B的所有邻居的开销。现在,将节点B标记为处理过。
找出接下来要处理的节点。
获取节点A的开销和邻居。
节点A只有一个邻居:终点节点。 当前,前往终点需要7分钟。如果经节点A前往终点,需要多长时间呢?
经节点A前往终点所需的时间更短!因此更新终点的开销和父节点。
处理所有的节点后,这个算法就结束了。