通过本篇文章学习一下二分图,以及使用BFS、DFS、并查集三种方法来判断二分图。
文章目录
1. 二分图
1.1 什么是二分图?
若无向图 G = (V, E) 的顶点集 V 可以分割为两个互不相交的子集,且图中每条边的两个顶点分别属于不同的子集,则称图 G 为一个二分图。
例如上图,顶点可以分为红色和蓝色两个集合
,且图中每条边的两个顶点分别属于不同的集合,因此 上图是二分图
。
又如上图,顶点无法划分为红色和蓝色两个集合
,使得图中每条边的两个顶点分别属于不同的集合,因此 上图不是二分图
。
1.2 如何判断二分图
深度优先搜索 / 广度优先搜索
我们使用图搜索算法从各个连通域的任一顶点开始遍历整个连通域,遍历的过程中用两种不同的颜色对顶点进行 染色,相邻顶点染成相反的颜色。这个过程中倘若发现相邻的顶点已经被染成了相同的颜色,说明它不是二分图;反之,如果所有的连通域都染色成功,说明它是二分图。
并查集
根据定义,如果是二分图的话,那么 图中每个顶点的所有邻接点都应该属于同一集合,且不与顶点处于同一集合。因此我们可以使用并查集来解决这个问题,我们遍历图中每个顶点,将当前顶点的所有邻接点进行合并,并判断这些邻接点中是否存在某一邻接点已经和当前顶点处于同一个集合中了,若是,则说明不是二分图。
2. 785. 判断二分图
2.1 题目描述
leetcode链接:785. 判断二分图
2.2 思路分析
方法一:广度优先搜索
- 首先定义 int[] visited 数组,初始值为 0 表示未被访问,赋值为 1 或者 -1 表示两种不同的颜色。
- 对于每个连通域,任选其中一个顶点作为搜索起点,加入队列,对该连通域进行 BFS 染色。
方法二:深度优先搜索
- 首先定义 int[] visited 数组,初始值为 0 表示未被访问,赋值为 1 或者 -1 表示两种不同的颜色。
- 对于每个连通域,任选其中一个顶点作为搜索起点,对该连通域进行 DFS 染色。
方法三:并查集
- 遍历图中每个顶点:
- 将当前顶点的所有邻接点进行合并判断是否存在邻接点与当前顶点已经在一个集合中了,若存在,则说明不是二分图
并查集的定义、构建:
// 并查集
class UnionFind {
int[] roots;
// 初始化每个点指向自己
public UnionFind(int n) {
roots = new int[n];
for (int i = 0; i < n; i++) {
roots[i] = i;
}
}
// 返回 i 的根
public int find(int i) {
if (roots[i] == i) {
return i;
}
return roots[i] = find(roots[i]);
}
// 判断 p 和 q 是否在同一个集合中
public boolean isConnected(int p, int q) {
return find(q) == find(p);
}
// 合并 p 和 q 到一个集合中
public void union(int p, int q) {
roots[find(p)] = find(q);
}
}
2.3 参考代码
方法一:广度优先搜索
class Solution {
public boolean isBipartite(int[][] graph) {
int n = graph.length;
int[] visited = new int[n]; // 0表示未被访问,1和-1表示两个不同的集合
Queue<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < n; i++) {
if (visited[i] != 0) { // 判断是否被访问
continue;
}
queue.offer(i);
visited[i] = 1;
while(!queue.isEmpty()) {
int v = queue.poll();
// 每出队一个顶点,将其所有邻接点染成相反的颜色并入队
for (int w : graph[v]) {
if (visited[w] == visited[v]) { // 如果相邻节点颜色相同
return false;
}
if (visited[w] == 0) {
visited[w] = -visited[v];
queue.offer(w);
}
}
}
}
return true;
}
}
方法二:深度优先搜索
class Solution {
public boolean isBipartite(int[][] graph) {
int n = graph.length;
int[] visited = new int[n];
for (int i = 0; i < n; i++) {
if (visited[i] == 0 && !dfs(graph, i, 1, visited)) {
return false;
}
}
return true;
}
public boolean dfs(int[][] graph, int v, int color, int[] visited) {
// 如果要对某顶点染色时,发现它已经被染色了,则判断它的颜色是否与本次要染的颜色相同,如果矛盾,说明此无向图无法被正确染色,返回 false。
if (visited[v] != 0) {
return visited[v] == color;
}
// 对当前顶点进行染色,并将当前顶点的所有邻接点染成相反的颜色。
visited[v] = color;
for (int w : graph[v]) {
if (!dfs(graph, w, -color, visited)) {
return false;
}
}
return true;
}
}
方法三:并查集
class Solution {
public boolean isBipartite(int[][] graph) {
int n = graph.length;
// 初始化并查集
UnionFind uf = new UnionFind(n);
// 遍历每个顶点
for (int i = 0; i < n; i++) {
int[] adjs = graph[i];
// 遍历当前顶点的所有邻接点,将其与当前顶点的第一个邻接点合并。
for (int w: adjs) {
// 若某个邻接点与当前顶点已经在一个集合中了,说明不是二分图,返回 false。
if (uf.isConnected(i, w)) {
return false;
}
uf.union(adjs[0], w);
}
}
return true;
}
// 并查集
class UnionFind {
int[] roots;
// 初始化每个点指向自己
public UnionFind(int n) {
roots = new int[n];
for (int i = 0; i < n; i++) {
roots[i] = i;
}
}
// 返回 i 的根
public int find(int i) {
if (roots[i] == i) {
return i;
}
return roots[i] = find(roots[i]);
}
// 判断 p 和 q 是否在同一个集合中
public boolean isConnected(int p, int q) {
return find(q) == find(p);
}
// 合并 p 和 q 到一个集合中
public void union(int p, int q) {
roots[find(p)] = find(q);
}
}
}
3. 886. 可能的二分法
3.1 题目描述
leetcode题目链接:886. 可能的二分法
3.2 思路分析
这道题与785. 判断二分图
相同,也是一道二分图判断问题,不喜欢的人不能分在一组。只需要构建好无向图其余解法就一样,同样的可以使用BFS、DFS和并查集进行求解。
无向图构建初始化:
// List嵌套List的方法,需要注意的是节点是从1开始的,但初始化需要从0开始
List<List<Integer>> degree = new ArrayList<>();
for (int i = 0; i <= n; i++) {
degree.add(new ArrayList<Integer>());
}
for (int[] dislike : dislikes) { // 构建不喜欢的图
degree.get(dislike[0]).add(dislike[1]);
degree.get(dislike[1]).add(dislike[0]);
}
// List嵌套一维数组,可以直接从1开始初始化
List<Integer>[] graph = new LinkedList[n + 1];
for (int i = 1; i <= n; i++) {
graph[i] = new LinkedList<>();
}
for (int[] tmp : dislikes) {
int v = tmp[0];
int w = tmp[1];
graph[v].add(w);
graph[w].add(v);
}
剩下的二分图判断就与上题一样了。
3.3 参考代码
方法一:BFS
class Solution {
public boolean possibleBipartition(int n, int[][] dislikes) {
List<List<Integer>> degree = new ArrayList<>();
for (int i = 0; i <= n; i++) {
degree.add(new ArrayList<Integer>());
}
for (int[] dislike : dislikes) { // 构建不喜欢的图
degree.get(dislike[0]).add(dislike[1]);
degree.get(dislike[1]).add(dislike[0]);
}
int[] visited = new int[n + 1]; // 0表示未被访问,1和-1表示两个不同的集合
Queue<Integer> queue = new LinkedList<>();
for (int i = 1; i <= n; i++) {
if (visited[i] != 0) {
continue;
}
queue.offer(i);
visited[i] = 1;
while(!queue.isEmpty()) {
int v = queue.poll();
for (int w : degree.get(v)) {
if (visited[w] == visited[v]) {
return false;
}
if (visited[w] == 0) {
visited[w] = -visited[v];
queue.offer(w);
}
}
}
}
return true;
}
}
方法二:DFS
class Solution {
public boolean possibleBipartition(int n, int[][] dislikes) {
List<List<Integer>> degree = new ArrayList<>();
for (int i = 0; i <= n; i++) {
degree.add(new ArrayList<Integer>());
}
for (int[] dislike : dislikes) { // 构建不喜欢的图
degree.get(dislike[0]).add(dislike[1]);
degree.get(dislike[1]).add(dislike[0]);
}
int[] visited = new int[n + 1]; // 0表示未被访问,1和-1表示两个不同的集合
for (int i = 1; i <= n; i++) {
if (visited[i] == 0 && !dfs(degree, i, 1, visited)) {
return false;
}
}
return true;
}
public boolean dfs(List<List<Integer>> degree, int v, int color, int[] visited) {
// 如果要对某顶点染色时,发现它已经被染色了,则判断它的颜色是否与本次要染的颜色相同,如果矛盾,说明此无向图无法被正确染色,返回 false。
if (visited[v] != 0) {
return visited[v] == color;
}
// 对当前顶点进行染色,并将当前顶点的所有邻接点染成相反的颜色。
visited[v] = color;
for (int w : degree.get(v)) {
if (!dfs(degree, w, -color, visited)) {
return false;
}
}
return true;
}
}
方法三:并查集
class Solution {
public boolean possibleBipartition(int n, int[][] dislikes) {
List<List<Integer>> degree = new ArrayList<>();
for (int i = 0; i <= n; i++) {
degree.add(new ArrayList<Integer>());
}
for (int[] dislike : dislikes) { // 构建不喜欢的图
degree.get(dislike[0]).add(dislike[1]);
degree.get(dislike[1]).add(dislike[0]);
}
// 初始化并查集
UnionFind uf = new UnionFind(n);
// 遍历每个顶点
for (int i = 1; i <= n; i++) {
// 遍历当前顶点的所有邻接点,将其与当前顶点的第一个邻接点合并。
for (int w: degree.get(i)) {
// 若某个邻接点与当前顶点已经在一个集合中了,说明不是二分图,返回 false。
if (uf.isConnected(i, w)) {
return false;
}
uf.union(degree.get(i).get(0), w);
}
}
return true;
}
// 并查集
class UnionFind {
int[] roots;
// 初始化每个点指向自己
public UnionFind(int n) {
roots = new int[n + 1];
for (int i = 1; i <= n; i++) {
roots[i] = i;
}
}
// 返回 i 的根
public int find(int i) {
if (roots[i] == i) {
return i;
}
return roots[i] = find(roots[i]);
}
// 判断 p 和 q 是否在同一个集合中
public boolean isConnected(int p, int q) {
return find(q) == find(p);
}
// 合并 p 和 q 到一个集合中
public void union(int p, int q) {
roots[find(p)] = find(q);
}
}
}
参考: