強連通體的GraphX的實現和解析

/**連通體*/
object ConnectComponents {

  /**在圖中若從某個頂點Vp出發,沿着一些邊經過頂點V1,V2,。。。Vm到達Vg則稱頂點序列(Vp,V1,V2....Vm,Vg)爲從Vp到Vg的路徑,其中Vp是路徑的起始點,Vg爲路徑終點。
   * 路徑上的邊的數目成爲該路徑的長度
   * 連通:在無向圖中,若從頂點Vi到頂點Vj之間有路徑稱爲這兩個頂點是連通的。
   * 連通圖:若圖中任意一對頂點之間都是連通的,則稱爲此圖爲連通圖。
   * 連通分量:非連通圖中的每個連通部分稱爲連通分量
   * 強連通:對於有向圖,若從頂點Vi到頂點Vj到頂點Vi之間都有路徑,則稱這兩頂點是強連通的。
   * 強連通圖:若有向圖中任何一對頂點都是強連通的,則此圖爲強連通圖。
   *
   * 性質:
   * 無向連通圖,則邊E的數目大於等於頂點V的數目減1:|E| >= |V| - 1,反之不成立;
   * 有向強連通圖的必要條件是邊的數目大於等於頂點的數目:|E| >= |V|,反之不成立。
   * 沒有迴路的無向圖是連通的當且它是樹,即等價於:|E| = |V| - 1
   * */
  def conCompts(sc: SparkContext): Unit ={

    val graph = Graph(sc.makeRDD((1L to 7L).map((_,""))),
      sc.makeRDD(Array(Edge(2L,5L,"配偶"),Edge(5L,3L,"親戚"),Edge(3L,2L,"子女"),Edge(4L,5L,"父母"),Edge(6L,7L,"親戚")))
    )

    graph
      .connectedComponents()
      .vertices
      .map(_.swap)
      .groupByKey().map(_._2)
      .foreach(println)
  }

  /**強連通:
   * (1)對圖中所有節點設定初始連通分支id,用自己的節點id作爲所屬連通分支的id,並將所有節點打上初始標記false;
   * (2)首先做循環,將只有出邊或入邊的節點標記爲true,將 **只存在單向邊的或者孤立的節點** 和 **已經
   * 確認且打好標記的強連通分量中的節點**(即被標記爲true的節點)從圖中去除;
   * (3)爲圖中節點正向着色,先用節點id爲自身着色,之後沿着出邊向鄰居節點發送自己的着色id(只有較小的着色id向較大的着色id的節點發送消息)。
   * (4)爲着色完成的圖中節點反向打標籤(是否完成連通分支id標記)。在着色完成的圖中,節點id與節點所在連通分支id相同時表明
   * 該節點是着色的root節點,標記爲true。若一個節點對應的入邊的另外一個節點是true,則該節點也被標記爲true。
   * 節點沿着入邊由src節點向dst節點發送自身標記情況,只有收到true的消息則節點便標記爲true。(只有着色相同,
   * 且一條邊上dst節點——發消息者是true但是src節點——收消息者是false時,dst節點纔會向src節點發送消息)*/
  def strongConCompt(sc: SparkContext): Unit ={
    val graph = Graph(sc.makeRDD((1L to 7L).map((_,""))),
      sc.makeRDD(Array(Edge(2L,5L,"配偶"),Edge(5L,3L,"親戚"),Edge(3L,2L,"子女"),Edge(4L,5L,"父母"),Edge(6L,7L,"親戚")))
    )
    val strongConCpt: Graph[VertexId, String] = graph.stronglyConnectedComponents(3)
    strongConCpt.vertices.foreach(println)
  }

  /**StronglyConnectedComponents源碼*/
  def strongCompt[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED], numIter: Int): Graph[VertexId, ED] = {

    // 初始化圖,將節點id作爲節點屬性,sccGraph是最後的返回結果圖
    var sccGraph = graph.mapVertices { case (vid, _) => vid }
    // 在迭代中使用的圖
    var sccWorkGraph = graph.mapVertices { case (vid, _) => (vid, false) }.cache()

    // 輔助變量prevSccGraph,用來unpersist緩存圖
    var prevSccGraph = sccGraph

    var numVertices = sccWorkGraph.numVertices
    var iter = 0
    while (sccWorkGraph.numVertices > 0 && iter < numIter) {
      iter += 1
      // 此處循環內部工作:
      // 1.第一次循環進入時:將sccWorkGrpah圖中只有單向邊的節點或者孤立節點去掉; 後面循環進入時:將sccWorkGraph圖中已經標識完成的強連通分量去掉。
      // 2.更新圖中節點所屬的強連通分支id
      // 只有在第一次進入第一層循環時,第一層循環內部的do-while循環纔會循環多次,第2次以上只會只運行一次do{}的內容,因爲後面圖中不存在單向節點了。
      do {
        numVertices = sccWorkGraph.numVertices
        sccWorkGraph = sccWorkGraph.outerJoinVertices(sccWorkGraph.outDegrees) {
          (vid, data, degreeOpt) => if (degreeOpt.isDefined) data else (vid, true)
        }.outerJoinVertices(sccWorkGraph.inDegrees) {
          (vid, data, degreeOpt) => if (degreeOpt.isDefined) data else (vid, true)
        }.cache() //得到圖中的有雙向邊的節點(vid,false), 單向邊或者孤立節點(vid,true),並且已經成功標記完連通分支的節點自身屬性便是(vid,true)

        // 拿到圖中只有單向邊的節點或孤立節點
        val finalVertices = sccWorkGraph.vertices
          .filter { case (vid, (scc, isFinal)) => isFinal}
          .mapValues { (vid, data) => data._1}


        // //外部第一次循環不會變動sccGraph節點的屬性,只有在第二次開始纔會將頂點所屬的強連通分支id更新到圖節點屬性中。
        sccGraph = sccGraph.outerJoinVertices(finalVertices) {
          (vid, scc, opt) => opt.getOrElse(scc)
        }.cache()
        sccGraph.vertices.count()
        sccGraph.edges.count()
        prevSccGraph.unpersist(blocking = false)
        prevSccGraph = sccGraph

        //只保留屬性attr._2爲false的節點(這些節點是未完成連通分量打標籤的節點,後面進入pregel重新着色)
        sccWorkGraph = sccWorkGraph.subgraph(vpred = (vid, data) => !data._2).cache()
      } while (sccWorkGraph.numVertices < numVertices)

      // 如果達到迭代次數則返回此時的sccGraph,將不再進入pregel進行下一步的着色和打標籤。
      if (iter < numIter) {
        // 初始用vid爲自身節點着色,每次重新進入pregel的圖將重新着色
        sccWorkGraph = sccWorkGraph.mapVertices { case (vid, (color, isFinal)) => (vid, isFinal) }

        sccWorkGraph = Pregel[(VertexId, Boolean), ED, VertexId](
          sccWorkGraph, Long.MaxValue, activeDirection = EdgeDirection.Out)(
          // vprog: 節點在自己所屬連通分支和鄰居所屬分支中取最小者更新自己。
          (vid, myScc, neighborScc) => (math.min(myScc._1, neighborScc), myScc._2),
          // sendMsg:正向(out)向鄰居傳播自身所屬的連通分支(只有當自己所屬連通分支比鄰居小纔會發送消息)
          e => {
            if (e.srcAttr._1 < e.dstAttr._1) {
              Iterator((e.dstId, e.srcAttr._1))
            } else {
              Iterator()
            }
          },
          // mergeMsg: 多條消息(鄰居的連通分支)取最小者
          (vid1, vid2) => math.min(vid1, vid2))

        //第二個pregel:爲着色後的節點打標籤,final表示該節點的連通分支id已經標記完成。
        sccWorkGraph = Pregel[(VertexId, Boolean), ED, Boolean](
          sccWorkGraph, false, activeDirection = EdgeDirection.In)(
          // vprog: 如果節點id和所屬連通分支id相同,則該節點是root
          //         root節點是完成連通分支標記的節點,是final (final是被標記爲true)
          //         如果節點和final節點是鄰居(收到的消息是final),則該節點也是final
          (vid, myScc, existsSameColorFinalNeighbor) => {
            val isColorRoot = vid == myScc._1
            (myScc._1, myScc._2 || isColorRoot || existsSameColorFinalNeighbor)
          },
          // 從完成着色的分量的root開始,反向(in)遍歷節點,當一條邊上兩個節點的着色不同時則不發送消息。
          e => {
            val sameColor = e.dstAttr._1 == e.srcAttr._1
            val onlyDstIsFinal = e.dstAttr._2 && !e.srcAttr._2
            if (sameColor && onlyDstIsFinal) {
              Iterator((e.srcId, e.dstAttr._2))
            } else {
              Iterator()
            }
          },
          (final1, final2) => final1 || final2)
      }
    }
    sccGraph
  }

  def createGraph[VD: ClassTag, ED: ClassTag](sc: SparkContext): Graph[String, String] ={
    val graph = Graph(sc.makeRDD((1L to 7L).map((_,""))),
      sc.makeRDD(Array(Edge(2L,5L,"配偶"),Edge(5L,3L,"親戚"),Edge(3L,2L,"子女"),Edge(4L,5L,"父母"),Edge(6L,7L,"親戚"))))
    graph
  }

  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("graphx visual").setMaster("local[2]")
    val sc = SparkContext.getOrCreate(sparkConf)
    conCompts(sc)
    val graph = createGraph(sc)
    strongCompt(graph, 3)

  }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章