本文最后更新于:2023年11月4日 下午
问题描述 Union-Find 算法用于确定网络中任意给定的两个触点是否相通。这样说可能不好理解,所以让我们通过一个简单的问题来了解一下它。 看到下面这张图,图上有九个点,现在任意给出两个点,我们需要快速判断这两个点是否相连,或者说这两个点是否相通,有什么好办法吗? 为了更好理解这个问题,我们可以将九个点转化为 0-8 这九个数字,并把其当成一个数组来看待,然后再假设问题的输入就是一些元组,如(1, 4)
,就代表 1 和 4 之间是连通的。 就以上面图为参考,我们想要知道相邻的两个点是否连通是很容易的,比如 0 和 1;但是,如果想要知道 0 和 4 是否是连通的,该用什么方法呢?对输入进行dfs
或是bfs
吗?那太复杂了,毕竟我们不需要给出两点间的详细路径,只需要知道两者是否连通,没必要浪费资源,所以我们需要换种思路。
Union-Find 算法的 API 为此,我们需要先为这个算法设计一些 API,这些操作既可以处理输入,也能够解决问题。下面给出UnionFind
类的基本框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class UnionFind { private int []? _id; private int _count; public UnionFind (int N ) { _count = N; _id = new int [N]; for (int i = 0 ; i < N; i++) { _id[i] = i; } } public int Count () { return _count; } public bool Connected (int p, int q ) { return Find(p) == Find(q); } public int Find (int p ) { throw new NotImplementedException(); } public void Union (int p, int q ) { throw new NotImplementedException(); } }
可以看到框架中有两个方法还未实现,这两个方法其实正是这个算法的关键,而我们接下来要干的事就是实现这两个方法。
Union 和 Find 方法的实现 quick-find 算法 该算法的思路就是保证当且仅当id[p]
等于id[q]
时p
和q
是连通的,换句话说,在同一个连通分量里的所有触点在id[]
中的值必须全部相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public uint Find (uint p ) { return _id![p]; }public void Union (uint p, uint q ) { uint pId = Find(p); uint qId = Find(q); if (pId == qId) { return ; } for (uint i = 0 ; i < _id!.Length; i++) { if (_id[i] == pId) { _id[i] = qId; } } _count--; }
quick-union 算法 这个算法的思路其实就是通过构造森林 来确定连通分量和两个触点是否相连。下面的图将有助于理解该算法: 也就是说,我们调用Find
方法的话,应该沿着其找到其所在分量的标识符,也就是根触点。而调用Union
方法对p
和q
进行归并的话,就利用Find
方法找到两个触点的根触点,若不同,更改其中一个根触点的标识符为另一个根触点即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public uint Find (uint p ) { while (p != _id![p]) { p = _id[p]; } return p; }public void Union (uint p, uint q ) { uint pRoot = Find(p); uint qRoot = Find(q); if (pRoot == qRoot) { return ; } _id![pRoot] = qRoot; _count--; }
Union 和 Find 方法的优化 加权quick-union 当然,普通的quick-union
算法是不会判断树的大小的,所以可能会出现一颗大树归并到小树上,或者出现更糟糕的情况:0链接到1,1链接到2,如此下去。 那么,为了改进我们的union
方法,我们现在需要记录一下每棵树的大小,并且总是将小树连接到大树上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 public class WeightedUnionFind { private uint [] _id; private uint _count; private int [] _size; public WeightedUnionFind (uint N ) { _count = N; _id = new uint [N]; _size = new int [N]; for (uint i = 0 ; i < N; i++) { _id[i] = i; _size[i] = 1 ; } } public uint Count () { return _count; } public bool Connected (uint p, uint q ) { return Find(p) == Find(q); } public uint Find (uint p ) { while (p != _id[p]) { p = _id[p]; } return p; } public void Union (uint p, uint q ) { uint pRoot = Find(p); uint qRoot = Find(q); if (pRoot == qRoot) { return ; } if (_size[pRoot] < _size[qRoot]) { _id[p] = q; _size[qRoot] += _size[pRoot]; } else { _id[q] = p; _size[pRoot] += _size[qRoot]; } _count--; } }
路径压缩 理想情况下,我们希望每个节点都直接链接到它的根节点上。为了实现这种理想情况,只需要为Find()
添加一个循环,将在路径上遇到的所有节点都直接链接到根节点。这样我们所得到的结果是几乎完全扁平化的树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 public class WeightedPathCompressUnionFind { private uint [] _id; private uint _count; private int [] _size; public WeightedPathCompressUnionFind (uint N ) { _count = N; _id = new uint [N]; _size = new int [N]; for (uint i = 0 ; i < N; i++) { _id[i] = i; _size[i] = 1 ; } } public uint Count () { return _count; } public bool Connected (uint p, uint q ) { return Find(p) == Find(q); } public uint Find (uint p ) { uint root = p; while (root != _id[root]) { root = _id[root]; } while (p != root) { uint temp = _id[p]; _id[p] = root; p = temp; } return p; } public void Union (uint p, uint q ) { uint pRoot = Find(p); uint qRoot = Find(q); if (pRoot == qRoot) { return ; } if (_size[pRoot] < _size[qRoot]) { _id[p] = q; _size[qRoot] += _size[pRoot]; } else { _id[q] = p; _size[pRoot] += _size[qRoot]; } _count--; } }
编写的测试方法 本来想说实现一下读取文件的,但是搁置了(逃
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 void UnionFindTest () { uint N = 9 ; try { Console.WriteLine("Please enter a number(e.g: 9):" ); N = UInt32.Parse(Console.ReadLine()); } catch (FormatException) { Console.WriteLine("Please enter a correct number." ); return ; } UnionFind unionFind = new UnionFind(N); Console.WriteLine("Please enter p and q(e.g: 1 4):" ); string ? numbers = "" ; while (!string .IsNullOrEmpty(numbers = Console.ReadLine())) { try { uint [] tmp = Array.ConvertAll(numbers.Split(" " ), uint .Parse); if (tmp.Length != 2 || tmp[0 ] >= N || tmp[1 ] >= N) { Console.WriteLine("Please check your input.(Wrong format)" ); continue ; } uint p = tmp[0 ]; uint q = tmp[1 ]; if (unionFind.Connected(p, q)) { continue ; } unionFind.Union(p, q); } catch { Console.WriteLine("Please check your input.(Not a num)" ); } } Console.WriteLine(unionFind.Count() + " components." ); }
希望本文章能够帮到您~