Leetcode(785)——判断二分图
题目
存在一个 无向图 ,图中有 n 个节点。其中每个节点都有一个介于 0 到 n - 1 之间的唯一编号。给你一个二维数组 graph ,其中 graph[u] 是一个节点数组,由节点 u 的邻接节点组成。形式上,对于 graph[u] 中的每个 v ,都存在一条位于节点 u 和节点 v 之间的无向边。该无向图同时具有以下属性:
- 不存在自环(graph[u] 不包含 u)。
- 不存在平行边(graph[u] 不包含重复值)。
- 如果 v 在 graph[u] 内,那么 u 也应该在 graph[v] 内(该图是无向图)
- 这个图可能不是连通图,也就是说两个节点 u 和 v 之间可能不存在一条连通彼此的路径。
二分图 定义:如果能将一个图的节点集合分割成两个独立的子集 A 和 B ,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为 二分图 。
如果图是二分图,返回 true
;否则,返回 false
。
示例 1:
输入: graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
输出: false
解释: 不能将节点分割成两个独立的子集,以使每条边都连通一个子集中的一个节点与另一个子集中的一个节点。
示例 2:
输入: graph = [[1,3],[0,2],[1,3],[0,2]]
输出: true
解释: 可以将节点分成两组: {0, 2} 和 {1, 3} 。
提示:
- graph.length == n
- 1 <= n <= 100
- 0 <= graph[u].length < n
- 0 <= graph[u][i] <= n - 1
- graph[u] 不会包含 u
- graph[u] 的所有值 互不相同
- 如果 graph[u] 包含 v,那么 graph[v] 也会包含 u
题解
方法一:DFS + 染色法
思路
我们使用图搜索算法从各个连通域的任一顶点开始遍历整个连通域,遍历的过程中用两种不同的颜色对顶点进行染色,相邻顶点染成相反的颜色。这个过程中倘若发现相邻的顶点被染成了相同的颜色,说明它不是二分图;反之,如果所有的连通域都染色成功,说明它是二分图。
算法的流程如下:
- 我们任选一个节点开始,将其染成红色,并从该节点开始对整个无向图进行遍历;
- 在遍历的过程中,如果我们通过节点
u
u
u 遍历到了节点
v
v
v(即
u
u
u 和
v
v
v 在图中有一条边直接相连),那么会有两种情况:
- 如果 v v v 未被染色,那么我们将其染成与 u u u 不同的颜色,并对 v v v 直接相连的节点进行遍历;
- 如果 v v v 被染色,并且颜色与 u u u 相同,那么说明给定的无向图不是二分图。我们可以直接退出遍历并返回 false \text{false} false 作为答案。
- 当遍历结束时,说明给定的无向图是二分图,返回 true \text{true} true 作为答案。
我们可以使用「深度优先搜索」或「广度优先搜索」对无向图进行遍历,下文分别给出了这两种搜索对应的代码。
注意:题目中给定的无向图不一定保证连通,因此我们需要进行多次遍历,直到每一个节点都被染色,或确定答案为 false \text{false} false 为止。每次遍历开始时,我们任选一个未被染色的节点,将所有与该节点直接或间接相连的节点进行染色。
代码实现
我自己的:
class Solution {
vector<int> g_p;
public:
bool isBipartite(vector<vector<int>>& graph) {
if(graph.empty() || graph.size() == 1) return true;
for(int n = 0; n < graph.size(); n++) g_p.push_back(0);
// 染色法 lastcolor 为上一个点的染色情况————0为未染色,-1为黑色,1为白色
for(int n = 0; n < graph.size(); n++){
if(g_p[n] == 0) // 未被访问过
if(!DFS(0, n, graph)) return false;
}
return true;
}
// 深度优先搜索
bool DFS(int lc, int p, vector<vector<int>>& g){
// lastcolor == 0 表示这是一个连通子图的起始点
if(lc == 0){
g_p[p] = 1;
for(auto& it: g[p])
if(!DFS(g_p[p], it, g)) return false;
}else{
if(g_p[p] == 0){
g_p[p] = -lc;
for(auto& it: g[p])
if(!DFS(g_p[p], it, g)) return false;
}else if(g_p[p] == lc) return false;
}
return true;
}
};
Leetcode 官方题解:
class Solution {
private:
static constexpr int UNCOLORED = 0;
static constexpr int RED = 1;
static constexpr int GREEN = 2;
vector<int> color;
bool valid;
public:
void dfs(int node, int c, const vector<vector<int>>& graph) {
color[node] = c;
int cNei = (c == RED ? GREEN : RED);
for (int neighbor: graph[node]) {
if (color[neighbor] == UNCOLORED) {
dfs(neighbor, cNei, graph);
if (!valid) {
return;
}
}
else if (color[neighbor] != cNei) {
valid = false;
return;
}
}
}
bool isBipartite(vector<vector<int>>& graph) {
int n = graph.size();
valid = true;
color.assign(n, UNCOLORED);
for (int i = 0; i < n && valid; ++i) {
if (color[i] == UNCOLORED) {
dfs(i, RED, graph);
}
}
return valid;
}
};
复杂度分析
时间复杂度:
O
(
n
+
m
)
O(n+m)
O(n+m),其中
n
n
n 和
m
m
m 分别是无向图中的点数和边数。
空间复杂度:
O
(
n
)
O(n)
O(n),存储节点颜色的数组需要
O
(
n
)
O(n)
O(n) 的空间,并且在深度优先搜索的过程中,栈的深度最大为
n
n
n,需要
O
(
n
)
O(n)
O(n) 的空间。
方法二:BFS + 染色法
思路
大致思路与方法一一致,只是遍历图的方式变为 BFS。
代码实现
我自己的:
class Solution {
vector<int> g_p;
public:
bool isBipartite(vector<vector<int>>& graph) {
if(graph.empty() || graph.size() == 1) return true;
for(int n = 0; n < graph.size(); n++) g_p.push_back(0);
return BFS(graph);
}
// 广度优先搜索
bool BFS(vector<vector<int>>& g){
queue<int> qg;
for(int n = 0; n < g.size(); n++){
if(g_p[n] == 0){
g_p[n] = 1;
qg.push(n);
while(!qg.empty()){
int i = qg.front();
for(auto& it: g[i]){
if(g_p[it] == 0){
g_p[it] = -g_p[i];
qg.push(it);
}else if(g_p[it] == g_p[i]) return false;
}
qg.pop();
}
}
}
return true;
}
};
Leetcode 官方题解:
class Solution {
private:
static constexpr int UNCOLORED = 0;
static constexpr int RED = 1;
static constexpr int GREEN = 2;
vector<int> color;
public:
bool isBipartite(vector<vector<int>>& graph) {
int n = graph.size();
vector<int> color(n, UNCOLORED);
for (int i = 0; i < n; ++i) {
if (color[i] == UNCOLORED) {
queue<int> q;
q.push(i);
color[i] = RED;
while (!q.empty()) {
int node = q.front();
int cNei = (color[node] == RED ? GREEN : RED);
q.pop();
for (int neighbor: graph[node]) {
if (color[neighbor] == UNCOLORED) {
q.push(neighbor);
color[neighbor] = cNei;
}
else if (color[neighbor] != cNei) {
return false;
}
}
}
}
}
return true;
}
};
复杂度分析
时间复杂度:
O
(
n
+
m
)
O(n+m)
O(n+m),其中
n
n
n 和
m
m
m 分别是无向图中的点数和边数。
空间复杂度:
O
(
n
)
O(n)
O(n),存储节点颜色的数组需要
O
(
n
)
O(n)
O(n) 的空间,并且在广度优先搜索的过程中,队列中最多有
n
−
1
n−1
n−1 个节点,需要
O
(
n
)
O(n)
O(n) 的空间。
方法三:并查集
思路
如果是二分图的话,那么图中每个顶点的所有邻接点都应该属于同一集合,且当前顶点的所有邻接点不与当前顶点处于同一集合。因此我们可以使用并查集来解决这个问题。
首先遍历图中每个顶点,将当前顶点的所有邻接点进行合并,并判断这些邻接点中是否存在某一邻接点已经和当前顶点处于同一个集合中了,若是,则说明不是二分图。
代码实现
网友解法:
class union_set {
vector<int> uset;
vector<int> rank;
public:
void make_set(int len) {
uset.resize(len);
rank.resize(len);
for (int i=0; i<len; ++i) uset[i] = i;
//cout << "make is done";
}
int find(int c) {
if (c==uset[c]) return c;
else {
uset[c] = find(uset[c]);
return uset[c];
}
}
void un(int l, int r) {
if (find(l) == find(r)) return ;
if (rank[l] < rank[r]) {
uset[find(l)] = r;
} else {
uset[find(r)] = l;
if (rank[l]==rank[r]) rank[l]++;
}
}
};
class Solution {
public:
bool isBipartite(vector<vector<int>>& graph) {
int n = graph.size();
union_set u;
u.make_set(n);
for (int i = 0; i < n; ++i) {
for (int j=0; j < graph[i].size(); ++j) {
int root = u.find(i);
if (root == u.find(graph[i][j])) return false;
}
for (int j=1; j<graph[i].size(); ++j) {
u.un(graph[i][j-1], graph[i][j]);
}
}
return true;
}
};
复杂度分析
时间复杂度:
O
(
N
+
M
)
O(N + M)
O(N+M),其中
N
N
N 是无向图的顶点数,
M
M
M 是无向图的边数。
空间复杂度:
O
(
N
)
O(N)
O(N),其中
N
N
N 是无向图的顶点数,
M
M
M 是无向图的边数。