并查集

约 1514 字大约 5 分钟

并查集

概念

  • 用于存储不相交的集合
  • 可以高效支持两种操作
    • Find(x):返回 x 所属集合的“代表”
    • Union(x, y): 合并包含 x 和 y 的两个集合
  • 两种操作都可以在常量时间内完成
  • 简单实现
  • 主要思想:用有根树表示每个集合
    • 每个节点都维护着一个到它的父节点的链接
    • 根节点是对应集合的“代表”
    • 例如: 两个集合{x, y, z} and {a, b, c, d}An image

代码实现

int Find(int x) {
    // 自己不是根节点,递归查找
    while(x != L[x]) x = L[x];
    return x;
}
void Union(int x, int y) {
    // 直接连接
    L[Find(x)] = Find(y);
}

Find(x)

  1. 跟随从 x 的连接直到一个节点指向自己,这可能需要 O(n) 时间,但我们会让它更快 An image
  2. Quick-findopen in new window,在查找时候直接返回id[p]
public class QuickFindUF {
    private int[] id;    // id[i] = component identifier of i
    private int count;   // number of components
    // 其他省略
    public int find(int p) {
        validate(p);
        return id[p];
    }

    public void union(int p, int q) {
      validate(p);
      validate(q);
      int pID = id[p];   // needed for correctness
      int qID = id[q];   // to reduce the number of array accesses

      // p and q are already in the same component
      if (pID == qID) return;

      for (int i = 0; i < id.length; i++)
          // 这里性能比较低,需要全部都修改
          if (id[i] == pID) id[i] = qID;
      count--;
  }
}

Union(x, y)

  1. 运行 Find(x) 和 Find(y) 以找到对应的根节点并将一个指向另一个 An image
  2. Quick-unionopen in new window
public class QuickUnionUF {
  private int[] parent;  // parent[i] = parent of i
  private int count;     // number of components

  public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    // 这里只变更了一个节点,所以需要变更的是根节点
    parent[rootP] = rootQ;
    count--;
  }

  public int find(int p) {
      validate(p);
      while (p != parent[p])
          p = parent[p];
      return p;
    }
}

Weighted quick-union

  1. 主要是将小子树挂到大子树上面 An image
  2. 代码open in new window实现
public class WeightedQuickUnionUF {
  private int[] parent;   // parent[i] = parent of i
  private int[] size;     // size[i] = number of elements in subtree rooted at i
  private int count;      // number of components

  public int find(int p) {
    validate(p);
    while (p != parent[p])
        p = parent[p];
    return p;
  }

  public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    // 让小节点挂在到大节点上
    // make smaller root point to larger one
    if (size[rootP] < size[rootQ]) {
        parent[rootP] = rootQ;
        size[rootQ] += size[rootP];
    }
    else {
        parent[rootQ] = rootP;
        size[rootP] += size[rootQ];
    }
    count--;
  }
}

Path Compression

  • 在糟糕的情况下,树可能会变得太深
    • 这会减慢未来的运行速度
  • 每次调用Find() 时,路径压缩都会使树变浅
  • 只要根保持不变,我们不在乎树的样子
    • Find(x)返回根后,回溯到 x 并将所有链接重新路由到根

Path-Compression实现

我们将假设链接存储在 L[]

int Find(int x) {
    if(x == L[x]) return x;
    int root = Find(L[x]);
    // 路径压缩
    L[x] = root;
    return root;
}
int Find(int x) {
    return x == L[x] ? x : L[x] = Find(L[x]);
}

Java代码下载

<!-- https://mvnrepository.com/artifact/edu.princeton.cs/algs4 -->
<dependency>
    <groupId>edu.princeton.cs</groupId>
    <artifactId>algs4</artifactId>
    <version>1.0.4</version>
</dependency>

通用代码

  1. 设计数据结构
public class UF {
    private int[] parent;  // parent[i] = parent of i
    private byte[] rank;   // rank[i] = rank of subtree rooted at i (never more than 31)
    private int count;
}
  1. 初始化数据
public UF(int n) {
    if (n < 0) throw new IllegalArgumentException();
    count = n;
    parent = new int[n];
    rank = new byte[n];
    for (int i = 0; i < n; i++) {
        // 自己指向自己
        parent[i] = i;
        rank[i] = 0;
    }
}
  1. 可以忽略不重要代码
// 判断索引
private void validate(int p) {
    int n = parent.length;
    if (p < 0 || p >= n) {
        throw new IllegalArgumentException("index " + p + " is not between 0 and " + (n-1));
    }
}

Find(x)代码

/**
* Returns the component identifier for the component containing site {@code p}.
*
* @param  p the integer representing one site
* @return the component identifier for the component containing site {@code p}
* @throws IllegalArgumentException unless {@code 0 <= p < n}
*/
public int find(int p) {
  // 校验索引
  validate(p);
  // 如果当前元素的父亲不是自己,继续查找
  while (p != parent[p]) {
      // 路径压缩
      parent[p] = parent[parent[p]];    // path compression by halving
      p = parent[p];
  }
  // 找到了根节点
  return p;
}

Union(x, y)代码

/**
* Merges the component containing site {@code p} with the
* the component containing site {@code q}.
*
* @param  p the integer representing one site
* @param  q the integer representing the other site
* @throws IllegalArgumentException unless
*         both {@code 0 <= p < n} and {@code 0 <= q < n}
*/
public void union(int p, int q) {
  // 查找p的根
  int rootP = find(p);
  // 查找q的根
  int rootQ = find(q);
  // 如果相等,已经连接过了
  if (rootP == rootQ) return;

  // 集合数量多的不变,少的将父亲指向多的
  // make root of smaller rank point to root of larger rank
  if      (rank[rootP] < rank[rootQ]) parent[rootP] = rootQ;
  else if (rank[rootP] > rank[rootQ]) parent[rootQ] = rootP;
  else {
      // 子树数量相等
      parent[rootQ] = rootP;
      // 等级+1
      rank[rootP]++;
  }
  // 集合数量减1
  count--;
}

Go实现open in new window

  1. 创建数据结构
type UF struct {
  parent, rank []int
  nComponents  int
}
  1. 初始化数据结构
func newUF(initSize int) *UF {
  // 初始化数据结构
  uf := &UF{
    parent:      make([]int, initSize),
    rank:        make([]int, initSize),
    nComponents: initSize,
  }
  // 子树等级初始化
  for index := range uf.rank {
    uf.rank[index] = 0
  }
  // 自己的父亲指向自己
  for index := range uf.parent {
    uf.rank[index] = index
  }
  return uf
}
  1. Find(x)
func (uf *UF) find(item int) int {
  for item != uf.parent[item] {
    // 路径压缩
    uf.parent[item] = uf.parent[uf.parent[item]]
    // 向上走
    item = uf.parent[item]
  }
  return item
}
  1. Union(x, y)
func (uf *UF) merge(p, q int) {
  // 查找p的根
  rootP := uf.find(p)
  // 查找q的根
  rootQ := uf.find(q)
  // 已经在一个集合里面了
  if rootQ == rootP {
    return
  }
  subTreeP := uf.rank[rootP]
  subTreeQ := uf.rank[rootQ]
  if subTreeP < subTreeQ {
    uf.parent[rootP] = rootQ
  } else if subTreeP > subTreeQ {
    uf.parent[rootQ] = rootP
  } else {
    // 子树等级相同,使用那个都行
    // uf.parent[rootP]  = rootQ
    // uf.parent[rootQ]  = rootP
    uf.parent[rootQ] = rootP
    uf.rank[rootP]++
  }
  // 集合个数减少一个
  uf.nComponents--
}

总结

参考材料