文章目录
题目
标题和出处
标题:可能的二分法
出处:886. 可能的二分法
难度
5 级
题目描述
要求
需要将一组 n \texttt{n} n 个人(编号从 1 \texttt{1} 1 到 n \texttt{n} n)分成任意大小的两组。每个人可能不喜欢其他的一些人,他们不应该属于同一组。
给定整数 n \texttt{n} n 和数组 dislikes \texttt{dislikes} dislikes,其中 dislikes[i] = [a i , b i ] \texttt{dislikes[i] = [a}\texttt{i}\texttt{, b}\texttt{i}\texttt{]} dislikes[i] = [ai, bi],表示编号为 a i \texttt{a}\texttt{i} ai 的人不喜欢编号为 b i \texttt{b}\texttt{i} bi 的人,如果可以用这种方法将所有人分成两组,返回 true \texttt{true} true。
示例
示例 1:
输入: n = 4, dislikes = [[1,2],[1,3],[2,4]] \texttt{n = 4, dislikes = [[1,2],[1,3],[2,4]]} n = 4, dislikes = [[1,2],[1,3],[2,4]]
输出: true \texttt{true} true
解释:第一组是 [1,4] \texttt{[1,4]} [1,4],第二组是 [2,3] \texttt{[2,3]} [2,3]。
示例 2:
输入: n = 3, dislikes = [[1,2],[1,3],[2,3]] \texttt{n = 3, dislikes = [[1,2],[1,3],[2,3]]} n = 3, dislikes = [[1,2],[1,3],[2,3]]
输出: false \texttt{false} false
示例 3:
输入: n = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]] \texttt{n = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]} n = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]
输出: false \texttt{false} false
数据范围
- 1 ≤ n ≤ 2000 \texttt{1} \le \texttt{n} \le \texttt{2000} 1≤n≤2000
- 0 ≤ dislikes.length ≤ 10 4 \texttt{0} \le \texttt{dislikes.length} \le \texttt{10}^\texttt{4} 0≤dislikes.length≤104
- dislikes[i].length = 2 \texttt{dislikes[i].length} = \texttt{2} dislikes[i].length=2
- 1 ≤ dislikes[i][j] ≤ n \texttt{1} \le \texttt{dislikes[i][j]} \le \texttt{n} 1≤dislikes[i][j]≤n
- a i < b i \texttt{a}\texttt{i} < \texttt{b}\texttt{i} ai<bi
- dislikes \texttt{dislikes} dislikes 中的所有组各不相同
前言
将 n n n 个人和他们之间的不喜欢关系看成无向图,每个人是一个结点,每个不喜欢关系是一条无向边。如果可以按照题目要求分组,则同一条边连接的两个结点属于不同的组,等价于无向图是二分图。因此,判断是否可以按照题目要求将 n n n 个人分成两组,等价于判断无向图是否为二分图。
解法一
思路和算法
根据二分图的定义,图中任意一条边连接的两个结点属于不同的组。
以下将两个组分别称为 A \text{A} A 组和 B \text{B} B 组。为了判断是否可以按照题目要求将 n n n 个人分成两组,需要将每个人对应的结点着色,将 A \text{A} A 组与 B \text{B} B 组的结点颜色分别称为 A \text{A} A 颜色与 B \text{B} B 颜色。
可以使用广度优先搜索判断无向图是否为二分图,搜索过程中将图中的每个结点着色。
由于题目中的图的表示方式是边数组,为了方便处理,需要首先将边数组转换成邻接结点列表的形式,转换后可以在 O ( 1 ) O(1) O(1) 时间获得一个结点的全部相邻结点,然后使用广度优先搜索遍历图。
遍历 n n n 个结点,如果遇到结点 i i i 未着色,则结点 i i i 所在的连通分量中的全部结点都未着色,将结点 i i i 的颜色设为 A \text{A} A 颜色,从结点 i i i 开始遍历其所在的连通分量中的全部结点,将每个结点着色并判断是否为二分图。
对于访问到的每个结点 node \textit{node} node,该结点一定已经着色,用 color \textit{color} color 表示结点 node \textit{node} node 的颜色,遍历与 next \textit{next} next 相邻的每个结点,执行如下操作。
-
如果相邻结点未着色,则将相邻结点的颜色设为与 color \textit{color} color 不同的另一种颜色,继续访问相邻结点。
-
如果相邻结点的颜色是 color \textit{color} color,则存在两个相邻结点的颜色相同,因此无向图不是二分图,返回 false \text{false} false。
如果遍历结束之后没有出现不符合二分图的着色情况,则无向图是二分图,返回 true \text{true} true。
上述做法的正确性说明如下。
着色的过程中,每个连通分量的首个着色结点的颜色是 A \text{A} A 颜色,对于其余每个结点,当该结点着色时,一定存在一个相邻结点的颜色与当前结点的颜色相反。如果遇到两个相邻结点的颜色相同,则任何着色方案都不可能使任意两个相邻结点的颜色不同,因此无向图不是二分图。
代码
java
class Solution {
static final int UNKNOWN = 0;
static final int COLOR_A = 1;
static final int COLOR_B = 2;
public boolean possibleBipartition(int n, int[][] dislikes) {
List<Integer>[] adjacentArr = new List[n + 1];
for (int i = 1; i <= n; i++) {
adjacentArr[i] = new ArrayList<Integer>();
}
for (int[] edge : dislikes) {
adjacentArr[edge[0]].add(edge[1]);
adjacentArr[edge[1]].add(edge[0]);
}
int[] colors = new int[n + 1];
for (int i = 1; i <= n; i++) {
if (colors[i] != UNKNOWN) {
continue;
}
colors[i] = COLOR_A;
Queue<Integer> queue = new ArrayDeque<Integer>();
queue.offer(i);
while (!queue.isEmpty()) {
int node = queue.poll();
int color = colors[node];
int nextColor = color == COLOR_A ? COLOR_B : COLOR_A;
List<Integer> adjacent = adjacentArr[node];
for (int next : adjacent) {
if (colors[next] == UNKNOWN) {
colors[next] = nextColor;
queue.offer(next);
} else if (colors[next] == color) {
return false;
}
}
}
}
return true;
}
}
复杂度分析
-
时间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n n n 是图中的结点数, m m m 是图中的边数。广度优先搜索的时间复杂度由结点数和边数决定。
-
空间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n n n 是图中的结点数, m m m 是图中的边数。邻接结点列表需要 O ( n + m ) O(n + m) O(n+m) 的空间,颜色数组和队列需要 O ( n ) O(n) O(n) 的空间。
解法二
思路和算法
也可以使用深度优先搜索判断无向图是否为二分图,搜索过程中将图中的每个结点着色。
由于题目中的图的表示方式是边数组,为了方便处理,需要首先将边数组转换成邻接结点列表的形式,转换后可以在 O ( 1 ) O(1) O(1) 时间获得一个结点的全部相邻结点,然后使用深度优先搜索遍历图。
遍历 n n n 个结点,如果遇到结点 i i i 未着色,则结点 i i i 所在的连通分量中的全部结点都未着色,将结点 i i i 的颜色设为 A \text{A} A 颜色,从结点 i i i 开始遍历其所在的连通分量中的全部结点,将每个结点着色并判断是否为二分图。
对于访问到的每个结点 node \textit{node} node,该结点一定已经着色,用 color \textit{color} color 表示结点 node \textit{node} node 的颜色,遍历与 next \textit{next} next 相邻的每个结点,执行如下操作。
-
如果相邻结点未着色,则将相邻结点的颜色设为与 color \textit{color} color 不同的另一种颜色,继续访问相邻结点。
-
如果相邻结点的颜色是 color \textit{color} color,则存在两个相邻结点的颜色相同,因此无向图不是二分图,返回 false \text{false} false。
如果遍历结束之后没有出现不符合二分图的着色情况,则无向图是二分图,返回 true \text{true} true。
上述做法的正确性说明如下。
着色的过程中,每个连通分量的首个着色结点的颜色是 A \text{A} A 颜色,对于其余每个结点,当该结点着色时,一定存在一个相邻结点的颜色与当前结点的颜色相反。如果遇到两个相邻结点的颜色相同,则任何着色方案都不可能使任意两个相邻结点的颜色不同,因此无向图不是二分图。
代码
java
class Solution {
static final int UNKNOWN = 0;
static final int COLOR_A = 1;
static final int COLOR_B = 2;
List<Integer>[] adjacentArr;
int[] colors;
public boolean possibleBipartition(int n, int[][] dislikes) {
adjacentArr = new List[n + 1];
for (int i = 1; i <= n; i++) {
adjacentArr[i] = new ArrayList<Integer>();
}
for (int[] edge : dislikes) {
adjacentArr[edge[0]].add(edge[1]);
adjacentArr[edge[1]].add(edge[0]);
}
this.colors = new int[n + 1];
for (int i = 1; i <= n; i++) {
if (colors[i] == UNKNOWN) {
if (!dfs(i, COLOR_A)) {
return false;
}
}
}
return true;
}
public boolean dfs(int node, int color) {
colors[node] = color;
boolean valid = true;
int nextColor = color == COLOR_A ? COLOR_B : COLOR_A;
List<Integer> adjacent = adjacentArr[node];
for (int next : adjacent) {
if (colors[next] == UNKNOWN) {
valid = dfs(next, nextColor);
} else if (colors[next] == color) {
valid = false;
}
if (!valid) {
return false;
}
}
return true;
}
}
复杂度分析
-
时间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n n n 是图中的结点数, m m m 是图中的边数。深度优先搜索的时间复杂度由结点数和边数决定。
-
空间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n n n 是图中的结点数, m m m 是图中的边数。邻接结点列表需要 O ( n + m ) O(n + m) O(n+m) 的空间,颜色数组和递归调用栈需要 O ( n ) O(n) O(n) 的空间。
解法三
预备知识
该解法涉及到并查集。
并查集是一种树型的数据结构,用于处理不相交集合的合并与查询问题。
思路和算法
判断无向图是否为二分图,可以将属于同一个集合的结点合并,然后遍历每一条边,根据同一条边连接的两个结点所属的集合判断无向图是否为二分图。
由于题目中的图的表示方式是边数组,为了方便处理,需要首先将边数组转换成邻接结点列表的形式,转换后可以在 O ( 1 ) O(1) O(1) 时间获得一个结点的全部相邻结点,然后使用并查集判断无向图是否为二分图。
并查集初始化时, n n n 个结点分别属于 n n n 个不同的集合,每个集合只包含一个结点。
初始化之后,执行合并操作。对于结点 i i i, graph [ i ] \textit{graph}[i] graph[i] 中的所有结点都和结点 i i i 属于不同的集合,因此 graph [ i ] \textit{graph}[i] graph[i] 中的所有结点属于同一个集合,将这些结点所在的集合做合并。
合并操作结束之后,遍历每一条边,执行如下判断。
-
如果存在同一条边连接的两个结点属于同一个集合,则无向图不是二分图,返回 false \text{false} false。
-
如果每一条边连接的两个结点都属于不同集合,则无向图是二分图,返回 true \text{true} true。
代码
java
class Solution {
public boolean possibleBipartition(int n, int[][] dislikes) {
List<Integer>[] adjacentArr = new List[n + 1];
for (int i = 1; i <= n; i++) {
adjacentArr[i] = new ArrayList<Integer>();
}
for (int[] edge : dislikes) {
adjacentArr[edge[0]].add(edge[1]);
adjacentArr[edge[1]].add(edge[0]);
}
UnionFind uf = new UnionFind(n + 1);
for (int i = 1; i <= n; i++) {
List<Integer> adjacent = adjacentArr[i];
int count = adjacent.size();
for (int j = 1; j < count; j++) {
uf.union(adjacent.get(0), adjacent.get(j));
}
}
for (int i = 1; i <= n; i++) {
int root = uf.find(i);
List<Integer> adjacent = adjacentArr[i];
for (int j : adjacent) {
if (uf.find(j) == root) {
return false;
}
}
}
return true;
}
}
class UnionFind {
private int[] parent;
private int[] rank;
public UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
rank = new int[n];
}
public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx]++;
}
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
}
复杂度分析
-
时间复杂度: O ( n + m × α ( m ) ) O(n + m \times \alpha(m)) O(n+m×α(m)),其中 n n n 是图中的结点数, m m m 是图中的边数, α \alpha α 是反阿克曼函数。生成邻接结点列表需要 O ( n + m ) O(n + m) O(n+m) 的时间,并查集的初始化需要 O ( n ) O(n) O(n) 的时间,然后遍历每条边执行 O ( m ) O(m) O(m) 次合并操作,最后遍历每条边判断是否为二分图,因此并查集初始化之后的操作的时间复杂度是 O ( m × α ( m ) ) O(m \times \alpha(m)) O(m×α(m)),总时间复杂度是 O ( n + m × α ( m ) ) O(n + m \times \alpha(m)) O(n+m×α(m))。
-
空间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n n n 是图中的结点数, m m m 是图中的边数。邻接结点列表需要 O ( n + m ) O(n + m) O(n+m) 的空间,并查集需要 O ( n ) O(n) O(n) 的空间。