Dijkstra最短路徑算法的理解與實現

Dijkstra大學的時候就學過,但最近在《Algorithms》這本書裏,看到了個頗有趣的講解,對這個算法有了新的理解,如下即是對這個算法的整理和實現。node

廣度搜索與最短距離

咱們知道,若是把全部的邊的距離當作1,廣度遍歷獲得的就是最短路徑。算法

更直觀的,咱們能夠這樣想,把節點都想成有質量的小球,節點間邊想成細線。咱們把S球拎起來,讓全部的球都天然下垂,這樣小球所在的層數,就是S球到其餘小球的最短距離了。以下圖:
圖片描述ruby

咱們能夠簡單證實下。優化

首先,廣度搜索,又被稱爲層次遍歷,就是一層一層進行搜索。當搜索到d層,全部小於d層的節點必定已經搜索完了。spa

假設在某一個時刻,全部d層的節點都已經計算正確了。那麼他們相鄰的節點的距離只有兩種狀況,一種是未知的,那麼他們同S的最短距離應爲d + 1。code

一種是已知的,距離小於等於d,根據假設,他的距離也已經計算正確了。blog

而後,咱們再考慮初始狀況。S節點的距離爲0,相鄰的節點距離都爲1,符合咱們的假設。因此經過廣度搜索獲得的距離是最短距離。隊列

這個"證實"只是用來理解這個算法的,並不去嚴謹,《算法導論》有嚴謹的證實,你們感興趣的話能夠去看看。圖片

廣度搜索到Dijkstra

既然咱們知道廣度搜索能獲得全部邊的距離爲1的節點的最短距離了。那麼廣度搜索和Dijkstra有沒有什麼關係呢?換句話說,最短路徑能不能變成廣度搜索呢?get

最短路徑和廣度搜索的區別就在於,廣度遍歷之間的節點距離只能爲1,而最短路徑計算的節點距離能夠爲任意正數。

若是咱們把全部距離都變成1會怎樣?

爲了達到這個目的,咱們把兩個幾點之間加上若是啞節點(dummy node),讓全部節點(節點+啞幾點)間的距離爲1,這樣咱們就能夠用廣度遍從來算最短距離了!

好比s同u的距離爲3,那麼咱們能夠在S和U之間加上兩個節點u1 ,u2。這樣就變成了S -> U1 -> U2 -> U,相鄰的節點間的距離爲1。以下圖:
圖片描述圖片描述

目前的算法看起來有點蠢,不過已經很接近Dijkstra了。咱們看下如何來優化它!

如何優化當前的算法嗎?

算法好玩的地方就在於,咱們能夠優化它。

首先,咱們要分析這個算法的問題。

假設從S到U的距離是10,那麼計算前99個啞節點的距離,都是沒有意義的。

若是咱們在90分鐘裏,睡一下,在第一百分鐘的時候,有一個鬧鈴把咱們叫醒,就行了!讓咱們來作這樣的一個鬧鈴!

假設咱們要求下圖的的最短距離,咱們能夠這麼作。咱們從0出發,從圖中能夠看出來,直到100或者200分鐘後,不會有任何有趣的事情發生。那咱們就設兩個鬧鈴,分別爲100分鐘、200分鐘以後響。

等到100這個鬧鈴響的時候,咱們到了1號節點,這個時候,咱們檢查1號節點的相鄰節點,發現50分鐘後,可能會到2號節點。因此咱們加入150這個鬧鈴。

150分鐘到的時候,咱們到了2號節點,因此2號的最短距離是150。這樣就獲得了全部節點的最短距離。
圖片描述

咱們的鬧鐘算法以下:

  • 講S的鬧鐘設爲0

  • 循環,直到沒有任何一個鬧鐘
    假設,下一個鬧鐘(距離最近的節點)響起響起的時間是T,到達的節點是U,那麼

    • 從S到U的距離就是T

    • 對於U的每個相鄰節點V

      • 若是v尚未鬧鐘,則將它的鬧鐘設置爲 T + l(u, v)

      • 若是v的鬧鐘,比 T + l(u,v)晚,則更新v的鬧鐘

Dijkstra實現

咱們來看下具體實現,實現算法能夠幫助咱們進一步理解算法。

class Dijkstra
  INFINITY = 1 << 32
  def initialize(weighted_graph)
    @weighted_graph = weighted_graph
  end

  def shortest_distances
    make_alarms
    decrease_alarm(0, 0)

    while v = alarm!
      @weighted_graph[v].each do |w, weight|
        if alarm(w) > alarm(v) + weight
          decrease_alarm(w, alarm(v) + weight)
        end
      end
    end

    @alarms
  end

  def make_alarms
    @vertexes_alarms = (0...vertexes_count).to_a

    @alarms = Array.new(vertexes_count, INFINITY)
  end

  def alarm(vertex)
    @alarms[vertex]
  end

  def decrease_alarm(vertex, time)
    @alarms[vertex] = time

    @vertexes_alarms.sort_by! do |vertex|
      @alarms[vertex]
    end
  end

  def alarm!
    @vertexes_alarms.delete_at(0)
  end

  def vertexes_count
    @vertexes_count ||= @weighted_graph.length
  end
end

優化

本身觀察這個算法,會發現decrease_alarm這個方法,每次都調用了sort!方法。但其實咱們每次拿出來的鬧鈴都是數值最小的那一個,用優先隊列(Priority queue)更合適。這樣能夠下降算法的複雜度,讓咱們的算法更快。

總結

算法的書有不少,但講清楚算法是怎麼來的,卻不多。《Algorithms》這本書採用的方式,很像《怎樣解題》所採用方式,頗有趣,也頗有啓發性。

同時算法最有趣的地方在於能夠一點一點改進和優化!

參考

  1. Algorithms

  2. 算法導論

  3. 怎樣解題

  4. 畫圖

相關文章
相關標籤/搜索