冗余连接问题

简介: 本节介绍如何使用并查集解决数据结构中的冗余连接问题。主要内容包括:树与图的区别,冗余连接问题的定义,以及利用并查集判断并找出图中多余的边。文中提供了 C、Java 和 Python 的完整实现代码,并通过示例详细讲解了解决过程。

接触过数据结构的读者都知道,数据结构里囊括了很多种存储数据的方案,包括线性表、树、图等。线性表的特征非常明显,很容易辨识,树和图是初学者经常混淆的。


在数据结构中,所有的树都可以看做是图型结构,但反过来,只有「连通且不存在环路的无向图」可以看做是树型结构。例如,观察下方哪个是树型结构,哪个是图型结构?


图 1 树和图的区别


图 1 中,c) 是有向图,无法被看做是树型结构。重点观察 a) 和 b),它们都是无向图,并且各自包含的顶点都是相互连通的,唯一的不同在于,b) 中存在 1-0-2-1 这条环路,而 a) 中不存在环路,因此 a) 既可以看做是一张图,也可以看做是一棵树;而 b) 只能看做是一张图。


本节要带领大家解决的冗余连接问题,指的是给定一张图型结构,它是由 n 个顶点的树外加一条边构成的,要求找出这条冗余的边。图中所有边的信息都记录在长度为 n 的二维数组 edges 里,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。如果有多条符合条件的边,则返回 edges 数组中最后出现的那个。


举个简单的例子,从下图中找到冗余的那条边。


图 2 给定的图


观察图 2 不难发现,冗余的边可以是 [1, 2]、[1, 3]、[2, 3] 中的任意一条,删除它们中的一个,剩下的就是一棵树。当然多条符合条件的边时,题目要求返回 edges 数组中最后出现的那个,也就是 [2, 3] 这条边。


冗余连接问题可以用并查集解决,接下来给大家讲解具体的实现过程。

并查集解决冗余连接问题

并查集解决冗余连接问题的思路是,依次判断 edges 数组中存储的各个边是否是冗余的,判断依据是:如果当前边两端的顶点不属于一个集合,就将它们合并为一个集合;反之,如果当前边两端的顶点属于同一个集合,表明两个顶点先前已经连通过了,再添加当前边会导致出现环路,因此当前边就是冗余的。


仍以图 2 为例,并查集解决冗余连接问题的过程是:

  • 初始状态下,顶点 1、2、3 各自属于不同的集合;
  • 检查 [1, 2] 边是否冗余:顶点 1 和 2 位于不同的集合里,因此 [1, 2] 不是冗余的,将顶点 1 和 2 合并为同一个集合;
  • 检查 [1, 3] 边是否冗余:顶点 1 和 3 位于不同的集合里,因此 [1, 3] 不是冗余的,将顶点 1 和 3 合并为同一个集合;
  • 检查 [2, 3] 边是否冗余:顶点 2 和 3 位于同一个集合里,因此 [2, 3] 是冗余的。


下面是用并查集解决冗余问题的 C 语言程序:

  1. #include <stdio.h>
  2. #include <stdlib.h>

  3. typedef enum { false, true } bool;
  4. #define MAX_SIZE 1000  // 可以根据需要调整最大尺寸

  5. // 并查集数组
  6. typedef struct {
  7. int parent[MAX_SIZE];
  8. int count;
  9. }UnionFind;

  10. // 初始化并查集
  11. void initialize(UnionFind* uf, int size) {
  12. for (int i = 0; i <= size; i++) {
  13.        uf->parent[i] = i;
  14. }
  15.    uf->count = size;
  16. }

  17. // 查找根节点(代表元素)
  18. int find(UnionFind* uf, int x) {
  19. if (uf->parent[x] != x) {
  20.        uf->parent[x] = find(uf, uf->parent[x]); // 路径压缩
  21. }
  22. return uf->parent[x];
  23. }

  24. // 合并两个元素所在的集合
  25. void unionSets(UnionFind* uf, int x, int y) {
  26. //找到两个元素所在集合的代表元素
  27. int xRoot = find(uf, x);
  28. int yRoot = find(uf, y);
  29. //如果代表元素不同,表明它们是两个集合,将它们合并
  30. if (xRoot != yRoot) {
  31.        uf->parent[xRoot] = yRoot; // uf->parent[yRoot] = xRoot;
  32.        uf->count--;
  33. }
  34. }

  35. int* findRedundantConnection(int(*edgs)[2], int n) {
  36. UnionFind uf;
  37. initialize(&uf, n);
  38. // 遍历所有的边
  39. for (int  i = 0; i < n; i++)
  40. {
  41. //判断当前边是否冗余
  42. if (find(&uf, edgs[i][0]) == find(&uf, edgs[i][1])) {
  43. return edgs[i];
  44. }
  45. else
  46. {
  47. unionSets(&uf, edgs[i][0], edgs[i][1]);
  48. }
  49. }
  50. }

  51. int main() {
  52. int edgs[3][2] = { {1,2},{1,3},{2,3} };

  53. //找到冗余的边
  54. int * ret = findRedundantConnection(edgs, 3);
  55. printf("冗余的边是:[%d, %d]\n", ret[0], ret[1]);
  56. return 0;
  57. }


下面是用并查集解决冗余问题的 Java 程序:

  1. // 并查集类,用于处理集合合并和查找操作(在 UnionFind.java 文件中)
  2. public class UnionFind {
  3. private int[] parent;  // 记录节点的父节点
  4. private int count;     // 记录集合的数量

  5. // 构造函数,初始化并查集,每个节点的父节点初始为自己,集合数量为节点个数
  6. public UnionFind(int size) {
  7.        parent = new int[size + 1];
  8. for (int i = 0; i <= size; i++) {
  9.            parent[i] = i;
  10. }
  11.        count = size;
  12. }

  13. // 查找根节点(代表元素)的方法,同时进行路径压缩
  14. public int find(int x) {
  15. if (parent[x] != x) {
  16.            parent[x] = find(parent[x]);  // 路径压缩
  17. }
  18. return parent[x];
  19. }

  20. // 合并两个元素所在的集合
  21. public void unionSets(int x, int y) {
  22. int xRoot = find(x);
  23. int yRoot = find(y);
  24. if (xRoot != yRoot) {
  25.            parent[xRoot] = yRoot;  // 合并集合
  26.            count--;
  27. }
  28. }

  29. // 获取当前集合的数量
  30. public int getCount() {
  31. return count;
  32. }
  33. }

  34. // FindRedundantConnection 类定义(在 FindRedundantConnection.java 文件中)
  35. public class FindRedundantConnection {
  36. // 查找冗余连接的方法
  37. public int[] findRedundantConnection(int[][] edges) {
  38. UnionFind uf = new UnionFind(edges.length);
  39. int[] result = new int[2];
  40. for (int[] edge : edges) {
  41. if (uf.find(edge[0]) == uf.find(edge[1])) {
  42.                result[0] = edge[0];
  43.                result[1] = edge[1];
  44. } else {
  45.                uf.unionSets(edge[0], edge[1]);
  46. }
  47. }
  48. return result;
  49. }

  50. public static void main(String[] args) {
  51. int[][] edges = { {1, 2}, {1, 3}, {2, 3} };

  52. FindRedundantConnection validPath = new FindRedundantConnection();
  53. int[] result = validPath.findRedundantConnection(edges);

  54.        System.out.println("冗余的边是:[" + result[0] + ", " + result[1] + "]");
  55. }
  56. }


下面是用并查集解决冗余问题的 Python 程序:

  1. class UnionFind:
  2. def __init__(self, size):
  3.        self.parent = [i for i in range(size + 1)]
  4.        self.count = size

  5. # 初始化并查集
  6. def initialize(self, size):
  7. for i in range(size + 1):
  8.            self.parent[i] = i
  9.        self.count = size

  10. # 查找操作,返回元素所在集合的代表元素
  11. def find(self, x):
  12. if self.parent[x] != x:
  13.            self.parent[x] = self.find(self.parent[x])  # 路径压缩
  14. return self.parent[x]

  15. # 合并两个集合
  16. def union_sets(self, x, y):
  17.        x_root = self.find(x)
  18.        y_root = self.find(y)
  19. if x_root != y_root:
  20.            self.parent[x_root] = y_root
  21.            self.count -= 1

  22. # 查找冗余连接的方法
  23. def find_redundant_connection(edges):
  24.    uf = UnionFind(len(edges))
  25.    result = [0, 0]
  26. for edge in edges:
  27. if uf.find(edge[0]) == uf.find(edge[1]):
  28.            result[0], result[1] = edge[0], edge[1]
  29. else:
  30.            uf.union_sets(edge[0], edge[1])
  31. return result


  32. if __name__ == "__main__":
  33.    edges = [[1, 2], [1, 3], [2, 3]]

  34.    ret = find_redundant_connection(edges)
  35. print(f"冗余的边是:[{ret[0]}, {ret[1]}]")


运行程序,输出结果为:

冗余的边是:[2, 3]接触过数据结构的读者都知道,数据结构里囊括了很多种存储数据的方案,包括线性表、树、图等。线性表的特征非常明显,很容易辨识,树和图是初学者经常混淆的。


在数据结构中,所有的树都可以看做是图型结构,但反过来,只有「连通且不存在环路的无向图」可以看做是树型结构。例如,观察下方哪个是树型结构,哪个是图型结构?


图 1 树和图的区别


图 1 中,c) 是有向图,无法被看做是树型结构。重点观察 a) 和 b),它们都是无向图,并且各自包含的顶点都是相互连通的,唯一的不同在于,b) 中存在 1-0-2-1 这条环路,而 a) 中不存在环路,因此 a) 既可以看做是一张图,也可以看做是一棵树;而 b) 只能看做是一张图。


本节要带领大家解决的冗余连接问题,指的是给定一张图型结构,它是由 n 个顶点的树外加一条边构成的,要求找出这条冗余的边。图中所有边的信息都记录在长度为 n 的二维数组 edges 里,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。如果有多条符合条件的边,则返回 edges 数组中最后出现的那个。


举个简单的例子,从下图中找到冗余的那条边。


图 2 给定的图


观察图 2 不难发现,冗余的边可以是 [1, 2]、[1, 3]、[2, 3] 中的任意一条,删除它们中的一个,剩下的就是一棵树。当然多条符合条件的边时,题目要求返回 edges 数组中最后出现的那个,也就是 [2, 3] 这条边。


冗余连接问题可以用并查集解决,接下来给大家讲解具体的实现过程。

并查集解决冗余连接问题

并查集解决冗余连接问题的思路是,依次判断 edges 数组中存储的各个边是否是冗余的,判断依据是:如果当前边两端的顶点不属于一个集合,就将它们合并为一个集合;反之,如果当前边两端的顶点属于同一个集合,表明两个顶点先前已经连通过了,再添加当前边会导致出现环路,因此当前边就是冗余的。


仍以图 2 为例,并查集解决冗余连接问题的过程是:

  • 初始状态下,顶点 1、2、3 各自属于不同的集合;
  • 检查 [1, 2] 边是否冗余:顶点 1 和 2 位于不同的集合里,因此 [1, 2] 不是冗余的,将顶点 1 和 2 合并为同一个集合;
  • 检查 [1, 3] 边是否冗余:顶点 1 和 3 位于不同的集合里,因此 [1, 3] 不是冗余的,将顶点 1 和 3 合并为同一个集合;
  • 检查 [2, 3] 边是否冗余:顶点 2 和 3 位于同一个集合里,因此 [2, 3] 是冗余的。


下面是用并查集解决冗余问题的 C 语言程序:

  1. #include <stdio.h>
  2. #include <stdlib.h>

  3. typedef enum { false, true } bool;
  4. #define MAX_SIZE 1000  // 可以根据需要调整最大尺寸

  5. // 并查集数组
  6. typedef struct {
  7. int parent[MAX_SIZE];
  8. int count;
  9. }UnionFind;

  10. // 初始化并查集
  11. void initialize(UnionFind* uf, int size) {
  12. for (int i = 0; i <= size; i++) {
  13.        uf->parent[i] = i;
  14. }
  15.    uf->count = size;
  16. }

  17. // 查找根节点(代表元素)
  18. int find(UnionFind* uf, int x) {
  19. if (uf->parent[x] != x) {
  20.        uf->parent[x] = find(uf, uf->parent[x]); // 路径压缩
  21. }
  22. return uf->parent[x];
  23. }

  24. // 合并两个元素所在的集合
  25. void unionSets(UnionFind* uf, int x, int y) {
  26. //找到两个元素所在集合的代表元素
  27. int xRoot = find(uf, x);
  28. int yRoot = find(uf, y);
  29. //如果代表元素不同,表明它们是两个集合,将它们合并
  30. if (xRoot != yRoot) {
  31.        uf->parent[xRoot] = yRoot; // uf->parent[yRoot] = xRoot;
  32.        uf->count--;
  33. }
  34. }

  35. int* findRedundantConnection(int(*edgs)[2], int n) {
  36. UnionFind uf;
  37. initialize(&uf, n);
  38. // 遍历所有的边
  39. for (int  i = 0; i < n; i++)
  40. {
  41. //判断当前边是否冗余
  42. if (find(&uf, edgs[i][0]) == find(&uf, edgs[i][1])) {
  43. return edgs[i];
  44. }
  45. else
  46. {
  47. unionSets(&uf, edgs[i][0], edgs[i][1]);
  48. }
  49. }
  50. }

  51. int main() {
  52. int edgs[3][2] = { {1,2},{1,3},{2,3} };

  53. //找到冗余的边
  54. int * ret = findRedundantConnection(edgs, 3);
  55. printf("冗余的边是:[%d, %d]\n", ret[0], ret[1]);
  56. return 0;
  57. }


下面是用并查集解决冗余问题的 Java 程序:

  1. // 并查集类,用于处理集合合并和查找操作(在 UnionFind.java 文件中)
  2. public class UnionFind {
  3. private int[] parent;  // 记录节点的父节点
  4. private int count;     // 记录集合的数量

  5. // 构造函数,初始化并查集,每个节点的父节点初始为自己,集合数量为节点个数
  6. public UnionFind(int size) {
  7.        parent = new int[size + 1];
  8. for (int i = 0; i <= size; i++) {
  9.            parent[i] = i;
  10. }
  11.        count = size;
  12. }

  13. // 查找根节点(代表元素)的方法,同时进行路径压缩
  14. public int find(int x) {
  15. if (parent[x] != x) {
  16.            parent[x] = find(parent[x]);  // 路径压缩
  17. }
  18. return parent[x];
  19. }

  20. // 合并两个元素所在的集合
  21. public void unionSets(int x, int y) {
  22. int xRoot = find(x);
  23. int yRoot = find(y);
  24. if (xRoot != yRoot) {
  25.            parent[xRoot] = yRoot;  // 合并集合
  26.            count--;
  27. }
  28. }

  29. // 获取当前集合的数量
  30. public int getCount() {
  31. return count;
  32. }
  33. }

  34. // FindRedundantConnection 类定义(在 FindRedundantConnection.java 文件中)
  35. public class FindRedundantConnection {
  36. // 查找冗余连接的方法
  37. public int[] findRedundantConnection(int[][] edges) {
  38. UnionFind uf = new UnionFind(edges.length);
  39. int[] result = new int[2];
  40. for (int[] edge : edges) {
  41. if (uf.find(edge[0]) == uf.find(edge[1])) {
  42.                result[0] = edge[0];
  43.                result[1] = edge[1];
  44. } else {
  45.                uf.unionSets(edge[0], edge[1]);
  46. }
  47. }
  48. return result;
  49. }

  50. public static void main(String[] args) {
  51. int[][] edges = { {1, 2}, {1, 3}, {2, 3} };

  52. FindRedundantConnection validPath = new FindRedundantConnection();
  53. int[] result = validPath.findRedundantConnection(edges);

  54.        System.out.println("冗余的边是:[" + result[0] + ", " + result[1] + "]");
  55. }
  56. }


下面是用并查集解决冗余问题的 Python 程序:

  1. class UnionFind:
  2. def __init__(self, size):
  3.        self.parent = [i for i in range(size + 1)]
  4.        self.count = size

  5. # 初始化并查集
  6. def initialize(self, size):
  7. for i in range(size + 1):
  8.            self.parent[i] = i
  9.        self.count = size

  10. # 查找操作,返回元素所在集合的代表元素
  11. def find(self, x):
  12. if self.parent[x] != x:
  13.            self.parent[x] = self.find(self.parent[x])  # 路径压缩
  14. return self.parent[x]

  15. # 合并两个集合
  16. def union_sets(self, x, y):
  17.        x_root = self.find(x)
  18.        y_root = self.find(y)
  19. if x_root != y_root:
  20.            self.parent[x_root] = y_root
  21.            self.count -= 1

  22. # 查找冗余连接的方法
  23. def find_redundant_connection(edges):
  24.    uf = UnionFind(len(edges))
  25.    result = [0, 0]
  26. for edge in edges:
  27. if uf.find(edge[0]) == uf.find(edge[1]):
  28.            result[0], result[1] = edge[0], edge[1]
  29. else:
  30.            uf.union_sets(edge[0], edge[1])
  31. return result


  32. if __name__ == "__main__":
  33.    edges = [[1, 2], [1, 3], [2, 3]]

  34.    ret = find_redundant_connection(edges)
  35. print(f"冗余的边是:[{ret[0]}, {ret[1]}]")


运行程序,输出结果为:

冗余的边是:[2, 3]

相关文章
|
3月前
|
算法 Java C语言
弗洛伊德算法求最短路径
弗洛伊德算法用于寻找加权图中各顶点间的最短路径,适用于无向图和有向图。算法基于动态规划思想,通过枚举中间顶点来更新路径,确保最终得到最短路径。该算法要求路径权值非负,否则可能出错。
358 0
|
3月前
|
存储 搜索推荐 算法
归并排序算法
归并排序是一种基于分治思想的高效排序算法,通过将序列不断划分为不可再分的子序列,再两两合并完成排序。其时间复杂度为O(nlogn),适用于对序列进行升序或降序排列。
257 0
|
3月前
|
算法 搜索推荐
分治算法的基本思想
分治算法通过将复杂问题拆分成多个独立的小问题,逐一解决后再合并结果。适合处理大规模数据,常见应用包括排序、查找最大值/最小值及汉诺塔等问题。
115 0
|
3月前
|
算法
回溯算法的基本思想
本节介绍回溯算法,通过图1中从A到K的路径查找示例,说明其与穷举法的异同。回溯算法通过“回退”机制高效试探各种路径,适用于决策、优化和枚举问题。
105 0
|
3月前
|
算法 Java 定位技术
迷宫问题
迷宫问题是指在给定区域内寻找从起点到终点的可行路径。可以使用回溯算法解决,通过不断尝试四个方向(上下左右)移动,若无法前进则回退,直到找到终点或遍历所有可能路径。文中还给出了C语言、Java和Python的实现代码,并展示了运行结果。
152 0
|
3月前
|
算法 程序员
时间复杂度和空间复杂度的概念
本文介绍了如何评估算法的执行效率和内存占用,重点讲解了时间复杂度和空间复杂度的概念及其计算方法。通过大O记法,可以量化算法的运行时间和内存使用情况,从而在不同算法间做出合理选择。
126 0
|
3月前
|
算法 Java C语言
汉诺塔问题
汉诺塔问题源自印度古老传说,涉及将一组圆盘从一根柱子移动到另一根,遵循特定规则。文章详细介绍了该问题的背景、解决思路以及如何使用分治算法实现,同时提供了C语言、Java和Python的代码示例。
257 0
|
3月前
|
存储 算法
最小生成树的概念与思想
数据结构中的图存储结构包含顶点和边,分为连通图与非连通图。生成树是包含所有顶点、任意两点间仅有一条通路的极小连通图。最小生成树则是权值总和最小的生成树,常用于解决道路建设等实际问题,常用算法有普里姆算法和克鲁斯卡尔算法。
102 0
|
3月前
|
存储 算法 Java
寻找图中是否存在路径
本节介绍了如何使用并查集判断图中两个顶点之间是否存在有效路径。通过将图的边信息存储到并查集中,同一连通区域的顶点会处于同一集合。只需比较两个顶点的根节点是否相同,即可判断它们是否连通。文中提供了C语言、Java和Python的实现代码,并通过示例验证了算法的正确性。
113 0