Spark GraphX之ConnectedComponents

图的连通性介绍

无向图

  • 顶点的连通性
    在一个无向图 G 中,若从顶点 i 到顶点 j 有路径相连(当然从 j 到 i 也一定有路径),则称 i 和 j 是连通的。
  • 连通图
    在一个无向图 G 中,如果图中任意两点都是连通的,那么图被称作连通图
  • 连通分量
    无向图G的极大连通子图称为G的连通分量( Connected Component),这里的极大是指顶点个数极大。任何连通图的连通分量只有一个,即是其自身,非连通的无向图有多个连通分量

有向图

  • 弱连通图
    有向图的底图(无向图)是连通图,则是弱连通图。简单来说就是把所有有向边变成无向边,若此时图连通,则是弱连通图。
  • 单向连通图
    有向图中,任意结点对中,至少从一个到另一个是可达的,就是单向连通。如下图,A能够到C,但C不能到A

在这里插入图片描述

  • 强连通图
    有向图中,强连通图是任意对中都互相可达。
  • 强连通分量
    有向图的极大强连通子图,称为强连通分量。对于一个有向图,它不一定是强连通的,但可以分为几个极大的强连通子图。
    下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量
    在这里插入图片描述

ConnectedComponents

简介

ConnectedComponents即连通分量。Graphx用图中顶点的id来标识节点所属的连通体,同一个连通体的编号是采用该联通体中最小的节点id来标识的。
寻找连通图在一些场景中是图计算的核心应用。比如:以关键词集合识别集群。以每个定点表示每一项(ltem),以边代表它们之间的联系,或者认为他们之间具有相似性。

核心思想

用节点的id来表示连通分量,将自身id传递给邻居节点(即一个Edge两端的节点),能够发送消息的在同一个连通分量中。消息的内容是将节点id发送给比当前id大的id,这样就能够保证同一连通分量中的分支id是最小的,同时发送消息部分方向。
过程如下:

  • ①首先初始化图,将图中顶点id作为顶点的属性,开始状态是每个节点单独作为一个连通分量,分量id是节点id;
  • ②对于每条边,如果边两端节点属性相同(说明两个节点位于同一连通分量中),不需要发送消息,否则将较小的属性发送给较大属性的节点;
  • ③同一个节点对于收到的多个消息,只接收最小的消息;
  • ④节点将自身属性记录的id与收到的消息中的id进行比较,采用最小的id更新自己的属性。
  • 迭代②③④

源码

object ConnectedComponents {
  /**
   *  返回一个图,图中节点的属性是当前连通分量中最小的顶点id
   * */
  def run[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED], maxIterations: Int): Graph[VertexId, ED] = {
  	// 规定最大迭代次数必须大于0
    require(maxIterations > 0, s"Maximum of iterations must be greater than 0," +
      s" but got ${maxIterations}")

    // 初始化图:将图中顶点的id作为顶点属性
    val ccGraph = graph.mapVertices { case (vid, _) => vid }
    // 一条边上两个顶点,将id较小的顶点的属性发送给id较大的顶点,目的是使最终连通分支的id属性为最小的节点id
    // 如果边的两个顶点属性相同,则说明已经在同一个连通分支,不需要发送消息
    def sendMessage(edge: EdgeTriplet[VertexId, ED]): Iterator[(VertexId, VertexId)] = {
      if (edge.srcAttr < edge.dstAttr) {
        Iterator((edge.dstId, edge.srcAttr))
      } else if (edge.srcAttr > edge.dstAttr) {
        Iterator((edge.srcId, edge.dstAttr))
      } else {
        Iterator.empty
      }
    }
    // 初始化消息,初始给定一个最大值,然后较大的节点id在接收到较小节点id时能够更新自己的属性
    val initialMessage = Long.MaxValue
    // Pregel是另一个算法详情见代码结束下方链接
    val pregelGraph = Pregel(ccGraph, initialMessage,
      maxIterations, EdgeDirection.Either)(
      // 比较当前属性值和接受到的id,取小的并更新属性
      vprog = (id, attr, msg) => math.min(attr, msg), 
      sendMsg = sendMessage,
      // 合并消息,接受多条消息时,先比较取最小的,在进行更新属性操作
      mergeMsg = (a, b) => math.min(a, b))
    // 用于取消缓存            
    ccGraph.unpersist()
    pregelGraph
  } // end of connectedComponents

   // 当迭代次数没写时,代用该方法默认传入int类型的最大值
  def run[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED]): Graph[VertexId, ED] = {
    run(graph, Int.MaxValue)
  }
}

案例

数据准备

people.csv

4,Dave,25
6,Faith,21
8,Harvey,47
2,Bob,18
1,Alice,20
3,Charlie,30
7,George,34
9,Ivy,21
5,Eve,30
10,Lily,35
11,Helen,35
12,Ann,35

links.csv

4,Dave,25
6,Faith,21
8,Harvey,47
2,Bob,18
1,Alice,20
3,Charlie,30
7,George,34
9,Ivy,21
5,Eve,30
10,Lily,35
11,Helen,35
12,Ann,35

图结构如下
在这里插入图片描述
代码:

import org.apache.spark.graphx.{Edge, Graph, VertexId}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

// 定义一个Person样例类
case class Person(name: String, age: Int)
object ConnectedComponents {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("ConnectedComponentsDemo").setMaster("local[*]")
    val sc = new SparkContext(conf)

    val people = sc.textFile("in/people.csv")
    // vertices
    val peopleRDD: RDD[(VertexId, Person)] = people.map(line => line.split(","))
      .map(row => (row(0).toLong, Person(row(1), row(2).toInt)))

    val links = sc.textFile("in/links.csv")
    // edges
    val linksRDD: RDD[Edge[String]] = links.map({line =>
      val row = line.split(",")
      Edge(row(0).toInt, row(1).toInt, row(2))
    })

    val peopleGraph: Graph[Person, String] = Graph(peopleRDD, linksRDD)
    // spark graphX已经封装了connectedComponents,直接使用即可
    val cc = peopleGraph.connectedComponents()
    // 按照连通分量id排序输出
    cc.vertices.collect().sortBy(x=>x._2).foreach(println)
  }
}

在这里插入图片描述
有结果可见,新的graph会将原来的graph的属性丢失,若想要显示,可以join原来的graph

import org.apache.spark.graphx.{Edge, Graph, VertexId}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

// 定义一个Person样例类
case class Person(name: String, age: Int)
object ConnectedComponents {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("ConnectedComponentsDemo").setMaster("local[*]")
    val sc = new SparkContext(conf)


    val people = sc.textFile("in/people.csv")
    // vertices
    val peopleRDD: RDD[(VertexId, Person)] = people.map(line => line.split(","))
      .map(row => (row(0).toLong, Person(row(1), row(2).toInt)))

    val links = sc.textFile("in/links.csv")
    // edges
    val linksRDD: RDD[Edge[String]] = links.map({line =>
      val row = line.split(",")
      Edge(row(0).toInt, row(1).toInt, row(2))
    })

    val peopleGraph: Graph[Person, String] = Graph(peopleRDD, linksRDD)
    // spark graphX已经封装了connectedComponents,直接使用即可
    val cc = peopleGraph.connectedComponents()
    // 与原来的图join,cc代表新图的属性,p_id代表就图的属性(Person)
    val newGraph = cc.outerJoinVertices(peopleRDD)((id, cc_id, p_id)=>(cc_id,p_id.get.name,p_id.get.age))

    cc.vertices.map(_._2).collect.distinct.foreach( // 获取连通分量的id
      id =>{
        // subgraph用于截取满足条件的子图
        val sub = newGraph.subgraph(vpred = (id1, id2) => id2._1==id)
        println(sub.triplets.collect().mkString(","))
    })
  }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值