【Leetcode】BFS、DFS、并查集判断二分图

通过本篇文章学习一下二分图,以及使用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);
        }
    }
}

参考:

Sweetiee姨:【Leetcode每日打卡】判断二分图

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值