第十一章:图论part05
并查集理论基础
并查集理论基础很重要,明确并查集解决什么问题,代码如何写,对后面做并查集类题目很有帮助。
www.programmercarl.com/kamacoder/%…
并查集(Union-Find)是一种高效的数据结构,主要用于处理不相交集合的合并及查询问题。它广泛应用于图论、网络连通性等领域。下面是并查集的基本概念、操作及其应用。
1. 基本概念
并查集主要包含两个核心操作:
- 查找(Find):确定某个元素属于哪个集合。
- 合并(Union):将两个集合合并成一个集合。
2. 数据结构
并查集通常使用数组来表示每个元素的父节点。具体来说:
parent[i]表示元素i的父节点。- 初始时,每个元素的父节点指向自己,表示每个元素自成一个集合。
3. 主要操作
3.1 查找操作(Find)
- 查找一个元素的根节点,返回该元素所属的集合。
- 为了提高效率,通常会使用 路径压缩。即在查找过程中,将沿途的所有节点直接连接到根节点,从而扁平化结构,降低后续查找的时间复杂度。
function find(x) {
if (parent[x] !== x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
3.2 合并操作(Union)
- 将两个元素的集合合并为一个集合。为了防止树的深度过大,通常会使用 按秩合并(Union by rank),即将较小树的根节点连接到较大树的根节点上。
- 可以在合并操作中先查找两个元素的根节点,然后根据秩决定如何合并。
function union(x, y) {
const rootX = find(x);
const rootY = find(y);
if (rootX !== rootY) {
// 按秩合并
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++; // 更新秩
}
}
}
4. 时间复杂度
- 查找和合并操作的时间复杂度几乎是常数级的,具体为 (O(\alpha(n))),其中 (\alpha) 是阿克曼函数的反函数,非常缓慢增长,因此在实际应用中几乎可以认为是常数时间。
5. 应用场景
- 连通分量:在无向图中,用于判断两个节点是否在同一个连通分量中。
- 网络连接:在网络中,检查是否存在某种连接。
- 动态连通性:在动态变化的图中,实时更新连接关系。
6. 示例
假设我们有一些元素和连接:
- 初始状态:每个元素自成一个集合。
- 进行合并操作:将两个元素连接。
- 查询某个元素的集合:找出该元素的根节点。
代码示例
下面是一个完整的并查集示例代码,包括路径压缩和按秩合并的实现:
class UnionFind {
constructor(n) {
this.parent = Array(n).fill(0).map((_, i) => i); // 初始化父节点
this.rank = Array(n).fill(1); // 初始化秩
}
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]); // 路径压缩
}
return this.parent[x];
}
union(x, y) {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX !== rootY) {
// 按秩合并
if (this.rank[rootX] > this.rank[rootY]) {
this.parent[rootY] = rootX;
} else if (this.rank[rootX] < this.rank[rootY]) {
this.parent[rootX] = rootY;
} else {
this.parent[rootY] = rootX;
this.rank[rootX]++;
}
}
}
connected(x, y) {
return this.find(x) === this.find(y); // 判断是否在同一集合
}
}
通过这个示例,可以很清楚地看到并查集的工作原理以及如何使用它来解决实际问题。
寻找存在的路径
并查集裸题,学会理论基础后,本题直接可以直接刷过
www.programmercarl.com/kamacoder/0…
简单合并
/**
* @param {number} n - 节点数量
* @param {number[][]} edges - 边的数组
* @param {number} source - 起始节点
* @param {number} destination - 目标节点
* @return {boolean} - 是否存在从 source 到 destination 的路径
*/
var validPath = function (n, edges, source, destination) {
// 初始化父节点数组,father[i] 表示节点 i 的父节点
let father = Array(n).fill(0);
for (let i = 0; i < n; i++) {
father[i] = i; // 每个节点的父节点初始化为自己
}
// 查找函数,带路径压缩
function find(u) {
if (u !== father[u]) {
father[u] = find(father[u]); // 路径压缩
}
return father[u];
}
// 检查两个节点是否在同一集合中
function isSame(u, v) {
return find(u) === find(v); // 直接使用 find 函数
}
// 合并两个节点所在的集合
function join(u, v) {
u = find(u);
v = find(v);
if (u === v) return; // 如果已经在同一集合中,无需合并
father[u] = v; // 将 u 的父节点设为 v
}
// 遍历所有边,将它们合并到同一集合
for (let i = 0; i < edges.length; i++) {
join(edges[i][0], edges[i][1]);
}
// 判断 source 和 destination 是否在同一集合中
return isSame(source, destination);
};
按秩合并
/**
* @param {number} n
* @param {number[][]} edges
* @param {number} source
* @param {number} destination
* @return {boolean}
*/
var validPath = function (n, edges, source, destination) {
// 初始化父节点数组和秩数组
const parent = Array(n).fill(0).map((_, index) => index);
const rank = Array(n).fill(1);
// 查找操作,带路径压缩
function find(x) {
if (parent[x] !== x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
// 合并操作,按秩合并
function union(x, y) {
const rootX = find(x);
const rootY = find(y);
if (rootX !== rootY) {
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
// 合并所有边
for (const [u, v] of edges) {
union(u - 1, v - 1); // 处理 1-indexed 到 0-indexed; 如果是从1到n的话需要这么处理; 如果是0到n-1则不需要!
}
// 查找 source 和 destination 的根节点
return find(source - 1) === find(destination - 1); // 处理 1-indexed 到 0-indexed
};
// 示例
const n = 5;
const edges = [[1, 2], [1, 3], [2, 4]];
const source = 1;
const destination = 4;
console.log(validPath(n, edges, source, destination)); // 输出 true