冗余连接问题(非常详细,图文并茂)
接触过数据结构的读者都知道,数据结构里囊括了很多种存储数据的方案,包括线性表、树、图等。线性表的特征非常明显,很容易辨识,树和图是初学者经常混淆的。
在数据结构中,所有的树都可以看做是图型结构,但反过来,只有「连通且不存在环路的无向图」可以看做是树型结构。例如,观察下方哪个是树型结构,哪个是图型结构?
图 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] 这条边。
冗余连接问题可以用并查集解决,接下来给大家讲解具体的实现过程。
仍以图 2 为例,并查集解决冗余连接问题的过程是:
下面是用并查集解决冗余问题的 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 语言程序:
#include <stdio.h> #include <stdlib.h> typedef enum { false, true } bool; #define MAX_SIZE 1000 // 可以根据需要调整最大尺寸 // 并查集数组 typedef struct { int parent[MAX_SIZE]; int count; }UnionFind; // 初始化并查集 void initialize(UnionFind* uf, int size) { for (int i = 0; i <= size; i++) { uf->parent[i] = i; } uf->count = size; } // 查找根节点(代表元素) int find(UnionFind* uf, int x) { if (uf->parent[x] != x) { uf->parent[x] = find(uf, uf->parent[x]); // 路径压缩 } return uf->parent[x]; } // 合并两个元素所在的集合 void unionSets(UnionFind* uf, int x, int y) { //找到两个元素所在集合的代表元素 int xRoot = find(uf, x); int yRoot = find(uf, y); //如果代表元素不同,表明它们是两个集合,将它们合并 if (xRoot != yRoot) { uf->parent[xRoot] = yRoot; // uf->parent[yRoot] = xRoot; uf->count--; } } int* findRedundantConnection(int(*edgs)[2], int n) { UnionFind uf; initialize(&uf, n); // 遍历所有的边 for (int i = 0; i < n; i++) { //判断当前边是否冗余 if (find(&uf, edgs[i][0]) == find(&uf, edgs[i][1])) { return edgs[i]; } else { unionSets(&uf, edgs[i][0], edgs[i][1]); } } } int main() { int edgs[3][2] = { {1,2},{1,3},{2,3} }; //找到冗余的边 int * ret = findRedundantConnection(edgs, 3); printf("冗余的边是:[%d, %d]\n", ret[0], ret[1]); return 0; }
下面是用并查集解决冗余问题的 Java 程序:
// 并查集类,用于处理集合合并和查找操作(在 UnionFind.java 文件中) public class UnionFind { private int[] parent; // 记录节点的父节点 private int count; // 记录集合的数量 // 构造函数,初始化并查集,每个节点的父节点初始为自己,集合数量为节点个数 public UnionFind(int size) { parent = new int[size + 1]; for (int i = 0; i <= size; i++) { parent[i] = i; } count = size; } // 查找根节点(代表元素)的方法,同时进行路径压缩 public int find(int x) { if (parent[x] != x) { parent[x] = find(parent[x]); // 路径压缩 } return parent[x]; } // 合并两个元素所在的集合 public void unionSets(int x, int y) { int xRoot = find(x); int yRoot = find(y); if (xRoot != yRoot) { parent[xRoot] = yRoot; // 合并集合 count--; } } // 获取当前集合的数量 public int getCount() { return count; } } // FindRedundantConnection 类定义(在 FindRedundantConnection.java 文件中) public class FindRedundantConnection { // 查找冗余连接的方法 public int[] findRedundantConnection(int[][] edges) { UnionFind uf = new UnionFind(edges.length); int[] result = new int[2]; for (int[] edge : edges) { if (uf.find(edge[0]) == uf.find(edge[1])) { result[0] = edge[0]; result[1] = edge[1]; } else { uf.unionSets(edge[0], edge[1]); } } return result; } public static void main(String[] args) { int[][] edges = { {1, 2}, {1, 3}, {2, 3} }; FindRedundantConnection validPath = new FindRedundantConnection(); int[] result = validPath.findRedundantConnection(edges); System.out.println("冗余的边是:[" + result[0] + ", " + result[1] + "]"); } }
下面是用并查集解决冗余问题的 Python 程序:
class UnionFind: def __init__(self, size): self.parent = [i for i in range(size + 1)] self.count = size # 初始化并查集 def initialize(self, size): for i in range(size + 1): self.parent[i] = i self.count = size # 查找操作,返回元素所在集合的代表元素 def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # 路径压缩 return self.parent[x] # 合并两个集合 def union_sets(self, x, y): x_root = self.find(x) y_root = self.find(y) if x_root != y_root: self.parent[x_root] = y_root self.count -= 1 # 查找冗余连接的方法 def find_redundant_connection(edges): uf = UnionFind(len(edges)) result = [0, 0] for edge in edges: if uf.find(edge[0]) == uf.find(edge[1]): result[0], result[1] = edge[0], edge[1] else: uf.union_sets(edge[0], edge[1]) return result if __name__ == "__main__": edges = [[1, 2], [1, 3], [2, 3]] ret = find_redundant_connection(edges) print(f"冗余的边是:[{ret[0]}, {ret[1]}]")
运行程序,输出结果为:
冗余的边是:[2, 3]