886. 可能的二分法 : 判定二分图模板题

吕旷
• 阅读 653

题目描述

这是 LeetCode 上的 886. 可能的二分法 ,难度为 中等

Tag : 「二分图」、「染色法」、「并查集」、「DFS」

给定一组 n 人(编号为 1, 2, ..., n), 我们想把每个人分进任意大小的两组。每个人都可能不喜欢其他人,那么他们不应该属于同一组。

给定整数 n 和数组 dislikes ,其中 $dislikes[i] = [a_i, b_i]$ ,表示不允许将编号为 $a_i$ 和  $b_i$ 的人归入同一组。当可以用这种方法将所有人分进两组时,返回 true;否则返回 false

示例 1:

输入:n = 4, dislikes = [[1,2],[1,3],[2,4]]

输出:true

解释:group1 [1,4], group2 [2,3]

示例 2:

输入:n = 3, dislikes = [[1,2],[1,3],[2,3]]

输出:false

示例 3:

输入:n = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]

输出:false

提示:

  • $1 <= n <= 2000$
  • $0 <= dislikes.length <= 10^4$
  • $dislikes[i].length = 2$
  • $1 <= dislikes[i][j] <= n$
  • $a_i < b_i$
  • dislikes 中每一组都 不同

染色法

无论是从题目描述和对点边的描述,这都是一道「染色法判定二分图」的模板题。

为了方便,我们令 dislikesds,将其长度记为 $m$。

题目要求我们将 $n$ 个点划分到两个集合中,同时我们将每个 $ds[i]$ 看做无向边的话,可知集合内部无边,即所有的边必然横跨两个集合之间。

使用 $ds[i]$ 进行建图,并将两个将要划分出的两个集合分别记为 AB,我们可以采用「染色」的方式,尝试将所有点进行划分。

构建一个与点数相等的数组 color,我们人为规定划分到集合 A 的点满足 $color[i] = 1$,划分到集合 B 的点满足 $color[i] = 2$,起始有 $color[i] = 0$,代表该点尚未被划分。

随后我们可以实现 DFS 函数为 boolean dfs(int u, int cur) 含义为尝试将点 ucur 色。根据定义可知,我们除了需要 color[u] = cur 以外,还需要遍历点 u 的所有出边(处理其邻点,将其划分到另一集合上),若在处理过程中发生冲突,则返回 false,若能顺利染色则返回 true

由于我们固定了颜色编号为 12,因此 cur 的对立色可统一为 3 - cur

最终,我们根据能否给所有点染色成功来决定答案。

Java 代码:

class Solution {
    int N = 2010, M = 2 * 10010;
    int[] he = new int[N], e = new int[M], ne = new int[M], color = new int[N];
    int idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    boolean dfs(int u, int cur) {
        color[u] = cur;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (color[j] == cur) return false;
            if (color[j] == 0 && !dfs(j, 3 - cur)) return false;
        }
        return true;
    }
    public boolean possibleBipartition(int n, int[][] ds) {
        Arrays.fill(he, -1);
        for (int[] info : ds) {
            int a = info[0], b = info[1];
            add(a, b); add(b, a);
        }
        for (int i = 1; i <= n; i++) {
            if (color[i] != 0) continue;
            if (!dfs(i, 1)) return false;
        }
        return true;
    }
}

TypeScript 代码:

const N = 2010, M = 2 * 10010
const he = new Array<number>(N), e = new Array<number>(M).fill(0), ne = new Array<number>(M).fill(0), color = new Array<number>(N)
let idx = 0
function add(a: number, b: number): void {
    e[idx] = b
    ne[idx] = he[a]
    he[a] = idx++
}
function dfs(u: number, cur: number): boolean {
    color[u] = cur
    for (let i = he[u]; i != -1; i = ne[i]) {
        const j = e[i];
        if (color[j] == cur) return false
        if (color[j] == 0 && !dfs(j, 3 - cur)) return false
    }
    return true
}
function possibleBipartition(n: number, ds: number[][]): boolean {
    he.fill(-1)
    idx = 0
    for (const info of ds) {
        const a = info[0], b = info[1]
        add(a, b); add(b, a)
    }
    color.fill(0)
    for (let i = 1; i <= n; i++) {
        if (color[i] != 0) continue
        if (!dfs(i, 1)) return false
    }
    return true
}

Python 代码:

class Solution:
    def possibleBipartition(self, n: int, ds: List[List[int]]) -> bool:
        N, M = 2010, 20010
        he, e, ne, color = [-1] * N, [0] * M, [0] * M, [0] * N
        idx = 0
        def add(a, b):
            nonlocal idx
            e[idx], ne[idx], he[a] = b, he[a], idx
            idx += 1
        def dfs(u, cur):
            color[u] = cur
            i = he[u]
            while i != -1:
                j = e[i]
                if color[j] == cur:
                    return False
                if color[j] == 0 and not dfs(j, 3 - cur):
                    return False
                i = ne[i]
            return True
        for info in ds:
            a, b = info[0], info[1]
            add(a, b)
            add(b, a)
        for i in range(1, n + 1):
            if color[i] != 0:
                continue
            if not dfs(i, 1):
                return False
        return True
  • 时间复杂度:$O(n + m)$
  • 空间复杂度:$O(n + m)$

反向点 + 并查集

我们知道对于 $ds[i] = (a, b)$ 而言,点 a 和点 b 必然位于不同的集合中,同时由于只有两个候选集合,因此这样的关系具有推断性:即对于 $(a, b)$ 和 $(b, c)$ 可知 ac 位于同一集合。

因此,我们可以对于每个点 x 而言,建议一个反向点 x + n:若点 x 位于集合 A 则其反向点 x + n 位于集合 B,反之同理。

基于此,我们可以使用「并查集」维护所有点的连通性:边维护变检查每个 $ds[i]$ 的联通关系,若 $ds[i] = (a, b)$ 联通,必然是其反向点联通所导致,必然是此前的其他 $ds[j]$ 导致的关系冲突,必然不能顺利划分成两个集合,返回 false,否则返回 true

Java 代码:

class Solution {
    int[] p = new int[4010];
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    boolean query(int a, int b) {
        return find(a) == find(b);
    }
    public boolean possibleBipartition(int n, int[][] ds) {
        for (int i = 1; i <= 2 * n; i++) p[i] = i;
        for (int[] info : ds) {
            int a = info[0], b = info[1];
            if (query(a, b)) return false;
            union(a, b + n); union(b, a + n);
        }
        return true;
    }
}

TypeScript 代码:

const p = new Array<number>(4010).fill(0)
function find(x: number): number {
    if (p[x] != x) p[x] = find(p[x])
    return p[x]
}
function union(a: number, b: number): void {
    p[find(a)] = p[find(b)]
}
function query(a: number, b: number): boolean {
    return find(a) == find(b)
}
function possibleBipartition(n: number, ds: number[][]): boolean {
    for (let i = 1; i <= 2 * n; i++) p[i] = i
    for (const info of ds) {
        const a = info[0], b = info[1]
        if (query(a, b)) return false
        union(a, b + n); union(b, a + n)
    }
    return true
}

Python 代码:

class Solution:
    def possibleBipartition(self, n: int, ds: List[List[int]]) -> bool:
        p = [i for i in range(0, 2 * n + 10)]
        def find(x):
            if p[x] != x:
                p[x] = find(p[x])
            return p[x]
        def union(a, b):
            p[find(a)] = p[find(b)]
        def query(a, b):
            return find(a) == find(b)
        for info in ds:
            a, b = info[0], info[1]
            if query(a, b):
                return False
            else:
                union(a, b + n)
                union(b, a + n)
        return True
  • 时间复杂度:$O(n + m)$
  • 空间复杂度:$O(n)$

最后

这是我们「刷穿 LeetCode」系列文章的第 No.886 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSou...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

本文由mdnice多平台发布

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java 二分法查找
packagecom.test;importjava.util.Arrays;publicclassBinaraySearch{publicstaticintsearch(intkey,inta){intlo0;
Souleigh ✨ Souleigh ✨
4年前
python实现——最优化算法(二分法、格点法、黄金分割法、牛顿法等)
二分法函数详见rres,此代码使该算法运行了两次pythondefasdf(x):rres8x32x27x3returnrresi2left0right1whilei0:ii1ans0.1mid1(leftrightans)/2
小恐龙 小恐龙
4年前
彻底搞懂系列B-树、B+树、B-树、B*树
(https://blog.csdn.net/chai471793/article/details/99563704)平衡二叉树概念平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构;特点平衡二叉树是采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大
Stella981 Stella981
3年前
Leetcode Index
序:  用于记录刷题过程中难度较大或思路怪异的题目,对于常规难度一般的题就不写入我的博客了,关于效率仅是提交时击败的百分比,可能会随时间波动,仅供参考,算法优劣以时间复杂度和空间复杂度为基准。欢迎留言讨论。题目序号难度效率Leetcode42.TrappingRainWater(https://www.oschina.
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
4个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
贾蔷 贾蔷
5天前
力扣1137题 解题思路和步骤 C++代码实现,力扣一共多少题
一、题目分析力扣1137题要求我们找到第N个泰波那契数。泰波那契数的定义是:T00,T11,T21,且在n0的条件下Tn3TnTn1Tn2。,当n4时,T4T3T2T14。这道题主要考查我们对递归或动态规划的理解和运用。在思考解题方法时,我们