《算法》笔记 12 - 最短路径

  • 加权有向图
  • 数据结构
    • 加权有向边
    • 加权有向图
    • 最短路径
  • 边的松弛
  • Dijkstra算法

地图或者导航系统是最短路径的典型应用,其中顶点对应交叉路口,边对应公路,边的权重对应经过一段路的成本(时间或距离)。在这个模型中,问题可以被归纳为:找出从一个顶点到达另一个顶点的成本最小的路径。此外,网络路由、任务调度等也属于同类问题。

加权有向图

加权有向图是研究最短路径问题的模型。在加权有向图中,每条有向边都有一个与之关联的权重,路径的权重指路径上所有边的权重之和,所以在加权有向图中,求顶点v到w的最短路径问题就成了求顶点v到w的所有路径中权重最小者。

数据结构

加权有向边

加权有向边是构成加权有向图的基本单元,具有起点、终点和权重属性。

public class DirectedEdge {
    private final int v; 
    private final int w; 
    private final double weight; 

    public DirectedEdge(int v, int w, double weight) {
        this.v = v;
        this.w = w;
        this.weight = weight;
    }

    public double weight() {
        return this.weight;
    }

    public int from() {
        return this.v;
    }

    public int to() {
        return this.w;
    }

    public String toString() {
        return String.format("%d->%d %.2f", v, w, weight);
    }
}

加权有向图

加权有向图的数据结构中,对加权有向边的组织方式与之前的有向图的组织方式类似,不同的是在邻接表中,之前存储的是图的顶点,现在则存储边。

public class EdgeWeightedDigraph {
    private static final String NEWLINE = System.getProperty("line.separator");
    private final int V; // vertex
    private int E; // edge
    private Bag<DirectedEdge>[] adj;

    public EdgeWeightedDigraph(int V) {
        this.V = V;
        this.E = 0;
        adj = (Bag<DirectedEdge>[]) new Bag[V];
        for (int v = 0; v < V; v++) {
            adj[v] = new Bag<DirectedEdge>();
        }
    }

    public EdgeWeightedDigraph(In in) {
        this(in.readInt());
        int E = in.readInt();
        for (int i = 0; i < E; i++) {
            int v = in.readInt();
            int w = in.readInt();
            double weight = in.readDouble();
            DirectedEdge e = new DirectedEdge(v, w, weight);
            addEdge(e);
        }
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    public void addEdge(DirectedEdge e) {
        adj[e.from()].add(e);
        E++;
    }

    public Iterable<DirectedEdge> adj(int v) {
        return adj[v];
    }

    public String toString() {
        StringBuilder s = new StringBuilder();
        s.append(V + " vertices, " + E + " edges " + NEWLINE);
        for (int v = 0; v < V; v++) {
            s.append(v + ": ");
            for (DirectedEdge w : adj[v]) {
                s.append(w + " | ");
            }
            s.append(NEWLINE);
        }
        return s.toString();
    }

    public Bag<DirectedEdge> edges() {
        Bag<DirectedEdge> b = new Bag<DirectedEdge>();
        for (int v = 0; v < V; v++) {
            for (DirectedEdge w : adj[v]) {
                b.add(w);
            }
        }
        return b;
    }
}

最短路径

在加权有向图中计算最短路径时,会用到一个由DirectedEdge对象组成的数组edgeTo[]和一个double类型的数组distTo[]。

  • edgeTo表示了一颗最短路径树,保存的是算法的计算结果。以s为起点的一颗最短路径树是图的一副子图,它包含了s和从s可达的所有顶点。这棵树的根节点为s,树的每条路径都是有向图中的一条最短路径。
    edgeTo的索引表示图的结点,edgeTo[4]的值为指向结点4的边,这条边的to=4,from=指向结点4的顶点,weight=边的权重。
    根据edgeTo可沿着最短路径树不断上溯,直到树的根结点s。edgeTo[s]的值为null。

  • distTo[]存储了从一个结点到达起点s的最短路径的成本。所以distTo[s]=0,并约定对于和s没有连通的结点,在distTo中对应位置的值为Double.POSITIVE_INFINITY,这样判断distTo某个位置是否等于Double.POSITIVE_INFINITY就可以知道它与s是否连通。

边的松弛

最短路径算法的实现基于一种叫做松弛(relaxation)的简单操作。一开始只知道图的边和它们的权重,distTo[]中只有起点所对应元素的值为0,其余元素的值都被初始化为Double.POSITIVE_INFINITY,随着算法的执行,它将起点到其他顶点的最短路径信息存入了edgeTo[]和distTo[]。在遇到新的边时,会通过松弛操作来更新最短路径。松弛操作是指对于边v->w,会检查从s到w的最短路径是否经过v,既s->v->w,如果是,就更新edgeTo[w]和distTo[w],否则保持现状。

if (distTo[w] > distTo[v] + e.weight()) {
    distTo[w] = distTo[v] + e.weight();
    edgeTo[w] = e;
}

松弛这个术语来自于用一根橡皮筋沿着连接两点的路径紧紧展开的比喻,松弛一条边就类似于将橡皮筋转移到一条更短的路径上,使橡皮筋相比之前要松弛。

在最短路径算法的实现中,会尝试松弛从一个顶点指出的所有边,如果某次松弛操作改变了distTo和edgeTo的值,则这次操作是成功的,最终会找出到达每个顶点的最短路径。

Dijkstra算法

Dijkstra算法是基于上述讨论实现的,除了distTo和edgeTo数组之外,还需要一条索引优先队列(IndexMinPQ),以保存需要被松弛的顶点并确认下一个被松弛的顶点。IndexMinPQ除了具有MinPQ的功能,还可以通过索引修改对应位置的值,算法会利用IndexMinPQ将顶点v和distTo[v]关联起来,并在松弛操作中根据索引设置对应的路径距离。

public class DijkstraSP {
    private DirectedEdge[] edgeTo;
    private double[] distTo;
    private IndexMinPQ<Double> pq;
    int s;

    public DijkstraSP(EdgeWeightedDigraph G, int s) {
        edgeTo = new DirectedEdge[G.V()];
        distTo = new double[G.V()];
        pq = new IndexMinPQ<Double>(G.V());
        this.s = s;

        for (int v = 0; v < G.V(); v++) {
            distTo[v] = Double.POSITIVE_INFINITY;
        }

        distTo[s] = 0.0;
        pq.insert(s, 0.0);
        while (!pq.isEmpty()) {
            relax(G, pq.delMin());
        }
    }

    private void relax(EdgeWeightedDigraph G, int v) {
        for (DirectedEdge e : G.adj(v)) {
            int w = e.to();
            if (distTo[w] > distTo[v] + e.weight()) {
                distTo[w] = distTo[v] + e.weight();
                edgeTo[w] = e;
                if (pq.contains(w))
                    pq.change(w, distTo[w]);
                else
                    pq.insert(w, distTo[w]);
            }
        }
    }

    public double distTo(int w) {
        return distTo[w];
    }

    public boolean hasPathTo(int w) {
        return distTo(w) < Double.POSITIVE_INFINITY;
    }

    public Iterable<DirectedEdge> pathTo(int v) {
        if (!hasPathTo(v))
            return null;

        Stack<DirectedEdge> path = new Stack<DirectedEdge>();
        for (DirectedEdge a = edgeTo[v]; a != null; a = edgeTo[a.from()]) {
            path.push(a);
        }
        return path;
    }

    // cmd /c --% java algs4.four.DijkstraSP ..\..\..\algs4-data\tinyEWG.txt
    public static void main(String[] args) {
        In in = new In(args[0]);
        EdgeWeightedDigraph ewdg = new EdgeWeightedDigraph(in);
        int s = Integer.parseInt(args[1]);
        DijkstraSP dijkstraSP = new DijkstraSP(ewdg, s);

        for (int t = 0; t < ewdg.V(); t++) {
            StdOut.print(s + " to " + t);
            StdOut.printf(" (%4.2f): ", dijkstraSP.distTo(t));
            if (dijkstraSP.hasPathTo(t)) {
                for (DirectedEdge e : dijkstraSP.pathTo(t)) {
                    StdOut.print(e + " ");
                }
            }
            StdOut.println();
        }
    }
}
8
16
4 5 0.35
4 7 0.37
5 7 0.28
0 7 0.16
1 5 0.32
0 4 0.38
2 3 0.17
1 7 0.19
0 2 0.26
1 2 0.36
1 3 0.29
2 7 0.34
6 2 0.40
3 6 0.52
6 0 0.58
6 4 0.93

对于上面tinyEWG.txt的内容所指定的图,如果要计算各点到起点0的最短路径,算法的执行轨迹为:


12.trace.JPG
  • 将顶点0加入优先队列,松弛以顶点0为起点的边,将从0指向的顶点2和4加入优先队列,将边0_>2和0->4添加到树中;
  • 从优先队列删除顶点2,将2->7添加到树中,将顶点7加入优先队列;
  • 从优先队列删除顶点4,将4->5添加到树中,但因为4->7松弛失败,这条边失效,将顶点5加入优先队列;
  • 从优先队列删除顶点7,将7->3添加到树中,7->5松弛失败,将顶点3加入优先队列;
  • 从优先队列删除顶点5,将5->1添加到树中,5->7松弛失败,将顶点5加入优先队列;
  • 从优先队列删除顶点3,将3->6添加到树中,将顶点6加入优先队列;
  • 从优先队列删除顶点6,优先队列为空,执行完毕。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 一、定义 在一幅加权有向图中,最短路径是指从顶点s到顶点t的最短路径是所有从s到t的路径中的权重最小者。求解最短路...
    null12阅读 2,437评论 0 4
  • 最短路径是指连接图中两个顶点的路径中,所有边构成的权值之和最小的路径。之前提到的广度优先遍历图结构,其实也是一种计...
    zhipingChen阅读 60,229评论 9 38
  • 现实生活中有很大一类问题可以用简洁明了的图论语言来描述,可以转化为图论问题。 相关定义 图可以表示为G=(V, E...
    芥丶未央阅读 1,650评论 0 7
  • 数据结构与算法--最短路径之Dijkstra算法 加权图中,我们很可能关心这样一个问题:从一个顶点到另一个顶点成本...
    sunhaiyu阅读 1,347评论 0 1
  • 图是一种比线性表和树更复杂的数据结构,在图中,结点之间的关系是任意的,任意两个数据元素之间都可能相关。图是一种多对...
    Alent阅读 2,281评论 1 22