1. 程式人生 > >[LeetCode] Possible Bipartition 可能的二分圖

[LeetCode] Possible Bipartition 可能的二分圖

union 原則 矩陣 的人 his target roo 兩個人 遞歸函數

Given a set of N people (numbered 1, 2, ..., N), we would like to split everyone into two groups of any size.

Each person may dislike some other people, and they should not go into the same group.

Formally, if dislikes[i] = [a, b], it means it is not allowed to put the people numbered a and b into the same group.

Return true if and only if it is possible to split everyone into two groups in this way.

Example 1:

Input: N = 4, dislikes = [[1,2],[1,3],[2,4]]
Output: true
Explanation: group1 [1,4], group2 [2,3]

Example 2:

Input: N = 3, dislikes = [[1,2],[1,3],[2,3]]
Output: false

Example 3:

Input: N = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]
Output: false

Note:

  1. 1 <= N <= 2000
  2. 0 <= dislikes.length <= 10000
  3. 1 <= dislikes[i][j] <= N
  4. dislikes[i][0] < dislikes[i][1]
  5. There does not exist i != j for which dislikes[i] == dislikes[j].

這道題又是關於二分圖的題,第一次接觸的時候是 Is Graph Bipartite?,那道題給的是建好的鄰接鏈表(雖然是用數組實現的),但是本質上和這道題是一樣的,同一條邊上的兩點是不能在同一個集合中的,那麽這就相當於本題中的dislike的關系,,也可以把每個dislike看作是一條邊,那麽兩端的兩個人不能在同一個集合中。看透了題目的本質後,就不難做了,跟之前的題相比,這裏唯一不同的就是鄰接鏈表沒有給我們建好,需要自己去建。不管是建鄰接鏈表,還是鄰接矩陣都行,反正是要先把圖建起來才能遍歷。那麽這裏我們先建立一個鄰接矩陣好了,建一個大小為 (N+1) x (N+1) 的二維數組g,其中若 g[i][j] 為1,說明i和j互相不鳥。那麽我們先根據dislikes的情況,把二維數組先賦上值,註意這裏 g[i][j] 和 g[j][i] 都要更新,因為是互相不鳥,而並不是一方熱臉貼冷屁股。下面就要開始遍歷了,還是使用染色發,使用一個一維的colors數組,大小為N+1,初始化是0,由於只有兩組,我們可以用1和-1來區分。那麽開始遍歷圖中的結點,對於每個遍歷到的結點,如果其還未被染色,還是一張白紙的時候,我們調用遞歸函數對其用顏色1進行嘗試染色。在遞歸函數中,現將該結點染色,然後就要遍歷所有跟其合不來的人,這裏就發現鄰接矩陣的好處了吧,不然每次還得遍歷dislikes數組。由於這裏是鄰接矩陣,所以我們只有在其值為1的時候才處理,當找到一個跟其合不來的人,首先檢測其染色情況,如果此時兩個人顏色相同了,說明已經在一個組裏了,這就矛盾了,直接返回false。如果那個人還是白紙一張,我們嘗試用相反的顏色去染他,如果無法成功染色,則返回false。循環順序退出後,返回true,參見代碼如下:

解法一:

class Solution {
public:
    bool possibleBipartition(int N, vector<vector<int>>& dislikes) {
        vector<vector<int>> g(N + 1, vector<int>(N + 1));
        for (auto dislike : dislikes) {
            g[dislike[0]][dislike[1]] = 1;
            g[dislike[1]][dislike[0]] = 1;
        }
        vector<int> colors(N + 1);
        for (int i = 1; i <= N; ++i) {
            if (colors[i] == 0 && !helper(g, i, 1, colors)) return false;
        }
        return true;
    }
    bool helper(vector<vector<int>>& g, int cur, int color, vector<int>& colors) {
        colors[cur] = color;
        for (int i = 0; i < g.size(); ++i) {
            if (g[cur][i] == 1) {
                if (colors[i] == color) return false;
                if (colors[i] == 0 && !helper(g, i, -color, colors)) return false;
            }
        }
        return true;
    }
};

我們還可以用叠代的寫法,不實用遞歸函數,但是整個思路還是完全一樣的。這裏我們建立鄰接鏈表,比鄰接矩陣能省一些空間,只把跟其相鄰的結點存入對應的數組內。還是要建立一個一維colors數組,並開始遍歷結點,若某個結點已經染過色了,跳過,否則就先給其染為1。然後我們借助queue來進行BFS遍歷,現將當前結點排入隊列,然後開始循環隊列,取出隊首結點,然後遍歷其所有相鄰結點,如果兩個顏色相同,直接返回false,否則若其為白紙,則賦相反顏色,並且排入隊列。最終若順序完成遍歷,返回true,參見代碼如下:

解法二:

class Solution {
public:
    bool possibleBipartition(int N, vector<vector<int>>& dislikes) {
        vector<vector<int>> g(N + 1);
        for (auto dislike : dislikes) {
            g[dislike[0]].push_back(dislike[1]);
            g[dislike[1]].push_back(dislike[0]);
        }
        vector<int> colors(N + 1);
        for (int i = 1; i <= N; ++i) {
            if (colors[i] != 0) continue;
            colors[i] = 1;
            queue<int> q{{i}};
            while (!q.empty()) {
                int t = q.front(); q.pop();
                for (int cur : g[t]) {
                    if (colors[cur] == colors[t]) return false;
                    if (colors[cur] == 0) {
                        colors[cur] = -colors[t];
                        q.push(cur);
                    }
                }
            }
        }
        return true;
    }
};

其實這道題還可以使用並查集Union Find來做,所謂的並查集,簡單來說,就是歸類,將同一集合的元素放在一起。那麽如何在能驗證兩個元素是否屬於同一個集合呢,這裏就要使用一個root數組(有時候是使用HashMap),如果兩個元素是同一個組的話,那麽最終調用find函數返回的值應該是相同的,可以理解為老祖宗相同就是同一個組,兩個點的root值不同,也可能是同一個組,因為find函數的運作機制是一直追根溯源到最原始的值。可以看到,這裏博主的find函數寫的是遞歸形式,一行搞定碉堡了,當然也有while循環式的叠代寫法。好,回過頭來繼續說這道題,這裏我們還是首先建圖,這裏建立鄰接鏈表,跟上面的使用二維數組的方法不同,這裏使用來HashMap,更加的節省空間。現在我們不需要用colors數組了,而是要使用並查集需要的root數組,給每個點都初始化為不同的值,因為在初始時將每個點都看作一個不同的組。然後我們開始遍歷所有結點,若當前結點沒有鄰接結點,直接跳過。否則就要開始進行處理了,並查集方法的核心就兩步,合並跟查詢。我們首先進行查詢操作,對當前結點和其第一個鄰接結點分別調用find函數,如果其返回值相同,則意味著其屬於同一個集合了,這是不合題意的,直接返回false。否則我們繼續遍歷其他的鄰接結點,對於每一個新的鄰接結點,我們都調用find函數,還是判斷若返回值跟原結點的相同,return false。否則就要進行合並操作了,根據敵人的敵人就是朋友的原則,所有的鄰接結點之間應該屬於同一個組,因為就兩個組,我所有不爽的人都不能跟我在一個組,那麽他們所有人只能都在另一個組,所以需要將他們都合並起來,合並的時候不管是用 root[parent] = y 還是 root[g[i][j]] = y 都是可以,因為不管直接跟某個結點合並,或者跟其祖宗合並,最終經過find函數追蹤溯源都會返回相同的值,參見代碼如下:

解法三:

class Solution {
public:
    bool possibleBipartition(int N, vector<vector<int>>& dislikes) {
        unordered_map<int, vector<int>> g;
        for (auto dislike : dislikes) {
            g[dislike[0]].push_back(dislike[1]);
            g[dislike[1]].push_back(dislike[0]);
        }
        vector<int> root(N + 1);
        for (int i = 1; i <= N; ++i) root[i] = i;
        for (int i = 1; i <= N; ++i) {
            if (!g.count(i)) continue;
            int x = find(root, i), y = find(root, g[i][0]);
            if (x == y) return false;
            for (int j = 1; j < g[i].size(); ++j) {
                int parent = find(root, g[i][j]);
                if (x == parent) return false;
                root[parent] = y;
            }
        }
        return true;
    }
    int find(vector<int>& root, int i) {
        return root[i] == i ? i : find(root, root[i]);
    }
};

討論:可以看到本文中的三種解法在建立圖的時候,使用的數據結構都不同,解法一使用二維數組建立了鄰接矩陣,解法二使用二維數組建立了鄰接鏈表,解法三使用了HashMap建立了鄰接鏈表。刻意使用不同的方法就是為了大家可以對比區別一下,這三種方法都比較常用,在不同的題目中選擇最適合的方法即可。

類似題目:

Is Graph Bipartite?

參考資料:

https://leetcode.com/problems/possible-bipartition/

https://leetcode.com/problems/possible-bipartition/discuss/159085/java-graph

https://leetcode.com/problems/possible-bipartition/discuss/195303/Java-Union-Find

https://leetcode.com/problems/possible-bipartition/discuss/158957/Java-DFS-solution

LeetCode All in One 題目講解匯總(持續更新中...)

[LeetCode] Possible Bipartition 可能的二分圖