求朋友圈的个数 Friend Circles

问题:

There are N students in a class. Some of them are friends, while some are not. Their friendship is transitive in nature. For example, if A is a direct friend of B, and B is a direct friend of C, then A is an indirect friend of C. And we defined a friend circle is a group of students who are direct or indirect friends.

Given a N*N matrix M representing the friend relationship between students in the class. If M[i][j] = 1, then the ith and jth students are direct friends with each other, otherwise not. And you have to output the total number of friend circles among all the students.

Example 1:

Input: 
[[1,1,0],
 [1,1,0],
 [0,0,1]]
Output: 2
Explanation:The 0th and 1st students are direct friends, so they are in a friend circle. 
The 2nd student himself is in a friend circle. So return 2.

Example 2:

Input: 
[[1,1,0],
 [1,1,1],
 [0,1,1]]
Output: 1
Explanation:The 0th and 1st students are direct friends, the 1st and 2nd students are direct friends, 
so the 0th and 2nd students are indirect friends. All of them are in the same friend circle, so return 1.

Note:

  1. N is in range [1,200].
  2. M[i][i] = 1 for all students.
  3. If M[i][j] = 1, then M[j][i] = 1.

解决:

① 求朋友圈个数,可以理解为图的遍历。dfs。对于某个人,遍历其好友,然后再遍历其好友的好友,那么我们就能把属于同一个朋友圈的人都遍历一遍,我们同时标记出已经遍历过的人,然后累积朋友圈的个数,再去对于没有遍历到的人再找其朋友圈的人,这样就能求出个数。

class Solution { //13ms
    public int findCircleNum(int[][] M) {
        int len = M.length;
        int res = 0;
        boolean[] isvisited = new boolean[len];
        for (int i = 0;i < len;i ++){
            if (isvisited[i]) continue;
            dfs(M,i,isvisited);
            res ++;
        }
        return res;
    }
    public void dfs(int[][] M,int i,boolean[] isvisited){
        isvisited[i] = true;
        for (int j = 0;j < M.length;j ++){
            if (M[i][j] == 0 || isvisited[j]) continue;
            dfs(M,j,isvisited);
        }
    }
}

进化版:

class Solution { //11ms
    public int findCircleNum(int[][] M) {
        int res = 0;
        int[] isvisited = new int[M.length];
        for (int i = 0; i < M.length; i ++) {
            if (isvisited[i] == 0) {
                dfs(M, isvisited, i);
                res ++;
            }
        }
        return res;
    }
    public void dfs(int[][] M, int[] isvisited, int i) {
        for (int j = 0; j < M.length; j ++) {
            if (isvisited[j] == 0 && M[i][j] == 1) {
                isvisited[j] = 1;
                dfs(M, isvisited, j);
            }
        }
    }
}

② BFS遍历。

class Solution {//34ms
    public int findCircleNum(int[][] M) {
        int len = M.length;
        int res = 0;
        boolean[] isvisited = new boolean[len];
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0;i < len;i ++){
            if (isvisited[i]) continue;
            queue.offer(i);
            while(! queue.isEmpty()){
                int tmp = queue.poll();
                isvisited[tmp] = true;
                for (int j = 0;j < len;j ++){
                    if (M[tmp][j] == 0 || isvisited[j]) continue;
                    queue.offer(j);
                }
            }
            res ++;
        }
        return res;
    }
}

③  使用并查集,核心思想是初始时给每一个对象都赋上不同的标签,然后对于属于同一类的对象,在root中查找其标签,如果不同,那么将其中一个对象的标签赋值给另一个对象,注意root数组中的数字跟数字的坐标是有很大关系的,root存的是属于同一组的另一个对象的坐标,这样通过getRoot函数可以使同一个组的对象返回相同的值。
class Solution { //17ms
    public int findCircleNum(int[][] M) {
        if (M == null || M.length == 0 || M.length != M[0].length){
            return 0;
        }
        int len = M.length;
        int res = len;
        int[] root = new int[len];
        for (int i = 0;i < len;i ++){
            root[i] = i;
        }
        for (int i = 0;i < len;i ++){
            for (int j = 0;j < len;j ++){
                if (i != j && M[i][j] == 1){
                    int p1 = findRoot(root,i);
                    int p2 = findRoot(root,j);
                    if (p1 != p2){
                        res --;
                        root[p2] = p1;
                    }
                }
            }
        }
        return res;
    }
    public int findRoot(int[] root,int i){
        while(root[i] != i){
            root[i] = root[root[i]];
            i = root[i];
        }
        return i;
    }
}

并查集Union Find:顾名思义就是有“合并集合”和“查找集合中的元素”两种操作的关于数据结构的一种算法。并查集算法不支持分割一个集合。

算法:

  • 用集合中的某个元素来代表这个集合,该元素称为集合的代表元
  • 一个集合内的所有元素组织成以代表元为根的树形结构
  • 对于每一个元素 parent[x]指向x在树形结构上的父亲节点。如果x是根节点,则令parent[x] = x。
  • 对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元。可以沿着parent[x]不断在树形结构中向上移动,直到到达根节点。
  • 判断两个元素是否属于同一集合,只需要看他们的代表元是否相同即可

 路径压缩:

  • 为了加快查找速度,查找时将x到根节点路径上的所有点的parent设为根节点,该优化方法称为压缩路径。
  • 使用该优化后,平均复杂度可视为Ackerman函数的反函数,实际应用中可粗略认为其是一个常数。

用途:

  • 维护无向图的连通性。支持判断两个点是否在同一连通块内,和判断增加一条边是否会产生环。
  • 用在求解最小生成树的Kruskal算法里。 

三个操作

  • 初始化 :包括对所有单个的数据建立一个单独的集合(即根据题目的意思自己建立的最多可能有的集合,为下面的合并查找操作提供操作对象)
    • 在每一个单个的集合里面,有三个东西:
      • 集合所代表的数据。(这个初始值根据需要自己定义,不固定)
      • 这个集合的层次通常用rank表示(一般来说,初始化的工作之一就是将每一个集合里的rank置为0)。
      • 这个集合的类别parent(有的人也喜欢用set表示)(其实就是一个指针,用来指示这个集合属于哪一类,合并过后的集合,他们的parent指向的最终值一定是相同的。)
      • (**有的简单题里面集合的数据就是这个集合的标号,也就是说只包含2和3,1省略了)。
      • 初始化的时候,一个集合的parent都是这个集合自己的标号。没有跟它同类的集合,那么这个集合的源头只能是自己了。

      • (最简单的集合就只含有这三个东西了,当然,复杂的集合就是把3指针这一项添加内容,如PKU食物链那题,我们还可以添加enemy指针,表示这个物种集合的天敌集合;food指针,表示这个物种集合的食物集合。随着指针的增加,并查集操作起来也变得复杂,题目也就显得更难了)

    •    结构体表示法:
    • #define MAX 10000
      struct Node
      {
          int data;
          int rank;
          int parent;
       }node[MAX];
       数组表示法:
      int set[max];//集合index的类别,或者用parent表示
      int rank[max];//集合index的层次,通常初始化为0
      int data[max];//集合index的数据类型
      
      //初始化集合
      void Make_Set(int i)
      {
          set[i]=i;//初始化的时候,一个集合的parent都是这个集合自己的标号。没有跟它同类的集合,那么这个集合的源头只能是自己了。
          rank[i]=0;
      }
    • 一般来说,题目简单用数组,题目复杂用结构体,因为结构体有条理,数组可以少打几个字。

  • 查找函数:就是找到parent指针的源头,可以把函数命名为get_parent(或者find_set,这个随你喜欢,以便于理解为主)
    • 如果集合的parent等于集合的编号(即还没有被合并或者没有同类),那么自然返回自身编号。
    • 如果不同(即经过合并操作后指针指向了源头(合并后选出的rank高的集合))那么就可以调用递归函数,如下面的代码:
      /**
      *查找集合i(一个元素是一个集合)的源头(递归实现)。
       如果集合i的父亲是自己,说明自己就是源头,返回自己的标号;
       否则查找集合i的父亲的源头。
      **/
      int get_parent(int x)
      {
          if(node[x].parent==x)
              return x;
          return get_parent(node[x].parent);
      }
    • 数组的话就是:
      //查找集合i(一个元素是一个集合)的源头(递归实现)
      int Find_Set(int i)
      { 
          //如果集合i的父亲是自己,说明自己就是源头,返回自己的标号
         if(set[i]==i)
             return set[i];
          //否则查找集合i的父亲的源头
          return  Find_Set(set[i]);        
      }
  • 合并集合函数:

    

 

  • 先看代码:    
    void Union(int a,int b)
    {
        a=get_parent(a);
        b=get_parent(b);
        if(node[a].rank>node[b].rank)
            node[b].parent=a;
        else
        {    
            node[a].parent=b;
            if(node[a].rank==node[b].rank)
                node[b].rank++;
        }
    }
  • 再给出数组显示的合并函数:
    void Union(int i,int j)
    {
        i=Find_Set(i);
        j=Find_Set(j);
        if(i==j) return ;
        if(rank[i]>rank[j]) set[j]=i;
        else
        {
            if(rank[i]==rank[j]) rank[j]++;   
            set[i]=j;
        }
    }

https://segmentfault.com/a/1190000004023326 

转载于:https://my.oschina.net/liyurong/blog/1605049

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值