狄克斯特拉算法

一、介绍

在前一篇博客中我们学习了广度优先搜索算法,它解决的是段数最少的路径,如果你要找到最快的路径,该怎么办呢?为此,可以使用本篇博客所讲述的算法——狄克斯特拉算法

狄克斯特拉算法

如果你使用广度优先搜索,将得到下面这条段数最少的路径。

狄克斯特拉算法

这条路径耗时7分钟。下面来看看能否找到耗时更短的路径!狄克斯特拉算法包含4个步骤。
(1) 找出“最便宜”的节点,即可在最短时间内到达的节点。
(2) 更新该节点的邻居的开销,其含义将稍后介绍。
(3) 重复这个过程,直到对图中的每个节点都这样做了。
(4) 计算最终路径。
第一步:找出最便宜的节点。你站在起点,不知道该前往节点A还是前往节点B。前往这两个节点都要多长时间呢?

狄克斯特拉算法

前往节点A需要6分钟,而前往节点B需要2分钟。至于前往其他节点,你还不知道需要多长时间。
由于你还不知道前往终点需要多长时间,因此你假设为无穷大(这样做的原因你马上就会明白)。节点B是最近的——2分钟就能达到。

狄克斯特拉算法

第二步:计算经节点B前往其各个邻居所需的时间。

狄克斯特拉算法

你刚找到了一条前往节点A的更短路径!直接前往节点A需要6分钟。

对于节点B的邻居,如果找到前往它的更短路径,就更新其开销。在这里,你找到了:

  • 前往节点A的更短路径(时间从6分钟缩短到5分钟);
  • 前往终点的更短路径(时间从无穷大缩短到7分钟)。

第三步:重复!
重复第一步:找出可在最短时间内前往的节点。你对节点B执行了第二步,除节点B外,可在最短时间内前往的节点是节点A。

狄克斯特拉算法

重复第二步:更新节点A的所有邻居的开销。

你发现前往终点的时间为6分钟!
你对每个节点都运行了狄克斯特拉算法(无需对终点这样做)。现在,你知道:

  • 前往节点B需要2分钟;
  • 前往节点A需要5分钟;
  • 前往终点需要6分钟。

狄克斯特拉算法包含4个步骤。
(1) 找出最便宜的节点,即可在最短时间内前往的节点。
(2) 对于该节点的邻居,检查是否有前往它们的更短路径,如果有,就更新其开销。
(3) 重复这个过程,直到对图中的每个节点都这样做了。
(4) 计算最终路径。

二、换钢琴

Rama想拿一本乐谱换架钢琴。
Alex说:“这是我最喜欢的乐队Destroyer的海报,我愿意拿它换你的乐谱。如果你再加5美元,还可拿乐谱换我这张稀有的Rick Astley黑胶唱片。”Amy说:“哇,我听说这张黑胶唱片里有首非常好听的歌曲,我愿意拿我的吉他或架子鼓换这张海报或黑胶唱片。”

Beethoven惊呼:“我一直想要吉他,我愿意拿我的钢琴换Amy的吉他或架子鼓。”

太好了!只要再花一点点钱,Rama就能拿乐谱换架钢琴。现在他需要确定的是,如何花最少的钱实现这个目标。我们来绘制一个图,列出大家的交换意愿。

狄克斯特拉算法

这个图中的节点是大家愿意拿出来交换的东西,边的权重是交换时需要额外加多少钱。拿海报换吉他需要额外加30美元,拿黑胶唱片换吉他需要额外加15美元。Rama需要确定采用哪种路径将乐谱换成钢琴时需要支付的额外费用最少。为此,可以使用狄克斯特拉算法!别忘了,狄克斯特拉算法包含四个步骤。在这个示例中,你将完成所有这些步骤,因此你也将计算最终路径。动手之前,你需要做些准备工作:创建一个表格,在其中列出每个节点的开销。这里的开销指的是达到节点需要额外支付多少钱。

狄克斯特拉算法

在执行狄克斯特拉算法的过程中,你将不断更新这个表。为计算最终路径,还需在这个表中添加表示父节点的列。

狄克斯特拉算法

第一步:找出最便宜的节点。在这里,换海报最便宜,不需要支付额外的费用。还有更便宜的换海报的途径吗?这一点非常重要,你一定要想一想。Rama能够通过一系列交换得到海报,还能额外得到钱吗?答案是不能,因为海报是Rama能够到达的最便宜的节点,没法再便宜了。

第二步:计算前往该节点的各个邻居的开销。

狄克斯特拉算法

现在的表中包含低音吉他和架子鼓的开销。这些开销是用海报交换它们时需要支付的额外费用,因此父节点为海报。这意味着,要到达低音吉他,需要沿从海报出发的边前行,对架子鼓来说亦如此。

狄克斯特拉算法

再次执行第一步:下一个最便宜的节点是黑胶唱片——需要额外支付5美元。
再次执行第二步:更新黑胶唱片的各个邻居的开销。

狄克斯特拉算法

你更新了架子鼓和吉他的开销!这意味着经“黑胶唱片”前往“架子鼓”和“吉他”的开销更低,因此你将这些乐器的父节点改为黑胶唱片。下一个最便宜的是吉他,因此更新其邻居的开销。

狄克斯特拉算法

你终于计算出了用吉他换钢琴的开销,于是你将其父节点设置为吉他。最后,对最后一个节点——架子鼓,做同样的处理。

狄克斯特拉算法

如果用架子鼓换钢琴,Rama需要额外支付的费用更少。因此,采用最便宜的交换路径时,Rama需要额外支付35美元。
现在来兑现前面的承诺,确定最终的路径。当前,我们知道最短路径的开销为35美元,但如何确定这条路径呢?为此,先找出钢琴的父节点。

狄克斯特拉算法

钢琴的父节点为架子鼓,这意味着Rama需要用架子鼓来换钢琴。因此你就沿着这一边。
我们来看看需要沿哪些边前行。钢琴的父节点为架子鼓。

狄克斯特拉算法

架子鼓的父节点为黑胶唱片。

狄克斯特拉算法

因此Rama需要用黑胶唱片了换架子鼓。显然,他需要用乐谱来换黑胶唱片。通过沿父节点回溯,便得到了完整的交换路径。

狄克斯特拉算法

三、注意

有向无环图

图还可能有环,这意味着你可从一个节点出发,走一圈后又回到这个节点。假设在下面这个带环的图中,你要找出从起点到终点的最短路径。

狄克斯特拉算法

绕环前行是否合理呢?你可以选择避开环的路径。

狄克斯特拉算法

也可选择包含环的路径。

狄克斯特拉算法

这两条路径都可到达终点,但环增加了权重。如果你愿意,甚至可绕环两次。

但每绕环一次,总权重都增加8。因此,绕环的路径不可能是最短的路径。

无向图意味着两个节点彼此指向对方,其实就是环!

狄克斯特拉算法

在无向图中,每条边都是一个环。狄克斯特拉算法只适用于有向无环图。

负权边

回到上面换钢琴的例子,假设黑胶唱片不是Alex的,而是Sarah的,且Sarah愿意用黑胶唱片和7美元换海报。换句话说,换得Alex的海报后,Rama用它来换Sarah的黑胶唱片时,不但不用支付额外的费用,还可得7美元。对于这种情况,如何在图中表示出来呢?

狄克斯特拉算法

从黑胶唱片到海报的边的权重为负!即这种交换让Rama能够得到7美元。现在,Rama有两种获得海报的方式。

狄克斯特拉算法

第二种方式更划算——Rama可赚2美元!你可能还记得,Rama可以用海报换架子鼓,但现在有两种换得架子鼓的方式。

狄克斯特拉算法

第二种方式的开销少2美元,他应采取这种方式。然而,如果你对这个图运行狄克斯特拉算法,Rama将选择错误的路径——更长的那条路径。如果有负权边,就不能使用狄克斯特拉算法。因为负权边会导致这种算法不管用。下面来看看对这个图执行狄克斯特拉算法的情况。首先,创建开销表。

狄克斯特拉算法

接下来,找出开销最低的节点,并更新其邻居的开销。在这里,开销最低的节点是海报。根据狄克斯特拉算法,没有比不支付任何费用获得海报更便宜的方式。(你知道这并不对!)无论如何,我们来更新其邻居的开销。

狄克斯特拉算法

现在,架子鼓的开销变成了35美元。
我们来找出最便宜的未处理节点。

狄克斯特拉算法

更新其邻居的开销。

狄克斯特拉算法

海报节点已处理过,这里却更新了它的开销。这是一个危险信号。节点一旦被处理,就意味着没有前往该节点的更便宜途径,但你刚才却找到了前往海报节点的更便宜途径!架子鼓没有任何邻居,因此算法到此结束,最终开销如下。

狄克斯特拉算法

换得架子鼓的开销为35美元。你知道有一种交换方式只需33美元,但狄克斯特拉算法没有找到。这是因为狄克斯特拉算法这样假设:对于处理过的海报节点,没有前往该节点的更短路径。这种假设仅在没有负权边时才成立。因此,不能将狄克斯特拉算法用于包含负权边的图。在包含负权边的图中,要找出最短路径,可使用另一种算法——贝尔曼-福德算法,你可以在网上找到其详尽的说明。

四、实现

有这样的一条道路,你现在在A点,想要前往H点,请找出最短路径

狄克斯特拉算法

想要使用JAVA构造有向图的数据结构,你可以使用HashMap嵌套,

HashMap<String, HashMap<String, Integer>>

外层String代表节点名称,内层hashmap代表改节点可前往的点,内部hashmap的String代表外层点可到达的点的名称,Integer代表路程

按照上述步骤书写代码,如下:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class Dijkstra {
    public static void main(String[] args) {
        HashMap<String, Integer> A = new HashMap<String, Integer>() {
            {
                put("B", 5);
                put("C", 1);
            }
        };

        HashMap<String, Integer> B = new HashMap<String, Integer>() {
            {
                put("E", 10);
            }
        };
        HashMap<String, Integer> C = new HashMap<String, Integer>() {
            {
                put("D", 5);
                put("F", 6);
            }
        };
        HashMap<String, Integer> D = new HashMap<String, Integer>() {
            {
                put("E", 3);
            }
        };
        HashMap<String, Integer> E = new HashMap<String, Integer>() {
            {
                put("H", 3);
            }
        };
        HashMap<String, Integer> F = new HashMap<String, Integer>() {
            {
                put("G", 2);
            }
        };
        HashMap<String, Integer> G = new HashMap<String, Integer>() {
            {
                put("H", 10);
            }
        };
        HashMap<String, HashMap<String, Integer>> allMap = new HashMap<String, HashMap<String, Integer>>() {
            {
                put("A", A);
                put("B", B);
                put("C", C);
                put("D", D);
                put("E", E);
                put("F", F);
                put("G", G);
            }
        };


        Dijkstra dijkstra = new Dijkstra();
        dijkstra.handle("A", "H", allMap);
    }

    private String getMiniCostKey(HashMap<String, Integer> costs, List<String> hasHandleList) {
        int mini = Integer.MAX_VALUE;
        String miniKey = null;
        for (String key : costs.keySet()) {
            if (!hasHandleList.contains(key)) {
                int cost = costs.get(key);
                if (mini > cost) {
                    mini = cost;
                    miniKey = key;
                }
            }
        }
        return miniKey;
    }

    private 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);
        }


        //选择最"便宜"的节点,这边即耗费时间最低的
        String minCostKey = getMiniCostKey(costMap, hasHandleList);
        while (minCostKey != null) {
            System.out.print("处理节点:" + minCostKey);
            HashMap<String, Integer> nodeMap = all.get(minCostKey);
            if (nodeMap != null) {
                //该节点没有子节点可以处理了,末端节点
                handleNode(minCostKey, nodeMap, costMap, parentMap);
            }
            //添加该节点到已处理结束的列表中
            hasHandleList.add(minCostKey);
            //再次获取下一个最便宜的节点
            minCostKey = getMiniCostKey(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 void handleNode(String startKey, HashMap<String, Integer> nodeMap, HashMap<String, Integer> costMap, HashMap<String, String> parentMap) {

        for (String key : nodeMap.keySet()) {
            //获取原本到父节点所需要花费的时间
            int hasCost = costMap.get(startKey);
            //获取父节点到子节点所需要花费的时间
            int cost = nodeMap.get(key);
            //计算从最初的起点到该节点所需花费的总时间
            cost = hasCost + cost;

            if (!costMap.containsKey(key)) {
                //如果原本并没有计算过其它节点到该节点的花费
                costMap.put(key, cost);
                parentMap.put(key, startKey);
            } else {
                //获取原本耗费的时间
                int oldCost = costMap.get(key);
                if (cost < oldCost) {
                    //新方案到该节点耗费的时间更少
                    //更新到达该节点的父节点和消费时间对应的散列表
                    costMap.put(key, cost);
                    parentMap.put(key, startKey);
                    System.out.print("更新节点:" + key + ",cost:" + oldCost + " --> " + cost);
                }
            }
        }
    }
}