目录
1. 问题描述
给定一组 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 <= 10^4
dislikes[i].length == 2
1 <= dislikes[i][j] <= n
ai < bi
dislikes
中每一组都 不同
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/possible-bipartition
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
2. 解题分析
2.1 思路一
dislikes代表一个图的edges列表表示,姑且叫这个图为dislike-graph。
一个朴素的想法是,构建这个图的补图like-graph,然后检查该补图like-graph的连通子图个数是不是小于等于2。在补图like-graph中,两个不同连通子图中的节点显然是相互dislike的。因此如果like-graph的连通子图个数超过了2个,就无法分成2个小组,确保小组内没有相互dislike的人。而如果like-graph的连通子图个数等于2,那这两个子图中包含的节点集合就是满足题意的唯一的分组方式。如果like-graph的连通子图个数等于1(即为全联通图),那就可以任意划分了。
当然,这个like-graph并不需要显式地创建。
只需要创建dislike-graph的邻接列表(创建like-graph的邻接列表比创建dislike-graph的邻接列表要麻烦,存储需求大),然后基于dislike-graph的邻接列表进行like-graph的遍历即可,广度优先搜索或深度优先搜索均可。
不过画了画图,从示例3的图发现以上这个想法并不正确。
2.2 思路二
事实上这个问题等价于寻找这个dislike-graph中是否存在奇数个节点构成的环。
(1) 存在奇数个节点构成的环的话,就无法把他们分成两组并避免不喜欢的人分到了一个组。这个是显而易见的。
(2) 不存在奇数个节点构成的环的情况下,是不是就一定能够分成满足条件的两组呢?
先假定以上这个思路成立吧(要证明的话好像还不是一件容易的事情)。
从任何一个节点出发,对图进行路径遍历,如果某个路径上的新探索的节点已经存在于该路径上,就表示找到了一个环路。找到环路后检查该环路的长度即可。
然后,重复以上过程,直到完成整个图的遍历。
如果找到一个长度为奇数的环即可退出并返回False。如果没有找到任何长度为奇数的环则返回True。
但是,针对某一条路径的搜索,是找到了一个环就退出呢,还是继续搜索呢?好像两者都行不通。
呃。。。这条路好像走不通
2.3 思路三:着色+深度优先搜索
尝试将每个人分配到两个组中的一个。假设第一组中的人是红色,第二组中的人是蓝色。
如果第一个人是红色的,那么这个人不喜欢的人必须是蓝色的。然后,任何不喜欢蓝色人的人都是红色的,那么任何不喜欢红色人的人都是蓝色的,依此类推。如果在任何时候存在冲突,那么这个任务是不可能的完成的,因为从第一步开始每一步都符合逻辑。如果没有冲突,那么着色是有效的,说明分成两组是可行的。
考虑由N个人构成的dislike-graph,dislikes数组表示改图的edge list。
基于以上思路,我们需要检查该图的每个连通分支是否为二分图。
对于每个连通的部分,我们只需试着用两种颜色对它进行着色,就可以检查它是否是二分的。可以按如下步骤进行深度优先搜索(本题的搜索与路径有关,不能用广度优先搜索):
- 将任一结点涂成红色,然后将它的所有邻居都涂成蓝色,然后将所有的邻居的邻居都涂成红色,以此类推。如果搜索过程发现某个准备涂成蓝色的节点之前已经被涂成红色(或者反过来)就表明产生冲突,分组无法成立,结束搜索。
- 以上遍历结束后,在从剩下未涂色的节点中任选一个开始新的遍历。
- 重复以上步骤2直到所有节点均已涂色(被访问),或者中途因发生冲突而提前退出。
3. 代码实现
import time
from typing import List
from collections import deque
class Solution:
def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
if n == 1:
return True
# construct adjacency list
adjlist = [[] for k in range(n+1)]
for dislike in dislikes:
adjlist[dislike[0]].append(dislike[1])
adjlist[dislike[1]].append(dislike[0])
color = (n+1) * [-1]
for k in range(1,n+1):
if color[k] < 0:
q = deque([k])
color[k] = 0
while len(q)>0:
node = q.pop()
for adj in adjlist[node]:
if color[adj] < 0:
color[adj] = 1 - color[node]
q.append(adj)
else:
if color[adj] == color[node]:
return False
return True
if __name__ == "__main__":
sln = Solution()
n = 4
dislikes = [[1,2],[1,3],[2,4]]
print(sln.possibleBipartition(n, dislikes))
n = 3
dislikes = [[1,2],[1,3],[2,3]]
print(sln.possibleBipartition(n, dislikes))
n = 5
dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]
print(sln.possibleBipartition(n, dislikes))
执行用时:108 ms, 在所有 Python3 提交中击败了91.64%的用户
内存消耗:18.4 MB, 在所有 Python3 提交中击败了89.10%的用户
嗯,能够在时间和内存两方面都表现不错,还是值得小小得意一下。
吐个小槽:n=1的时候按leetcode的反馈应该返回True,但是这其实是个定义的问题。leetcode拿这个当作一个testcase有些无聊了。不如直接定义n>=2不是更清爽嘛。
回到总目录: 笨牛慢耕的Leetcode每日一题总目录(动态更新。。。)