union-find算法详解及应用(Percolation 评分100)

it2025-09-19  6

union-find 算法

动态连通性

首先详细说明算法要解决的问题:问题的输入是一列整数对,其中每个整数都表示一个某种类型的对象,(当然,输入的这一列整数对应该是同一类型的对象)一对整数 p q 可以被理解为 “p 和 q 是相连的”。假设相连是一种等价关系,这也就意味着它具有:

自反性:p 和 p 是相连的。对称性:如果 p 和 q 是相连的,那么 q 和 p 也是相连的。传递性:如果 p 和 q 是相连的且 q 和 r 是相连的,那么 p 和 r 也是相连的。

等价关系能够将对象分为多个等价类。在本问题中,当且仅当两个对象相连时它们才属于同一个等价类。我们需要设计一个数据结构来保存所有的已知的整数对信息,并用它们来判断一对新对象是否是相连的。我们将这个问题通俗的叫做动态连通性问题。

它可以解决的问题:判断计算机网络中两个计算机是否相连,社交网络中两个人是否相识等。

实现

union-find API

public class UFUF(int N)以整数标识(0 到 N-1 )初始化 N 个触点void union(int p, int q)在 p 和 q 之间添加一条连接int find(int p)p所在的分量(等价类)的标识符boolean connected(int p, int q)如果 p 和 q 存在于同一个分量中则返回 trueint count()连通分量的数量

下面我们应该做的是:

定义一种数据结构表示已知的连接;基于此数据结构实现高效的 union() find() connected()和 count() 方法。

API已经说明触点和分量都会用 int 值表示,所以我们可以用一个以触点为索引的数组 id[] 作为基本数据结构来表示所有分量。我们将触点 i 所在的分量信息保存在id[i] 中,connected() 方法的实现只用一条语句 find(p) == find(q),find() 和 union() 的实现是本节剩余内容要讨论的主题。

quick-find 算法

这种方法保证当且仅当 id[p] 等于 id[q] 时 p 和 q 是连通的。换句话说,在同一个连通分量中的所有触点在 id[] 中的值必须全部相同。所以我们在合并在不同分量中的两个触点时,必须将两个集合中所有触点所对应的 id[] 元素变为同一个值。 根据上述文字我们得到 find() 和 union() 的代码简单明了。

public int find(int p){ return id[p]; } public void union(int p, int q) { // 将 p 和 q 归并到相同的分量中 pID = find(p); qID = fing(q); // 如果 p 和 q 已经在相同的分量中则不需要采取任何行动 if (pID == qID) return; // 将 p 的分量重命名为 q 的名称 for (int i = 0; i < id.length; i++) if (id[i] == pID) id[i] = qID; count--; }

find() 操作的速度显然是很快的,因为它只需要访问 id[] 数组一次,但quick-find算法一般无法处理大型问题,因为对于每一对输入 union() 都需要扫描整个id[] 数组,quick-find 算法的运行时间对于最终只能得到少数连通分量的一般应用是平方级别的,所以,我们需要寻找更好的算法。

quick-union 算法

我们要讨论的这个算法重点是提高 union() 的速度。它也基于相同的数据结构——以触点作为索引的 id[] 数组,但我们赋予这些值的意义不同,我们需要用它们来定义更加复杂的结构。确切的说,每个触点对应的 id[] 元素都是同一个分量中另一个触点的名称(也可能是它自己)——我们将这种联系称为链接。

在实现 find() 方法时,我们从给定的触点开始,由它的链接得到另一个触点,跟随着链接直到到达一个根触点,即链接指向自己的触点。当且仅当分别由两个触点开始的这个过程到达了同一个根触点时,证明它们存在于同一个连通分量之中。

为了保证 find() 方法的有效性,我们需要 union(p, q) 来保证这一点。它的实现很简单:我们由 p 和 q 的链接分别找到它们的根触点,然后只需将一个根触点链接到另一个,即可将一个分量重命名为另一个分量。 根据上述文字我们得到 find() 和 union() 方法如下:

public int find(int p) { // 找出分量的名称 while (p != id[p]) p = id[p]; reurn p; } public void union(int p, int q) { // 将 p 和 q 的根节点统一 int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) return; id[pRoot] = qRoot; count--; }

目前,我们可以将 quick-union 算法看作是 quick-find 算法的一种改良,因为它解决了 quick-find 中最主要的问题(union() 操作总是线性级别的)。对于一般的输入数据这显然是一次改进,但 quick-union 算法仍存在问题 ,我们不能保证在所有情况下它都能比 quick-find 算法快得多。在最坏情况下,算法的运行时间是平方级别的。

加权的 quick-union 算法

幸好,我们只需要简单地修改 quick-union 算法就能保证这样糟糕的情况不再出现。与其在 union() 中随意将一棵树连接到另一棵树,我们现在会记录每一棵树的大小并总是将较小的树连接到较大的树上。这项改动需要添加一个数组和一些代码来记录每一个树中的节点数,我们称它为加权 quick-union 算法。

完整算法如下:

public class WeightedQuickUnionUF { private int[] id; // 父链接数组(由触点索引) private int[] sz; // (由触点索引的)各个根节点所对应的分量的大小 private int count; // 连通分量的数量 public WeightedQuickUnionUF(int N) { count = N; id = new int[N]; for (int i = 0; i < id.length; i++) id[i] = i; sz = new int[N]; for (int i = 0; i < sz.length; i++) sz[i] = 1; } public int count(){ return count; } public boolean connected(int p, int q){ return finf(p) == find(q); public int find(int p) { // 找出分量的名称 while (p != id[p]) p = id[p]; reurn p; } public void union(int p, int q) { // 将 p 和 q 的根节点统一 int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) return; // 将小树的根节点连接到大树的根节点 if (sz[pRoot] < sz[qRoot]) { id[pRoot] = qRoot; sz[qRoot] += sz[pRoot]; } else { id[qRoot] = pRoot; sz[pRoot] += sz[qRoot]; } count--; }

可以证明,加权 quick-union 算法构造的森林中任意节点的深度最多为lgN。

进一步优化:路径压缩,优化 find() 方法,在 find() 中添加一个平循环,将在路径上遇到的所有节点都直接链接到根节点,这样我们得到的是几乎完全扁平化的树,和 quick-find 算法在理想情况下所得到的树非常接近。

平循环:

public int find(int p) { if (id[p] == p) return; else{ id[p] = find(id[p]); return id[p]; } }

应用(Percolation)

该应用为Coursera中algs4的作业,详细的问题描述点这里

实现代码(评分100,backwash问题看这里):

Percolation.java

import edu.princeton.cs.algs4.WeightedQuickUnionUF; /** * @Author: xiaojiang * @DATE: Created in 8:52 2020/9/30 */ public class Percolation { private final int dimension; private final boolean[] grids; private int openSites = 0; private final WeightedQuickUnionUF uf; private final WeightedQuickUnionUF siteIsFull; // creates n-by-n grid, with all sites initially blocked public Percolation(int n) { // Throw an IllegalArgumentException in the constructor if n ≤ 0. if (n <= 0) throw new IllegalArgumentException("error range"); dimension = n; uf = new WeightedQuickUnionUF(dimension*dimension + 2); siteIsFull = new WeightedQuickUnionUF(dimension*dimension + 1); grids = new boolean[dimension*dimension + 1]; // array elements are false } // opens the site (row, col) if it is not open already public void open(int row, int col) { if (row < 1 || row > dimension || col < 1 || col > dimension) throw new IllegalArgumentException("error range"); if (isOpen(row, col)) return; else { grids[map(row, col)] = true; openSites++; } // up if (row - 1 > 0) { if (isOpen(row - 1, col)) { uf.union(map(row, col), map(row - 1, col)); siteIsFull.union(map(row, col), map(row - 1, col)); } } else { uf.union(map(row, col), 0); siteIsFull.union(map(row, col), 0); } // down if (row + 1 <= dimension) { if (isOpen(row + 1, col)) { uf.union(map(row, col), map(row + 1, col)); siteIsFull.union(map(row, col), map(row + 1, col)); } } else uf.union(map(row, col), dimension * dimension + 1); // left if (col - 1 > 0) { if (isOpen(row, col - 1)) { uf.union(map(row, col), map(row, col) - 1); siteIsFull.union(map(row, col), map(row, col) - 1); } } // right if (col + 1 <= dimension) { if (isOpen(row, col + 1)) { uf.union(map(row, col), map(row, col) + 1); siteIsFull.union(map(row, col), map(row, col) + 1); } } } // is the site (row, col) open? public boolean isOpen(int row, int col) { if (row < 1 || row > dimension || col < 1 || col > dimension) throw new IllegalArgumentException("error range"); return grids[map(row, col)]; } // is the site (row, col) full? public boolean isFull(int row, int col) { if (row < 1 || row > dimension || col < 1 || col > dimension) throw new IllegalArgumentException("error range"); return siteIsFull.find(map(row, col)) == siteIsFull.find(0); } // returns the number of open sites public int numberOfOpenSites() { return openSites; } // does the system percolate? public boolean percolates() { return uf.find(0) == uf.find(dimension*dimension + 1); } // map from 2D to 1D indices private int map(int row, int col) { return (row-1)*dimension + col; } }

PercolationStats.java

import edu.princeton.cs.algs4.StdOut; import edu.princeton.cs.algs4.StdRandom; import edu.princeton.cs.algs4.StdStats; /** * @Author: xiaojiang * @DAtE: Created in 9:27 2020/9/30 */ public class PercolationStats { private static final double CONFIDENCE_95 = 1.96; private final int t; private final double[] openSitesNumber; // perform independent trials on an n-by-n grid public PercolationStats(int n, int trials) { if (n <= 0 || trials <= 0) throw new IllegalArgumentException("error range"); int dim = n; t = trials; openSitesNumber = new double[t]; int row = 0; int col = 0; for (int i = 0; i < t; i++) { Percolation model = new Percolation(dim); while (!model.percolates()) { row = StdRandom.uniform(1, dim + 1); col = StdRandom.uniform(1, dim + 1); if (model.isOpen(row, col)) continue; model.open(row, col); } openSitesNumber[i] = (double) model.numberOfOpenSites() / (dim * dim); } } // sample mean of percolation threshold public double mean() { return StdStats.mean(openSitesNumber); } // sample standard deviation of percolation threshold public double stddev() { return StdStats.stddev(openSitesNumber); } // low endpoint of 95% confidence interval public double confidenceLo() { return (mean() - CONFIDENCE_95 * stddev() / Math.sqrt(t)); } // high endpoint of 95% confidence interval public double confidenceHi() { return (mean() + CONFIDENCE_95 * stddev() / Math.sqrt(t)); } // test client (see below) public static void main(String[] args) { PercolationStats p = new PercolationStats(Integer.parseInt(args[0]), Integer.parseInt(args[1])); StdOut.printf("mean = %f\n", p.mean()); StdOut.printf("stddev = %f\n", p.stddev()); StdOut.printf("95%%confidence interval = [%f,%f]", p.confidenceLo(), p.confidenceHi()); } }
最新回复(0)