代码随想录算法训练营第五十六天 | 108.冗余连接,109.冗余连接II

目录

108.冗余连接

思路

方法一: 并查集

心得收获 

109.冗余连接II 

思路

方法一:并查集


108.冗余连接

有一个图,它是一棵树,他是拥有 n 个节点和 n - 1 条边的连通无环无向图(其实就是一个线形图),如图:

现在在这棵树上的基础上,添加一条边(依然是n个节点,但有n条边),使这个图变成了有环图,如图

先请你找出冗余边,删除后,使该图可以重新变成一棵树。

输入描述

第一行包含一个整数 N,表示图的节点个数和边的个数。

后续 N 行,每行包含两个整数 s 和 t,表示图中 s 和 t 之间有一条边。

输出描述

输出一条可以删除的边。如果有多个答案,请删除标准输入中最后出现的那条边。

输入示例

3
1 2
2 3
1 3

输出示例

1 3

提示信息

图中的 1 2,2 3,1 3 等三条边在删除后都能使原图变为一棵合法的树。但是 1 3 由于是标准输出里最后出现的那条边,所以输出结果为 1 3

数据范围:

1 <= N <= 1000

思路

这道题目也是并查集基础题目。

这里我依然降调一下,并查集可以解决什么问题:两个节点是否在一个集合,也可以将两个节点添加到一个集合中。

如果还不了解并查集,可以看这里:并查集理论基础

我们再来看一下这道题目。

题目说是无向图,返回一条可以删去的边,使得结果图是一个有着N个节点的树(即:只有一个根节点)。

如果有多个答案,则返回二维数组中最后出现的边。

那么我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。

如图所示,节点A 和节点 B 不在同一个集合,那么就可以将两个 节点连在一起。

如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了。

如图所示:

已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和 节点B 连在一起就一定会出现环。

方法一: 并查集

class UnionFind:
    def __init__(self,n) -> None:
        self.father = list(i for i in range(n+1))

    def find(self,u):
        
        if u != self.father[u]:
            self.father[u] = self.find(self.father[u])
        return self.father[u]
    
    def join(self,u,v):
        u = self.find(u)
        v = self.find(v)
        if u == v: return
        self.father[v] = u

    def is_same(self,u,v):
        u = self.find(u)
        v = self.find(v)

        return u == v
    
def main():
    n = int(input())

    g = UnionFind(n)

    for i in range(n):
        u,v = map(int,input().split())
        if g.is_same(u,v):
            print(f'{u} {v}')
        else:
            g.join(u,v)

if __name__ == "__main__":
    main()

心得收获 

题目要求 “请删除标准输入中最后出现的那条边” ,不少录友疑惑,这代码分明是遇到在同一个根的两个节点立刻就返回了,怎么就求出 最后出现的那条边 了呢。

有这种疑惑的录友是 认为发现一条冗余边后,后面还可能会有一条冗余边。

其实并不会。

题目是在 树的基础上 添加一条边,所以冗余边仅仅是一条。

到这一条可能靠前出现,可能靠后出现。

例如,题目输入示例:

输入示例

3
1 2
2 3
1 3

图:

输出示例

1 3

当我们从前向后遍历,优先让前面的边连上,最后判断冗余边就是 1 3。

如果我们从后向前便利,优先让后面的边连上,最后判断的冗余边就是 1 2。

题目要求“请删除标准输入中最后出现的那条边”,所以 1 3 这条边才是我们要求的。

109.冗余连接II 

有一种有向树,该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。如图:

现在有一个有向图,有向图是在有向树中的两个没有直接链接的节点中间添加一条有向边。如图:

输入一个有向图,该图由一个有着 n 个节点(节点编号 从 1 到 n),n 条边,请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一颗有向树。

输入描述

第一行输入一个整数 N,表示有向图中节点和边的个数。

后续 N 行,每行输入两个整数 s 和 t,代表这是 s 节点连接并指向 t 节点的单向边

输出描述

输出一条可以删除的边,若有多条边可以删除,请输出标准输入中最后出现的一条边。

输入示例

3
1 2
1 3
2 3

输出示例

2 3

提示信息

在删除 2 3 后有向图可以变为一棵合法的有向树,所以输出 2 3

数据范围:

1 <= N <= 1000.

思路

本题与 108.冗余连接 类似,但本题是一个有向图,有向图相对要复杂一些。

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

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

我们来想一下 有向树的性质,如果是有向树的话,只有根节点入度为0,其他节点入度都为1(因为该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点)。

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

如图:

找到了节点3 的入度为2,删 1 -> 3 或者 2 -> 3 。选择删顺序靠后便可。

但 入度为2 还有一种情况,情况二,只能删特定的一条边,如图:

节点3 的入度为 2,但在删除边的时候,只能删 这条边(节点1 -> 节点3),如果删这条边(节点4 -> 节点3),那么删后本图也不是有向树了(因为找不到根节点)。

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

情况三: 如果没有入度为2的点,说明 图中有环了(注意是有向环)。

如图:

对于情况三,删掉构成环的边就可以了。

方法一:并查集

class UnionFind:
    # 并查集初始化
    def __init__(self,n) -> None:
        self.father = list(i for i in range(n+1))

    # 并查集里寻根的过程
    def find(self,u):
        
        if u != self.father[u]:
            self.father[u] = self.find(self.father[u])
        return self.father[u]
    
    #  将v->u 这条边加入并查集
    def join(self,u,v):
        u = self.find(u)
        v = self.find(v)
        if u == v: return
        self.father[v] = u

    # 判断 u 和 v是否找到同一个根
    def is_same(self,u,v):
        u = self.find(u)
        v = self.find(v)

        return u == v
    # 在有向图里找到删除的那条边,使其变成树
    def getRemoveEdge(self,edges,n):
        # 遍历所有的边
        for i in range(n): 
            # 构成有向环了,就是要删除的边
            if self.is_same(edges[i][0], edges[i][1]) : 
                print(f"{edges[i][0]} {edges[i][1]}")
                return
            else:
                self.join(edges[i][0], edges[i][1])
        
    def isTreeAfterRemoveEdge(self,edges,deleteEdge,n) :
        for i in range(n) :
            if i == deleteEdge: continue
            # 构成有向环了,一定不是树
            if self.is_same(edges[i][0], edges[i][1]) : 
                return False
            self.join(edges[i][0], edges[i][1])
    
        return True



def main():
    n = int(input())
    edges = []
    # 记录节点入度
    inDegree = [0] * (n+1)
    for i in range(n):
        s,t = map(int,input().split())
        inDegree[t] += 1
        edges.append([s, t])
    # 记录入度为2的边(如果有的话就两条边)

    vec = []
    # 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边
    for i in range(n-1,-1,-1) :
        if inDegree[edges[i][1]] == 2 :
            vec.append(i)
    g = UnionFind(n)
    # 情况一、情况二
    if len(vec) > 0:
        # 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边
        if g.isTreeAfterRemoveEdge(edges, vec[0],n):
            print(f'{edges[vec[0]][0]} {edges[vec[0]][1]}')
        else:
            print(f'{edges[vec[1]][0]} {edges[vec[1]][1]}')
        
        return 0
    
    # 处理情况三
    # 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了
    g.getRemoveEdge(edges,n)
if __name__ == "__main__":
    main()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值