[LeetCode] Possible Bipartition 可能的二分圖
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 <= N <= 2000
0 <= dislikes.length <= 10000
1 <= dislikes[i][j] <= N
dislikes[i][0] < dislikes[i][1]
- There does not exist
i != j
for whichdislikes[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 可能的二分圖