Graphx 最短路徑源碼解析

1. 最短路徑測試代碼

下面主要是對Spark圖計算框架GraphX中的單源點最短路徑的源碼進行解析。

    test("shortPaths") {
        // 測試的真實結果,後面用於對比
        val shortestPaths = Set(
        (1, Map(1 -> 0, 4 -> 2)), (2, Map(1 -> 1, 4 -> 2)), (3, Map(1 -> 2, 4 -> 1)),
        (4, Map(1 -> 2, 4 -> 0)), (5, Map(1 -> 1, 4 -> 1)), (6, Map(1 -> 3, 4 -> 1)))

        // 構造有向圖的邊序列
        val edgeSeq = Seq((1, 2), (1, 5), (2, 3), (2, 5), (3, 4), (4, 5), (4, 6)).flatMap {
            case e => Seq(e, e.swap)
        }

        // 構造有向圖
        val edges = sc.parallelize(edgeSeq).map { case (v1, v2) => (v1.toLong, v2.toLong) }
        val graph = Graph.fromEdgeTuples(edges, 1)

        // 要求最短路徑的點集合
        val landmarks = Seq(1, 4).map(_.toLong)

        // 計算最短路徑
        val results = ShortestPaths.run(graph, landmarks).vertices.collect.map {
        case (v, spMap) => (v, spMap.mapValues(i => i))
        }

        // 與真實結果對比
        assert(results.toSet === shortestPaths)
    }

2. Graphx底層實現代碼

    package org.apache.spark.graphx.lib

    import org.apache.spark.graphx._
    import scala.reflect.ClassTag

    object ShortestPaths {
        // 定義一個Map[VertexId,Int]類型的Map函數,別名爲SPMap,函數的屬性Key爲VertexId類型,
        // 其實也就是scala中的Long類型,它在圖中的別名是VertexId,還有Int類型的路徑的長度。
        type SPMap = Map[VertexId, Int]

        // 初始化圖的屬性信息
        private def makeMap(x: (VertexId, Int)*) = Map(x: _*)

        // 主要用於將自身的屬性值(即源頂點屬性值)中路徑的長度加1(這裏說明該最短路徑模型只能應用與非帶權圖,即權值都相等的圖),然後和目標定點的屬性值比較
        private def incrementMap(spmap: SPMap): SPMap = spmap.map { case (v, d) => v -> (d + 1) }

        // 比較源頂點屬性和發送信息過來頂點的屬性取最小值。
        private def addMaps(spmap1: SPMap, spmap2: SPMap): SPMap =
        // 先將兩個集合spmap1和spma2的頂點整合要一起,這裏用了一個++來處理
        // 再形成一個新的k->v的map
        // 其中v是兩個消息中值最小的一個
        (spmap1.keySet ++ spmap2.keySet).map {
            k => k -> math.min(spmap1.getOrElse(k, Int.MaxValue), spmap2.getOrElse(k, Int.MaxValue))
        }.toMap

        // 計算給定了起始和終點序列的最短路徑
        // ED是邊的屬性值,計算過程中不會被使用
        // graph是要計算最短路徑的圖
        // landmarks是要求最短路徑頂點id的集合,最短路徑會計算每一個landmark
        // 返回的是一個圖,每個頂點的屬性就是landmark點間的最短路徑
        def run[VD, ED: ClassTag](graph: Graph[VD, ED], landmarks: Seq[VertexId]): Graph[SPMap, ED] = {
        val spGraph = graph.mapVertices { (vid, attr) =>
        // 如果landmark只有一個點1
        // 將landmarks中的頂點初始化爲Map(1-> 0),即自身到自身的距離爲0,其餘的頂點屬性初始化爲Map()。
        if (landmarks.contains(vid)) makeMap(vid -> 0) else makeMap()
        }

        // 定義一個initMessage它的值爲Map()
        // 作用是在Pregel第一次運行的時候,所有圖中的頂點都會接收到initMessage。
        val initialMessage = makeMap()

        // 用戶定義的頂點程序運行在每一個頂點中,負責接收進來的信息,和計算新的頂點值。
        // 在第一次迭代的時候,所有的頂點程序將會被默認的defaultMessage調用,在次輪迭代中,頂點程序只有接收到message纔會被調用。
        def vertexProgram(id: VertexId, attr: SPMap, msg: SPMap): SPMap = {
            addMaps(attr, msg)
        }


        // 該函數應用於鄰居頂點在當前迭代中接收message
        // 一旦收到通知,相對於發送該消息的點,就是目的節點,相對於收到消息的點就是源節點
        // 這個地方從源節點考慮
        def sendMessage(edge: EdgeTriplet[SPMap, _]): Iterator[(VertexId, SPMap)] = {
        // 對所有目的節點值加1

        val newAttr = incrementMap(edge.dstAttr)
        // 求得最短路徑,將源節點的值發送給所有所有的源節點,其實這裏源節點就是相鄰點的意思,換成目的節點應該也是可以的
        if (edge.srcAttr != addMaps(newAttr, edge.srcAttr)) Iterator((edge.srcId, newAttr))
        else Iterator.empty
        }

        // 調用pregel函數
        // 第一個參數列表包含配置參數初始消息、最大迭代數、發送消息的邊的方向(默認是沿邊方向出)
        // 第二個參數列表包含用戶 自定義的函數用來接收消息(vprog)、計算消息(sendMsg)、合併消息(mergeMsg)
        Pregel(spGraph, initialMessage)(vertexProgram, sendMessage, addMaps)
        }
    }

GraphX最短路徑求解中使用了Pregel模型,這是一個非常高效的圖計算模型。但目前最短路徑有如下限制:

  1. 只能用於非帶權圖(權值相等);
  2. 利用的算法是迪傑斯特拉求解最短路徑。

相關討論:[1][2][3][4]


【完】

發佈了84 篇原創文章 · 獲贊 324 · 訪問量 66萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章