Skip to content
— algorithm

Union Find

并查集

定义

并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。

顾名思义,并查集支持两种操作:

  • 合并(Union):合并两个元素所属集合(合并对应的树)
  • 查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合

定义来源:https://oi-wiki.org/ds/dsu/ 链接里包含并查集在C++和python的实现,以下为golang实现方式

实现

go

type UnionFind struct {
	parent []int //根节点记录数组,下标为子节点,值为根节点下标
	size   []int //子集中数量,可以按需设计,比如改成height
}
//初始化并查集
func NewUnionFind(n int) *UnionFind {
	parent := make([]int, n)
	size := make([]int, n)
	for i := 0; i < n; i++ {
		parent[i] = i
		size[i] = 1
	}
	return &UnionFind{parent: parent, size: size}
}
//向并查集放入新关系
func (uf *UnionFind) Union(u, v int) {
	rootX, rootY := uf.Find(u), uf.Find(v)//查找要关联的两个节点的根节点
	if rootX == rootY {
		//一致表明添加过,所以不再重新添加,size也不用更新[^1]
		return
	}
	if uf.size[rootX] < uf.size[rootY] {//不一致并且子节点的根节点大于链接对象根节点的,将该节点的根节点作为共同的根节点,切换关系
		rootX, rootY = rootY, rootX
	}
	uf.parent[rootY] = rootX//绑定链接对象到目标节点的根节点
	uf.size[rootX] += uf.size[rootY] //子集总数合并,特殊场景,其他场景按需设计
}
// 查找i节点的根节点
func (uf *UnionFind) Find(i int) int {
	if uf.parent[i] != i {//存在根节点,递归更新,路径压缩,直接将路径的子节点的父节点指向根节点
		uf.parent[i] = uf.Find(uf.parent[i])
	}
	return uf.parent[i] //返回存储在parent的根节点
}
func (uf *UnionFind) Size(i int) int {
	return uf.size[uf.Find(i)] // 在该场景中,size查询的是节点i所在的子集里节点的总数
}

代码演示

Go 实现

go
package main

import "fmt"

type UnionFind struct {
	parent []int
	rank   []int
}

func NewUnionFind(n int) *UnionFind {
	parent := make([]int, n)
	rank := make([]int, n)
	for i := range parent {
		parent[i] = i
	}
	return &UnionFind{parent: parent, rank: rank}
}

func (uf *UnionFind) Find(x int) int {
	if uf.parent[x] != x {
		uf.parent[x] = uf.Find(uf.parent[x])
	}
	return uf.parent[x]
}

func (uf *UnionFind) Union(x, y int) bool {
	px, py := uf.Find(x), uf.Find(y)
	if px == py {
		return false
	}
	if uf.rank[px] < uf.rank[py] {
		px, py = py, px
	}
	uf.parent[py] = px
	if uf.rank[px] == uf.rank[py] {
		uf.rank[px]++
	}
	return true
}

func (uf *UnionFind) Connected(x, y int) bool {
	return uf.Find(x) == uf.Find(y)
}

// 运行示例输出:
//
//	0 and 2 connected: true
//	0 and 3 connected: false
//	3 and 4 connected: true
//	1 and 5 connected: false
//	after union(2,3):
//	0 and 4 connected: true
func main() {
	uf := NewUnionFind(6)

	uf.Union(0, 1)
	uf.Union(1, 2)
	uf.Union(3, 4)

	fmt.Println("0 and 2 connected:", uf.Connected(0, 2)) // true
	fmt.Println("0 and 3 connected:", uf.Connected(0, 3)) // false
	fmt.Println("3 and 4 connected:", uf.Connected(3, 4)) // true
	fmt.Println("1 and 5 connected:", uf.Connected(1, 5)) // false

	uf.Union(2, 3)
	fmt.Println("after union(2,3):")
	fmt.Println("0 and 4 connected:", uf.Connected(0, 4)) // true
}

JS 性能对比

ts
// 并查集性能对比:naive vs. 路径压缩 + 按秩合并

interface UnionFind {
  find: (x: number) => number
  union: (x: number, y: number) => void
}

function makeNaive(n: number): UnionFind {
  const parent = Array.from({ length: n }, (_, i) => i)
  function find(x: number): number {
    while (parent[x] !== x) x = parent[x]
    return x
  }
  function union(x: number, y: number): void {
    parent[find(x)] = find(y)
  }
  return { find, union }
}

function makeOptimized(n: number): UnionFind {
  const parent = Array.from({ length: n }, (_, i) => i)
  const rank = Array.from({ length: n }).fill(0) as number[]
  function find(x: number): number {
    while (parent[x] !== x) {
      parent[x] = parent[parent[x]] // path halving
      x = parent[x]
    }
    return x
  }
  function union(x: number, y: number): void {
    const px = find(x)
    const py = find(y)
    if (px === py)
      return
    if (rank[px] < rank[py]) {
      parent[px] = py
    }
    else if (rank[px] > rank[py]) {
      parent[py] = px
    }
    else {
      parent[py] = px
      rank[px]++
    }
  }
  return { find, union }
}

// 简单 LCG 伪随机,保证两次跑相同操作序列
function createRng(seed: number) {
  return () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 0x100000000)
}

function runBenchmark(
  uf: UnionFind,
  n: number,
  ops: { type: 'union' | 'find', x: number, y?: number }[],
): number {
  const start = performance.now()
  for (const op of ops) {
    if (op.type === 'union')
      uf.union(op.x, op.y!)
    else
      uf.find(op.x)
  }
  return performance.now() - start
}

const N = 50_000
const OP_COUNT = 200_000
const rng = createRng(42)
const ops: { type: 'union' | 'find', x: number, y?: number }[] = []
for (let i = 0; i < OP_COUNT; i++) {
  const type = rng() < 0.3 ? 'union' : 'find'
  const x = (rng() * N) | 0
  const y = (rng() * N) | 0
  ops.push(type === 'union' ? { type: 'union', x, y } : { type: 'find', x })
}

const naiveTime = runBenchmark(makeNaive(N), N, ops)
const optimizedTime = runBenchmark(makeOptimized(N), N, ops)

// 输出性能对比结果(使用 warn 以便在控制台可见)
// 示例输出:
//   n=50,000, 200,000 次操作
//   naive:     1409.34 ms
//   optimized: 5.17 ms
//   加速比:    272.74x
console.warn(`n=${N.toLocaleString()}, ${OP_COUNT.toLocaleString()} 次操作`)
console.warn(`naive:     ${naiveTime.toFixed(2)} ms`)
console.warn(`optimized: ${optimizedTime.toFixed(2)} ms`)
console.warn(`加速比:    ${(naiveTime / optimizedTime).toFixed(2)}x`)

Last updated:

冲吧,向那太阳,向那片海