可能的二分法

标签: 深度优先搜索 广度优先搜索 并查集

难度: Medium

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

给定整数 n 和数组 dislikes ,其中 dislikes[i] = [ai, bi] ,表示不允许将编号为 ai 和  bi的人归入同一组。当可以用这种方法将所有人分进两组时,返回 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 <= 104
  • dislikes[i].length == 2
  • 1 <= dislikes[i][j] <= n
  • ai < bi
  • dislikes 中每一组都 不同

Submission

运行时间: 72 ms

内存: 21.7 MB

class Solution:
    def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
        graph = [[] for _ in range(n+1)]
        for i, j in dislikes:
            graph[i].append(j)
            graph[j].append(i)

        left = [False]*(n+1)
        right = [False]*(n+1)
        visited = [False]*(n+1)

        def dfs(v, is_left):
            if visited[v]:
                return (is_left and left[v]) or (not is_left and right[v])
            if is_left:
                left[v]=True
            else:
                right[v]=True
            visited[v]=True
            for i in graph[v]:
                if not dfs(i, not is_left):
                    return False
            return True
        
        for i in range(1, n+1):
            if not visited[i] and not dfs(i, True):
                return False
        return True
            

Explain

该题解采用了图的深度优先搜索(DFS)算法来解决问题。首先,将给定的不喜欢关系转化为一个图的邻接表表示,其中每个节点代表一个人,每条边代表两个人之间的不喜欢关系。然后,尝试将图分为两个不相交的子集(即两个组),使得每个组内的人都不互相不喜欢。为此,我们使用DFS遍历图,同时用两个数组 'left' 和 'right' 分别记录每个人是否属于左组或右组。在遍历过程中,如果遇到一个已经访问过的节点,我们检查它是否已经被分配到了正确的组中;如果遇到一个未访问过的节点,我们将其分配到当前组的对立组中,并继续DFS遍历其邻居。如果在任何时候,我们发现无法将一个节点分配到一个组中而不违反不喜欢关系,我们返回 false。如果整个图都被成功地遍历并分组,我们返回 true。

时间复杂度: O(V + E) 或 O(n + dislikes.length)

空间复杂度: O(V + E) 或 O(n + dislikes.length)

class Solution:
    def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
        graph = [[] for _ in range(n+1)]  # 创建图的邻接表
        for i, j in dislikes:
            graph[i].append(j)  # 添加边
            graph[j].append(i)

        left = [False]*(n+1)  # 记录节点是否属于左组
        right = [False]*(n+1)  # 记录节点是否属于右组
        visited = [False]*(n+1)  # 记录节点是否被访问过

        def dfs(v, is_left):
            if visited[v]:
                return (is_left and left[v]) or (not is_left and right[v])
            if is_left:
                left[v]=True
            else:
                right[v]=True
            visited[v]=True
            for i in graph[v]:
                if not dfs(i, not is_left):
                    return False
            return True
        
        for i in range(1, n+1):
            if not visited[i] and not dfs(i, True):
                return False
        return True

Explore

在DFS过程中,节点的分组基于其父节点的分组情况来决定。具体的规则是:如果当前节点(我们尝试分配的节点)是从另一个已分组的节点访问到的,那么它应当被分配到与父节点相反的组中。这是因为题目要求每个组内的成员不互相不喜欢,所以如果父节点在左组,当前节点就应该分到右组,反之亦然。这种分组策略是为了确保所有不喜欢的关系都横跨两个组,而不是在同一个组内。

在此上下文中,‘正确的组’指的是一个节点根据其与其他节点的不喜欢关系应该所在的组。具体来说,如果我们通过DFS访问到一个已经标记为访问过的节点,我们需要检查这个节点之前的分组是否与当前尝试分配的组相反。例如,如果我们当前在左组中的节点遍历到一个右组的节点,这是符合预期的;但如果发现这个节点已经在左组,那么就存在矛盾,因为它同时与左组中的一个节点有不喜欢关系,这违反了分组条件。

这种情况的检测通过DFS递归过程实现。在DFS中,每当我们尝试将一个节点分配到一个组时,我们会递归地尝试将与之有不喜欢关系的节点分配到相反的组。如果在此过程中,我们尝试将一个节点分配到一个组,而这个节点已经在相反的组中(由之前的DFS过程确定),则意味着无法满足分组要求(即存在同一组内的节点互相不喜欢)。这时,DFS将返回false,传递到上层递归调用中,最终结果为无法进行有效分组。