並查集(二)並查集的演算法應用案例上

夢想家haima 發表於 2021-07-24
演算法

直接看本文的,建議先看並查集(一)並查集的幾種實現。並查集的題在力扣上都是中等題或者難度題,這個特殊的資料結構還有一些門檻

P261. 以圖判樹

力扣第261題 這道題應該算是最適合去理解並查集的 https://leetcode-cn.com/problems/graph-valid-tree/

題目

給定從 0 到 n-1 標號的 n 個結點,和一個無向邊列表(每條邊以結點對來表示),
請編寫一個函式用來判斷這些邊是否能夠形成一個合法有效的樹結構。

示例 1:

輸入: n = 5, 邊列表 edges = [[0,1], [0,2], [0,3], [1,4]]
輸出: true

示例 2:

輸入: n = 5, 邊列表 edges = [[0,1], [1,2], [2,3], [1,3], [1,4]]
輸出: false

題意

我們首先要理解 一個合法有效的樹結構的意思,關鍵在於什麼樣的結構是一個合法有效樹結構,這裡給的邊的列表是無向的,無向也很重要,這樣你不用考慮節點遍歷需要有方向性。

  • 所有節點組成一棵樹,所有節點都會連線到一起,連線到一個頂點上
  • 是樹結構,不能有環,來研究一下環的情況
  o a              o a
 / \   樹         / \   圖 
o   o b        c o — o b

前面是樹,我們來考慮連結的點,連線點a、b,連通前它們各自的點處於的狀態是 不連通的狀態
再來看一下後面圖中的b、c的情況,b和c在沒有直接連線時,它們是不是已經處於連通的狀態了,因為它們已經通過頂點a連通了

程式碼思路:通過並查集合並所有節點,如果成樹滿足兩個條件

  • 最後只剩下一株
  • 合併兩個點之前,連個點處於不連通的狀態

這裡我們拿之前最後一種路徑壓縮的,之前講過的五種方法中,任意選一種都可以,只是通常都選擇後面兩種效能較好的。同時注意一下,這裡我們還在並查集的類中加入了一個成員變數plant,把並查集看做多棵樹的話,最開始所有節點沒有合併,樹的總數為元素個數size,合併一次,樹就減少一棵,所以join方法中plant--;

並查集類

public class UFRankUnionCompressPath {


    int[] parent;
    int[] rank;//樹的層數
    int plant;  //一共有多少株樹

    public UFRankUnionCompressPath(int size) {
        plant = size;
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

    private int findP(int x) {//查詢操作,其實是查詢  根節點
        if (x < 0 || x >= parent.length)
            return -1;  //或者直接丟擲異常

        while (parent[x] != parent[parent[x]])
        //如果parent[x]==parent[parent[x]] 說明這顆樹到了第二層,或者第一層
        //第一層 因為parent[x]=x所以有  parent[x]==parent[parent[x]]
        //第二層 因為第二層的parent是根節點 有: parent[x]= root  
        //所以有 parent[parent[x]]= parent[root]  而本身 parent[root]=root
        {
            // x = parent[x];  //
            parent[x] = parent[parent[x]]; //將下層節點往頂層提升,最終
        }
        return parent[x];
    }


    public void join(int a, int b) {
        int ap = findP(a);
        int bp = findP(b);
        if (ap != bp) {
            plant--;
            //a的層數越高,就將層數少的合併到層數高的上面
            if (rank[ap] > rank[bp])
                parent[bp] = ap;
            else if (rank[ap] < rank[bp]) {
                parent[ap] = bp;
            } else {
                //相同情況的話,隨便就可以
                parent[ap] = bp;
                rank[bp]++;
            }
        }
    }

    public int getPlant() {
        return plant;
    }

    public boolean isJoined(int a, int b) { //兩個節點是否是  連線的
        return findP(a) == findP(b);
    }
}

具體解題實現

class Solution {
    public boolean validTree(int n, int[][] edges) {
            UFRankUnionCompressPath uf = new UFRankUnionCompressPath (n);
            for (int[] edge : edges) {
                //連通前是斷開狀態
                if(uf.isJoined(edge[0],edge[1])){
                     return false;
                }
                uf.join(edge[0], edge[1]);
            }
            //最後連通只有一棵樹
            return uf.getPlant()==1;    
    
        }
}

另外一種實現

除了上面這種實現,這裡還有另外一種實現方式。如果是一棵樹,邊數和頂點數是滿足以下關係的
邊數 = 點數 - 1,具體的定理的證明你可以試試。在這種情況下,我們就不用去判斷連線前是不連通的狀態了,如果有邊連線前是連通的,那它一定也不滿足邊數 = 點數 - 1

class Solution {
    public boolean validTree(int n, int[][] edges) {
        //樹,首先要滿足邊數 = 點數 - 1,其次是保證聯通。
        if (n < 1 || edges.length != n - 1) {
            return false;
        }

        UFRankUnionCompressPath uf = new UFRankUnionCompressPath (n);
        for (int[] edge : edges) {
            uf.join(edge[0], edge[1]);
        }
        
        return uf.getPlant()==1;

    }
}

695. 島嶼的最大面積

力扣第695題 這題比剛剛那道題稍微複雜了一點點,但理解了還是很簡單的
https://leetcode-cn.com/problems/max-area-of-island/

題目

給定一個包含了一些 0 和 1 的非空二維陣列 grid 。

一個 【島嶼】是由一些相鄰的 1 (代表土地) 構成的組合,這裡的「相鄰」要求兩個 1 
必須在水平或者豎直方向上相鄰。你可以假設 grid 的四個邊緣都被 0(代表水)包圍著。

找到給定的二維陣列中最大的島嶼面積。(如果沒有島嶼,則返回面積為 0 。)


示例 1:

輸入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]

輸出:4

題意

題意是比較簡單的,就是說挨著的1就能連線成陸地(但只在水平和錘子方向挨著,斜著不算),計算最大的陸地連線了幾個點,如題中給出的案例,最大島嶼是陣列左上角連線的4個數字。

解題思路

我們知道並查集是解決連通性的問題,那麼可以這樣用並查集連線上陸地的點,再通過計數算出該連通的樹的節點數。我們又需要再次將之前的並查集做一定的修改,增加一個記錄節點數的變數,因為是每個節點都有,使用陣列記錄。

class Solution {
     public int maxAreaOfIsland(int[][] grid) {

        int h = grid.length;
        int w = grid[0].length;
        UFRankUnionPlant uf = new UFRankUnionPlant(w * h);
        boolean noJoin = true;
        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                //int index = w * i + j;
                if (grid[i][j] == 1) {
                    noJoin = false;
                    //做合併操作
                    if (i - 1 >= 0 && grid[i - 1][j] == 1) {
                        //將二維陣列點對映成一維的點
                        uf.join(w * (i - 1) + j, w * i + j);  
                    }

                    if (j - 1 >= 0 && grid[i][j - 1] == 1) {
                        uf.join(w * i + j - 1, w * i + j);
                    }
                }
            }
        }
        //特別注意,因為並查集的初始的len一定是1,但如果沒產生任何合併,最大島嶼是0
        return  noJoin? 0:  uf.maxLen;  
    }
}

//基於並查集修改的類
public class UFRankUnionPlant {

    int[] parent;
    int[] rank;//樹的層數
    int plant;//株
    int maxLen; //全域性記錄最大株的成員變數
    int[] len;  //記錄每株數的節點數的陣列

    public UFRankUnionPlant(int size) {
        parent = new int[size];
        rank = new int[size];
        len = new int[size];    //記錄根的節點數
        plant = size;
        maxLen = 1;
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
            rank[i] = 1;
            len[i] = 1;
        }
    }


    public int findP(int x) {//查詢操作,其實是查詢  根節點
        if (x < 0 || x >= parent.length)
            return -1;  //或者直接丟擲異常

        while (parent[x] != x)//一直搜尋到根節點
            x = parent[x];

        return x;
    }


    public void join(int a, int b) {
        int ap = findP(a);
        int bp = findP(b);
        if (ap != bp) {
            plant--;
            //a的層數越高,就將層數少的合併到層數高的上面
            int joinlen = len[ap] + len[bp];
            maxLen = Math.max(joinlen, maxLen);

            if (rank[ap] > rank[bp]) {
                parent[bp] = ap;
                len[ap] = joinlen;
            } else if (rank[ap] < rank[bp]) {
                parent[ap] = bp;
                len[bp] = joinlen;
            } else {
                //相同情況的話,隨便就可以
                parent[ap] = bp;
                rank[bp]++;
                len[bp] = joinlen;
            }
        }
    }

    public int getPlant() {
        return plant;
    }

    public boolean isJoined(int a, int b) { //兩個節點是否是  連線的
        return findP(a) == findP(b);
    }


}