leetcode
leetcode 1101 ~ 1150
查找集群内的关键连接

查找集群内的关键连接

难度:

标签:

题目描述

力扣数据中心有 n 台服务器,分别按从 0 到 n-1 的方式进行了编号。它们之间以 服务器到服务器 的形式相互连接组成了一个内部集群,连接是无向的。用  connections 表示集群网络,connections[i] = [a, b] 表示服务器 a 和 b 之间形成连接。任何服务器都可以直接或者间接地通过网络到达任何其他服务器。

关键连接 是在该集群中的重要连接,假如我们将它移除,便会导致某些服务器无法访问其他服务器。

请你以任意顺序返回该集群内的所有 关键连接

 

示例 1:

输入:n = 4, connections = [[0,1],[1,2],[2,0],[1,3]]
输出:[[1,3]]
解释:[[3,1]] 也是正确的。

示例 2:

输入:n = 2, connections = [[0,1]]
输出:[[0,1]]

 

提示:

  • 2 <= n <= 105
  • n - 1 <= connections.length <= 105
  • 0 <= ai, bi <= n - 1
  • ai != bi
  • 不存在重复的连接

代码结果

运行时间: 305 ms, 内存: 60.2 MB


// 思路:与传统Java实现类似,这里依然使用Tarjan算法。我们用Stream来处理输入并输出结果。
// 但由于DFS的递归特性,核心算法部分并没有使用Stream来替代。

import java.util.*;
import java.util.stream.Collectors;

public class CriticalConnectionsStream {
    private int time = 0;
    private List<List<Integer>> result = new ArrayList<>();
    private int[] ids, low;
    private List<Integer>[] graph;

    public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) {
        graph = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            graph[i] = new ArrayList<>();
        }
        connections.forEach(conn -> {
            graph[conn.get(0)].add(conn.get(1));
            graph[conn.get(1)].add(conn.get(0));
        });
        ids = new int[n];
        low = new int[n];
        Arrays.fill(ids, -1);
        for (int i = 0; i < n; i++) {
            if (ids[i] == -1) {
                dfs(i, -1);
            }
        }
        return result.stream().collect(Collectors.toList());
    }

    private void dfs(int u, int parent) {
        ids[u] = low[u] = ++time;
        for (int v : graph[u]) {
            if (v == parent) continue;
            if (ids[v] == -1) { // v is not visited
                dfs(v, u);
                low[u] = Math.min(low[u], low[v]);
                if (low[v] > ids[u]) {
                    result.add(Arrays.asList(u, v));
                }
            } else {
                low[u] = Math.min(low[u], ids[v]);
            }
        }
    }
}

解释

方法:

这个题解使用了Tarjan算法来查找无向图中的桥(关键连接)。该算法通过深度优先搜索(DFS)来遍历图,并使用一个数组ids来记录每个节点在DFS树中的编号。对于每个节点,我们用它的编号初始化ids数组,然后在DFS过程中更新ids值。如果一个节点的编号等于它在ids数组中的值,且它不是根节点,那么该节点与它的父节点之间的边就是一座桥。这是因为从根节点到该节点的路径上没有其他路径可以到达该节点。

时间复杂度:

O(V+E)

空间复杂度:

O(V+E)

代码细节讲解

🦆
在Tarjan算法中,为什么需要使用一个数组ids来记录每个节点在DFS树中的编号?
在Tarjan算法中,使用数组ids来记录每个节点在深度优先搜索(DFS)中的访问编号是为了帮助识别图中的桥。每个节点在第一次被访问时被分配一个唯一的编号,这个编号反映了它在DFS遍历中的访问顺序。通过比较节点及其邻居的编号,算法可以确定某个节点是否能够通过另一条非直接连接的路径连接回它的祖先节点,从而判断一个节点与其父节点之间的连接是否是桥。如果从一个节点到其任何祖先的其他路径不存在,则该连接为桥。
🦆
你是如何处理图中可能存在的环的情况?在这种情况下,算法的行为是怎样的?
在图中存在环的情况下,Tarjan算法通过检查已访问的邻居节点的ids值来处理环。当遇到一个已访问的邻居节点时,算法会使用这个邻居的ids值来更新当前节点的ids值。如果通过邻居节点可以找到一条到达当前节点祖先的路径,那么当前节点的ids值会被更新为较小的值,表示当前节点通过邻居节点可以回溯到更早的祖先节点。这种更新机制确保了算法可以正确处理环的存在,不会错误地将环内的连接标记为桥。
🦆
题解中提到,如果一个节点的编号等于它在ids数组中的值,并且它不是根节点,这条边就是桥。能否详细解释为什么满足这两个条件的边会是桥?
在Tarjan算法中,如果一个节点的ids值在DFS完成后仍然等于其初始时分配的编号,并且该节点不是根节点,这表明从该节点到其父节点的连接没有其他替代路径。也就是说,即使该节点与其父节点之间的直接连接被去除,也没有其他路径可以从该节点回到它的任何祖先节点。这样的边被称为桥,因为它是连接两个分离部分的唯一路径。
🦆
题解中的 `dfs` 函数递归调用时,cur_id参数递增,这是基于什么考虑?增加cur_id的作用是什么?
在题解中的`dfs`函数递归调用时,参数`cur_id`递增是为了确保每个节点在DFS遍历中都能获得一个唯一且递增的访问编号。这个编号帮助算法跟踪遍历的顺序和构建DFS树。递增`cur_id`的作用是为每个新访问的节点提供一个新的、比之前所有节点都大的编号,这有助于算法后续判断节点间的连接关系,尤其是在检测桥的过程中非常关键。每个节点的编号不仅代表了它的访问顺序,还作为一个标记,用于判断是否有其他路径可以到达该节点的祖先。

相关问题