图论 14. 冗余连接II(比较难的并查集)

图论 14. 冗余连接II(比较难的并查集)

109. 冗余连接II

代码随想录

卡码网无难度标识

这道题与图论 13. 冗余连接-CSDN博客的区别就是,从无向图变成了有向图

这道题就比较复杂了,没做出来(主要是没有分清楚两种情况,以及第一种情况的子情况,实际上是对问题的解读不够透彻)

  • 思路:(直接摘录、修改自代码随想录的思路)

    • 本题的本质是 :

      • 有一个有向图,是由一颗有向树 + 一条有向边组成的 (所以此时这个图就不能称之为有向树),现在让我们找到那条边 把这条边删了,让这个图恢复为有向树。

      • 还有“若有多条边可以删除,请输出标准输入中最后出现的一条边”,这说明在两条边都可以删除的情况下,要删顺序靠后的边!

    • 有向树的定义:

      有向树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点

      有向树的性质:

      如果是有向树的话,只有根节点入度为0,其他节点入度都为1。

      因此异常情况可能是:出现了一个入度为2的点(情况一),或者所有点入度都为1(情况二)

    • 情况一:

      一颗有向树 + 一条有向边可能导致出现(也最多只会出现)一个入度为2的点,

      如果我们找到入度为2的点,那么删一条指向该节点的边就行了。

      (入度为2的点有两条入边,但是删除哪一条能保证回到有向树是不一定的,所以也要分两种子情况)

      1. 情况一之一:随便删其中一条入边都可以,那么删顺序靠后的入边

        例如case: 1-> 2, 1 -> 3, 2 -> 3

        节点3 的入度为2,删 1 -> 3 或者 2 -> 3 均能回到有向树。

        选择删顺序靠后便可。

      2. 情况一之二:只能删特定的一条入边

        例如case: 1-> 3, 3 -> 2, 2 -> 1, 4 -> 3

        节点3 的入度为 2,但在删除边的时候,只能删 1 -> 3

        如果删4 -> 3,那么删后本图也不是有向树了(因为找不到根节点)。

      综上,如果发现入度为2的节点,我们需要判断 删除哪一条边,删除后本图能成为有向树。如果是删哪个都可以,优先删顺序靠后的边。

      (所以两种情况合并就是,找到入度为2的结点,从顺序靠后的入边开始判断是否删除后能成为有向树即可)

    • 情况一代码要点:

      1. 根据每条边,统计节点入度,存在列表inDegree​中

      2. 找到入度为2的结点(并获取它的两条入边),从顺序靠后的入边开始判断是否删除后能成为有向树即可

      3. isTreeAfterRemoveEdge()判断删一个边之后是不是有向树:

        每判断一次,要开一个新的并查集。

        将所有边的两端节点分别加入并查集,遇到当前待删除的边则跳过

        如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环,

        要删掉当前待删除的边

    • 情况二:

      要删掉构成环的边

      由于经过前面情况一的判断,此时明确没有入度为2的结点,

      同时,由于本题中明确指出,必然出现异常,那么剩下的一定是落入**其他节点入度都为1**的情况,

      此时出现有向环,找到构成环的边就是要删除的边。

    • 情况二代码要点:

      getRemoveEdge()查找在情况二之下需要删除的边:

      同样也需要开一个新的并查集。

      将所有边的两端节点分别加入并查集,如果遇到当前待加入并查集的边,其两端节点本来就都在并查集(同根)了,说明构成了环,删除该边即可。

  • 代码:

    import sys
    
    class unionFind():
        def __init__(self, n):
            self.n = n # 元素数量 
            self.father = list(range(n + 1)) # 结点编号1~n对应的父结点映射
    
        def find(self, u):
            if self.father[u] != u:
                self.father[u] = self.find(self.father[u])
            return self.father[u]
    
        def isSame(self, u, v):
            return self.find(u) == self.find(v)
    
        def join(self, u, v):
            root_u = self.find(u)
            root_v = self.find(v)
            if root_u == root_v: # 当前待加入边的两端结点已经在并查集中了,在本题目中说明此次join会导致成环
                return True
            else: # 正常加入
                self.father[root_v] = root_u
                return False
    
    def isTreeAfterRemoveEdge(n, edges, deleteEdge):
        # n结点数量,edges图的所有边
        # deleteEdge当前待删除边
        # 创建并查集,同时更新最新的possible冗余边
        # 返回值:当前待删除边被删除后,图是否变成了树(是否不再成环)
        uf = unionFind(n) # 开一个并查集
        for edge in edges:
            if edge != deleteEdge: # 跳过待删除边
                if uf.join(edge[0], edge[1]): # join的返回值为当前是否成环
                    # 若成环,说明即便删除当前待删除边,环也没有消除,所以当前待删除边不是所要的删除边
                    return False
        return True # 遍历完毕没有出现环,说明当前待删除边就是需要的删除边
    
    
    def getRemoveEdge(n, edges):
        # 将所有边的两端节点分别加入并查集,如果遇到当前待加入并查集的边,其两端节点本来就都在并查集(同根)了,说明构成了环,删除该边即可。
        # n结点数量,edges图的所有边
        # 返回情况二下找到的待删除边
        uf = unionFind(n) # 开一个并查集
        for edge in edges:
            if uf.join(edge[0], edge[1]): # join的返回值为当前是否成环
                # 若成环,说明当前边就是情况二下要删除的边,删除即可破环恢复为树
                return edge
    
    
    def main():
        # 注意本题有向树的定义:
        # 有向树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点
        # 注意根结点是出发结点
    
        # 在将一条边加入并查集(join)的时候,如果该边两个结点同根,说明两结点的边是冗余的,返回一个True作为标记;否则返回一个False。
        # 然后找出最下面的冗余边即可(标记最新的冗余边的索引)
        lines = sys.stdin.readlines()
    
        n = int(lines[0].strip()) # 结点个数=边数=n
    
        # 从输入获取边的信息,并获取各结点入度信息
        edges = [] # 存储有向图的所有有向边
        inDegree = [0] * (n + 1) # 存储结点编号对应的入度
        for i in range(1, len(lines)):
            s, t = map(int, lines[i].strip().split()) 
            edges.append([s, t]) # 边 s -> t
            inDegree[t] += 1 # 更新t结点入度
    
        # 进入情况一的查询
        # 查找是否存在入度为2的结点,若有,存储它的两条入边
        in_edges = [] # 存储情况一中入度为2的结点的两条入边
        for edge in edges: # 用遍历边的方式查找
            if inDegree[edge[1]] == 2:
                in_edges.append(edge)
    
        # 如果in_edges非None,说明确实为情况1
        if in_edges:
            # 倒叙查询,先查询顺序靠后出现的待删除边
            if isTreeAfterRemoveEdge(n, edges, in_edges[1]): # 尝试删除当前待删除边in_edges[1]
                print(" ".join(map(str, in_edges[1])))
            else: # 若in_edges[1]不符合删除条件,那么就说明只可能删除另一条边in_edges[0]了!
                print(" ".join(map(str, in_edges[0])))
        # 进入情况二的查询
        else:
            deleteEdge = getRemoveEdge(n, edges)
            print(" ".join(map(str, deleteEdge)))
    
    
    if __name__ == '__main__':
        main()
    
    
    

你可能感兴趣的:(小白的代码随想录刷题笔记,Mophead的小白刷题笔记,leetcode,python,代码随想录,图论)