算法题 冗余连接

冗余连接

问题描述

在本问题中,树指的是一个连通且无环的无向图。

给定一个由 N 个节点(节点值从 1 到 N)组成的树,以及一条额外的边。结果图是连通的,但有且仅有一个环。

返回一条可以删除的边,使得结果图重新成为一棵树。如果有多个答案,则返回最后出现的那条边。

注意:边的表示形式为 [u, v],其中 u < v

示例

输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图如下:
  1
 / \
2 - 3
删除边 [2,3] 后,图变成一棵树。

输入: [[1,2], [2,3], [3,1], [1,4]]
输出: [3,1]
解释: 给定的无向图如下:
  1 - 4
 / \
2 - 3
删除边 [3,1] 后,图变成一棵树。

算法思路

并查集

  1. 核心思想:树有 N 个节点和 N-1 条边,添加一条额外边后形成 N 条边和一个环
  2. 检测环
    • 遍历每条边,尝试将其两个端点连接
    • 如果两个端点已经连通,说明添加这条边会形成环
    • 这条边就是冗余边

操作

  • find(x):查找 x 的根节点(带路径压缩优化)
  • union(x, y):合并 x 和 y 所在的集合
  • 如果 find(x) == find(y),说明 x 和 y 已经连通

代码实现

方法一:并查集

class Solution {
    /**
     * 找到冗余连接,使得删除后图重新成为树
     * 使用并查集检测环
     * 
     * @param edges 无向图的边列表
     * @return 冗余的边(最后出现的形成环的边)
     */
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length; // 节点数为边数(因为有冗余边)
        
        // 初始化并查集,节点编号从1到n
        UnionFind uf = new UnionFind(n + 1); // 索引0不使用
        
        // 按顺序处理每条边
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            
            // 如果两个节点已经连通,这条边就是冗余的
            if (uf.find(u) == uf.find(v)) {
                return edge;
            }
            
            // 否则合并两个节点
            uf.union(u, v);
        }
        
        // 理论上不会执行到这里
        return new int[0];
    }
    
    /**
     * 并查集实现类
     */
    static class UnionFind {
        private int[] parent;
        private int[] rank; // 用于按秩合并优化
        
        public UnionFind(int size) {
            parent = new int[size];
            rank = new int[size];
            
            // 初始化:每个节点的父节点是自己
            for (int i = 0; i < size; i++) {
                parent[i] = i;
                rank[i] = 0;
            }
        }
        
        /**
         * 查找x的根节点(带路径压缩)
         */
        public int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]); // 路径压缩
            }
            return parent[x];
        }
        
        /**
         * 合并x和y所在的集合(按秩合并)
         */
        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            
            if (rootX != rootY) {
                // 按秩合并:将较小秩的树连接到较大秩的树
                if (rank[rootX] < rank[rootY]) {
                    parent[rootX] = rootY;
                } else if (rank[rootX] > rank[rootY]) {
                    parent[rootY] = rootX;
                } else {
                    parent[rootY] = rootX;
                    rank[rootX]++;
                }
            }
        }
    }
}

方法二:简化并查集(无按秩合并)

class Solution {
    /**
     * 简化并查集,只使用路径压缩
     */
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length;
        int[] parent = new int[n + 1];
        
        // 初始化:每个节点的父节点是自己
        for (int i = 1; i <= n; i++) {
            parent[i] = i;
        }
        
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            
            int rootU = find(parent, u);
            int rootV = find(parent, v);
            
            if (rootU == rootV) {
                return edge; // 找到冗余边
            }
            
            // 合并两个集合
            parent[rootU] = rootV;
        }
        
        return new int[0];
    }
    
    /**
     * 带路径压缩的查找操作
     */
    private int find(int[] parent, int x) {
        if (parent[x] != x) {
            parent[x] = find(parent, parent[x]);
        }
        return parent[x];
    }
}

方法三:DFS检测环

import java.util.*;

class Solution {
    /**
     * 使用DFS检测环的方法
     * 时间复杂度较高,逻辑直观
     */
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length;
        List<List<Integer>> graph = new ArrayList<>();
        
        // 初始化邻接表
        for (int i = 0; i <= n; i++) {
            graph.add(new ArrayList<>());
        }
        
        // 逐条添加边,每次添加后检测是否形成环
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            
            // 在添加当前边之前,检查u和v是否已经连通
            if (hasPath(graph, u, v, new boolean[n + 1])) {
                return edge; // 如果已经连通,这条边会形成环
            }
            
            // 添加当前边到图中
            graph.get(u).add(v);
            graph.get(v).add(u);
        }
        
        return new int[0];
    }
    
    /**
     * 使用DFS检查从start到end是否存在路径
     */
    private boolean hasPath(List<List<Integer>> graph, int start, int end, boolean[] visited) {
        if (start == end) {
            return true;
        }
        
        visited[start] = true;
        for (int neighbor : graph.get(start)) {
            if (!visited[neighbor] && hasPath(graph, neighbor, end, visited)) {
                return true;
            }
        }
        return false;
    }
}

算法分析

  • 时间复杂度
    • 并查集:O(N × α(N))
    • DFS:O(N²),每条边都要做一次DFS
  • 空间复杂度
    • 并查集:O(N),存储父节点数组
    • DFS:O(N),存储邻接表和访问数组

算法过程

输入:edges = [[1,2], [1,3], [2,3]]

并查集

  1. 初始化:parent = [0,1,2,3]
  2. 处理边[1,2]:
    • find(1)=1, find(2)=2 → 不同集合
    • union(1,2)parent = [0,1,1,3]
  3. 处理边[1,3]:
    • find(1)=1, find(3)=3 → 不同集合
    • union(1,3)parent = [0,1,1,1]
  4. 处理边[2,3]:
    • find(2)=1, find(3)=1 → 相同集合!
    • 返回[2,3]

路径压缩

  • 初始:1→2, 1→3
  • 查询find(2)时:2→1(直接指向根)
  • 查询find(3)时:3→1(直接指向根)

测试用例

public class TestFindRedundantConnection {
    
    public static void printArray(int[] arr) {
        System.out.print("[");
        for (int i = 0; i < arr.length; i++) {
            if (i > 0) System.out.print(", ");
            System.out.print(arr[i]);
        }
        System.out.println("]");
    }
    
    public static void main(String[] args) {
        Solution solution = new Solution();
        
        // 测试用例1:标准示例1
        int[][] edges1 = {{1,2}, {1,3}, {2,3}};
        int[] result1 = solution.findRedundantConnection(edges1);
        System.out.print("Test 1: ");
        printArray(result1); // [2, 3]
        
        // 测试用例2:标准示例2
        int[][] edges2 = {{1,2}, {2,3}, {3,1}, {1,4}};
        int[] result2 = solution.findRedundantConnection(edges2);
        System.out.print("Test 2: ");
        printArray(result2); // [3, 1]
        
        // 测试用例3:更大的图
        int[][] edges3 = {{1,2}, {1,3}, {1,4}, {2,5}, {2,6}, {3,7}, {3,8}, {4,9}, {4,10}, {5,6}};
        int[] result3 = solution.findRedundantConnection(edges3);
        System.out.print("Test 3: ");
        printArray(result3); // [5, 6]
        
        // 测试用例4:线性链 + 回边
        int[][] edges4 = {{1,2}, {2,3}, {3,4}, {4,5}, {5,1}};
        int[] result4 = solution.findRedundantConnection(edges4);
        System.out.print("Test 4: ");
        printArray(result4); // [5, 1]
        
        // 测试用例5:星型结构
        int[][] edges5 = {{1,2}, {1,3}, {1,4}, {1,5}, {2,3}};
        int[] result5 = solution.findRedundantConnection(edges5);
        System.out.print("Test 5: ");
        printArray(result5); // [2, 3]
        
        // 测试用例6:最小情况
        int[][] edges6 = {{1,2}, {1,2}};
        int[] result6 = solution.findRedundantConnection(edges6);
        System.out.print("Test 6: ");
        printArray(result6); // [1, 2]
        
        // 测试用例7:复杂环
        int[][] edges7 = {{1,3}, {3,4}, {4,2}, {2,1}, {1,5}};
        int[] result7 = solution.findRedundantConnection(edges7);
        System.out.print("Test 7: ");
        printArray(result7); // [2, 1]
    }
}

关键点

  1. 并查集

    • 高效检测两个节点是否已经连通
    • 动态维护连通分量
    • 路径压缩和按秩合并优化性能
  2. 为什么按输入顺序处理就能得到正确答案?

    • 要求返回"最后出现"的冗余边
    • 输入数组已经按边的出现顺序排列
    • 第一个形成环的边就是答案(因为之前的边都没形成环)
    • N个节点的树有N-1条边
    • N个节点N条边必然有且仅有一个环
    • 删除环中的任意一条边都能恢复树的结构
  3. 优化

    • 路径压缩:在find操作中将路径上的所有节点直接指向根
    • 按秩合并:将较小的树合并到较大的树,保持树的平衡

常见问题

  1. 为什么不用检测所有可能的冗余边?
    • 保证只有一个环,所以第一个检测到的冗余边就是答案
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值