並查集 & 拓撲排序 - [547. 朋友圈]

並查集 & 拓撲排序 - 547. 朋友圈

班上有N名學生。其中有些人是朋友,有些則不是。他們的友誼具有是傳遞性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那麼我們可以認爲 A 也是 C 的朋友。所謂的朋友圈,是指所有朋友的集合。

給定一個N * N的矩陣 M,表示班級中學生之間的朋友關係。如果M[i][j] = 1,表示已知第 i 個和 j 個學生互爲朋友關係,否則爲不知道。你必須輸出所有學生中的已知的朋友圈總數。

示例 1:

輸入: 
[[1,1,0],
[1,1,0],
[0,0,1]]
輸出: 2 
說明:已知學生0和學生1互爲朋友,他們在一個朋友圈。
第2個學生自己在一個朋友圈。所以返回2。

示例 2:

輸入: 
[[1,1,0],
[1,1,1],
[0,1,1]]
輸出: 1
說明:已知學生0和學生1互爲朋友,學生1和學生2互爲朋友,所以學生0和學生2也是朋友,所以他們三個在一個朋友圈,返回1。

注意

N 在[1,200]的範圍內。
對於所有學生,有M[i][i] = 1
如果有M[i][j] = 1,則有M[j][i] = 1

一. 知識要點

連通分量

  1. 定義:不連通的圖是由2個或者2個以上的連通子圖組成的。這些不相交的連通子圖稱爲圖的連通分量。比如下圖中有四個連通分量
    在這裏插入圖片描述

拓撲排序方式

  1. BFS - 廣度優先搜索
    在這裏插入圖片描述

  2. DFS - 深度優先搜索
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lAIeZzGq-1574245242449)(en-resource://database/6736:1)]

並查集

  1. 定義:就是有“合併集合”和“查找集合中的元素”兩種操作的關於數據結構的一種算法。 容易理解的解釋江湖中的並查集
  2. 作用
    1. 網絡連接判斷
    2. 間接好友關係判斷
  3. api設計
    1. union(p,q); : 合併p、q兩點使他們兩個連通.
    2. find(p); : 找到節點q的連通性,(處在什麼狀態合誰聯通)
    3. isConnected(p,q); : 通過find的api,我們可以找到兩個節點是否會連通的,即api
  4. 實現方式
    1. 快速查找:Quick-Find
    2. 快速合併:Quick-Union
    3. 加權快速合併:Weighted Quick-Union
    4. 路徑壓縮:Weighted Quick-Union With Path Compression
  5. 時間複雜度比較
方式 union複雜度 find複雜度
Quick-Find O(n) O(1)
Quick-Union 樹的高度 樹的高度
Weighted Quick-Union O(lgn) O(lgn
Weighted Quick-Union With Path Compression O(1) O(1)

二.實現思路

本題有兩種實現方式

  1. 通過 拓撲排序DFS方式 判斷圖中 連通分量 的數量
  2. 通過 並查集 判斷圖中 連通分量 的數量

DFS(深度優先遍歷)

  1. 初始化
    1. 被訪問數組 visited :默認每個節點都爲0
    2. 連通分量個數count:默認爲0
    3. 循環從每個沒被訪問過 的節點進行dfs
      1. 如果被訪問過,則將這個節點的visited變爲1
      2. 每次dfs。count++

並查集:

  1. 初始化(默認每個節點和其他節點都沒有連線)
    1. 連通分量個數 count:分量數目爲節點數目
    2. 節點父節點數組 parent:所有父節點默認是自己
    3. 所在樹的樹深度數組 rank:所有樹深度爲1
  2. 循環判斷節點是否是鄰接節點(是否有臨邊)
    1. 如果是鄰接節點就調用union方法,並將節點傳入方法
      1. 查找(找父親,換父親,本質上更新 parent數組):
        1. 第一次:如果父節點不是當前節點,則做路徑壓縮。把當前節點指向 爺爺 節點
        2. 然後從當前節點向上循環。直到到根節點停止,並將父節點返回
      2. 合併(將父親不同的樹拼到一起):
        1. 如果兩個節點的父節點不同,則將短的樹直接合併到長的樹上
        2. 每次合併之後,把 連通分量 減一

三.代碼實現

DFS

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

並查集

class UnionFind {
    /**
     * 連通分量的個數
     */
    private int count;
    private int[] parent;
    /**
     * 以索引爲 i 的元素爲根結點的樹的深度(最深的那個深度)
     */
    private int[] rank;

    public UnionFind(int n) {
        this.count = n;
        this.parent = new int[n];
        this.rank = new int[n];
        for (int i = 0; i < n; i++) {
            this.parent[i] = i;
            // 初始化時,所有的元素只包含它自己,只有一個元素,所以 rank[i] = 1
            this.rank[i] = 1;
        }
    }

    public int getCount() {
        return this.count;
    }

    public int find(int p) {
        // 在 find 的時候執行路徑壓縮

        //第一種:部分壓縮,速度快但是壓縮不徹底
        while (p != this.parent[p]) {
            // 兩步一跳完成路徑壓縮
            this.parent[p] = this.parent[this.parent[p]];
            p = this.parent[p];
        }

        //第二種:全部壓縮,速度稍慢但是壓縮徹底,每個元素直接指向根節點
        //if (p!=this.parent[p])
        //    this.parent[p]=find(this.parent[p]);
        //return this.parent[p];

        return p;
    }

    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot == qRoot) {
            return;
        }
        // 元素rank少的,合併到元素多的
        if (rank[pRoot] > rank[qRoot]) {
            parent[qRoot] = pRoot;
        } else if (rank[pRoot] < rank[qRoot]) {
            parent[pRoot] = qRoot;
        } else {
            parent[qRoot] = pRoot;
            rank[pRoot]++;
        }
        // 每次 union 以後,連通分量減 1
        count--;
    }
}

public class Solution {

    public int findCircleNum(int[][] M) {
        int len = M.length;
        UnionFind uf = new UnionFind(len);
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < i; j++) {
                if (M[i][j] == 1) {
                    uf.union(i, j);
                }
            }
        }
        return uf.getCount();
    }

    public static void main(String[] args) {
        int[][] M = {{1, 1, 0},
                {1, 1, 0},
                {0, 0, 1}};
        Solution solution = new Solution();
        int res = solution.findCircleNum(M);
        System.out.println(res);
    }
}

附:四種並查集實現方式(轉)

  1. 快速查找:Quick-Find
/**
 * 簡單的數組並查集
 *      通過數組來維護區域是否連通,相同區域id的數據連通
 *      find時間複雜度爲O(1)
 *      Union時間複雜度爲O(n)
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS01 {
    //用一個數組來表示節點的連通性,有相同id內容的節點是連通的
    private int[] mIds;
    //節點個數
    private int mcount;

    /**
     * 初始化狀態,並設置每個節點互不連通
     * @param capcity
     */
    public UFS01(int capcity){
        mIds = new int[capcity];
        mcount = capcity;
        for (int i = 0; i < capcity; i++) {
            mIds[i] = i;//各自節點的id都不一樣
        }
    }

    /**
     * 返回當前節點的連通id
     * @param p
     * @return
     */
    public int find(int p){
        if (p<0 || p>=mcount){
            throw new RuntimeException("越界嘍");
        }
        return mIds[p];
    }

    /**
     * 判斷a,b節點是否連通
     * @param a
     * @param b
     * @return
     */
    public boolean isConnect(int a, int b){
        return find(a)==find(b);
    }

    /**
     * 連通a,b節點
     *      聯合的整體思路:
     *          要麼把a索引在mIds中的狀態變成b的,
     *          要麼把b索引在mIds中的狀態變成a的
     * @param a
     * @param b
     */
    public void union(int a, int b){
        int aId = find(a);
        int bId = find(b);

        //如果已經連通,就不管了
        if (aId == bId){
            return;
        }

        //將bId的全部變成aId,需要將每個節點的id都變過來的
        for (int i = 0; i < mIds.length; i++) {
            if (mIds[i] == bId){
                mIds[i] = aId;
            }
        }
    }
}

  1. 快速合併:Quick-Union
/**
 * 類似樹的並查集
 *      通過指向父節點的指針來維護區域是否連通
 *      時間複雜度不定,如果組成了線性的樹,時間複雜度偏高。
 *
 *      可以改進的方向:維護每個節點的下面層數 或者 子節點 個數,union的時候,將個數少的節點連接到個數多的節點上面
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS02 {
    /**
     * 維護指向父節點的指針
     */
    private int[] mParents;
    private int mCount;

    /**
     * 初始化數組,默認每個節點都是區域頭節點,即指針指向自己
     * @param capacity
     */
    public UFS02(int capacity){
        mCount = capacity;
        mParents = new int[capacity];
        for (int i = 0; i < capacity; i++) {
            mParents[i] = i;
        }
    }

    /**
     * 查找P節點的區域頭結點
     * @param p
     * @return
     */
    public int find(int p){
        if (p<0 || p>=mCount){
            throw new RuntimeException("越界嘍");
        }
        /**
         * 向上查找,直到是一個區域頭結點
         */
        while (p != mParents[p]){
            p = mParents[p];
        }
        return p;
    }

    public boolean isConnect(int a, int b){
        return find(a)==find(b);
    }

    /**
     * 聯合,將a,b節點的區域頭結點聯合即可
     * @param a
     * @param b
     */
    public void union(int a, int b){
        int aRoot = find(a);
        int bRoot = find(b);
        if (aRoot == bRoot){
            return;
        }
        mParents[bRoot] = aRoot;
    }
}
  1. 加權快速合併:Weighted Quick-Union
/**
 * 可以改進的方向:維護每個節點的子節點 個數,union的時候,將個數少的節點連接到個數多的節點上面
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS04 {
    private int[] mParents;
    //新加一個數組用來記錄每一個節點,以它爲根的元素的個數。
    //mSize[i]表示以i爲根的樹結構中的元素個數。
    private int[] mSize;
    private int mCount;

    public UFS04(int capacity){
        mCount = capacity;
        mParents = new int[mCount];
        mSize = new int[mCount];
        for (int i = 0; i < mCount; i++) {
            mParents[i] = i;
            //默認每個都是1:獨立的時候含有一個元素.
            mSize[i] = 1;
        }
    }
    //以下find和isConnected都用不到mSize.
    public int find(int p){
        if( p<0 || p>=mCount){
            //...做一些異常處理
        }
        while(p!=mParents[p]){
            p = mParents[p];
        }
        return p;
    }
    public boolean isConnected(int p,int q){
        return find(p)==find(q);
    }
    //聯合的時候就需要用到mSize了.看看那個節點爲根的樹形集合中元素多,
    //然後把少的那個節點對應的根,指向多的那個節點對應的根。
    public void union(int p,int q){
        //前兩步不變
        int pRoot= find(p);
        int qRoot = find(q);
        if(pRoot == qRoot){
            return;
        }
        int pSize = mSize[pRoot];//初始事都是根,爲1
        int qSize = mSize[qRoot];
        //如果pRoot爲根的樹形集合含有的元素比qRoot的多
        if(pSize > qSize){
            //注意是少的索引的父節點指向多的
            mParents[qRoot] = pRoot;
            //注意此時mSize的改變,由於qRoot歸併到了pRoot當中那麼
            //需要加上相應數量的size,注意qRoot對應的size並沒有改變
            mSize[pRoot] = pSize+qSize;
        }/*else if(pSize < qSize){//同理
            mParents[pRoot] = qRoot;
            mSize[qRoot] = pSize+qSize;
        }else{//如果兩個相等那麼就無所謂了,誰先合併到誰都可以.
            mParents[qRoot] = pRoot;
            mSize[pRoot] = pSize+qSize;
        }*/
        //然後就可以把等於的合入到大於或者小於的裏面.
        else{//此處把小於和等於合到一塊
            mParents[pRoot] = qRoot;
            mSize[qRoot] = pSize+qSize;
        }
    }
}

  1. 路徑壓縮:Weighted Quick-Union With Path Compression
/**
 * 可以改進的方向:維護每個節點的下面層數,union的時候,將個數少的節點連接到個數多的節點上面
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS05 {
    private int[] mParents;
    //mRank[i]表示以i爲根節點的集合所表示的樹的層數
    private int[] mRank;
    private int mCount;
    public UFS05(int capacity){
        mCount = capacity;
        mParents = new int[mCount];
        mRank = new int[mCount];
        for (int i = 0; i < mCount; i++) {
            mParents[i] = i;
            //默認每個都是1:表示深度爲1層
            mRank[i] = 1;
        }
    }
    //以下find和isConnected都用不到mRank.
    public int find(int p){
        if( p<0 || p>=mCount){
            //...做一些異常處理
        }
        while(p!=mParents[p]){
            p = mParents[p];
        }
        return p;
    }
    public boolean isConnected(int p,int q){
        return find(p)==find(q);
    }
    //找到p、q節點所在的樹形集合的根節點,它的深度。然後把深度小的根節點合入到深度大的根節點當中
    public void union(int p,int q){
        //前兩步不變
        int pRoot= find(p);
        int qRoot = find(q);
        if(pRoot == qRoot){
            return;
        }
        int pRank = mRank[pRoot];//初始事都是深度爲1
        int qRank= mRank[qRoot];
        //如果p的深度比q的深度大.
        if(pRank > qRank){
            //注意是小的指向大的,也就是爲小的重新讀之
            mParents[qRoot] = pRoot;
            //此時把並不需要維護pRank,因爲qRank是比pRank小的
            //也就是q更淺,它不會增加p的深度,只會增加去p的寬度
        }else if(pRank < qRank){
            mParents[pRoot] = qRoot;
            //同樣的道理不需要維護qRank,p只會增加它的寬度
        }else{
            //當兩個深度相同的時候,誰指向誰都可以,但是注意此時的深度維護
            //被指向的那個的深度需要加1.
            //此時讓qRoot指向pRoot吧.
            mParents[qRoot] = pRoot;
            mRank[pRoot]++;
        }
    }
}

測試用例圖
在這裏插入圖片描述

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